mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 19:01:42 +00:00
feat(eks): gateway + handler scaffolding for EKS control plane (#267)
* feat(eks): gateway + handler scaffolding for EKS control plane
Lands the EKS service contract — gateway REST-JSON dispatcher, handler
interface + stub impl, JetStream KV bucket layout, and daemon wiring —
ahead of the K3s control-plane bodies.
Every action currently returns NotImplemented (HTTP 501 + JSON
NotImplementedException) so downstream work can call into a stable
contract while real bodies land sprint by sprint.
- Gateway: 34-action REST-JSON routing table under svc=eks, per-action
closures dispatch to handlers_eks.NATSEKSService. ErrorHandler /
writeClusterUnavailable / writeThrottleError gain EKS JSON branches so
clients see application/x-amz-json-1.1 envelopes.
- Handler: EKSService interface + NATS wrapper + stub impl with
var _ EKSService compile checks on both implementations.
- KV: eks-account-{accountID} lazy per-account bucket factory plus the
shared spinifex-eks-leader bucket (TTL 60s, History 1) bootstrapped
at daemon start. Key-path helpers cover the full Q2 layout
(cluster meta, nodegroups, access entries, OIDC providers + signing
key + JWKS, admin kubeconfig, K3s node-token, events).
- Daemon: 34 eks.<Action> subscriptions on the spinifex-workers queue
group, init via initServiceWithRetry, mirrors the ELBv2 layout.
- awserrors: new ErrorNotImplemented constant (HTTP 501).
- tags: ManagedByEKS constant for downstream SG/NLB/VM tagging.
* test(eks): cover scaffolding wrappers + daemon NATS handlers
Adds two test files closing the diff-coverage gap on the EKS
scaffolding commit:
- spinifex/gateway/eks/wrappers_test.go drives every per-action
gateway wrapper (cluster, nodegroup, access, addons, oidc, tags —
34 actions, 50+ subtest invocations) through an embedded NATS
server with a `eks.>` catch-all stub responder. Covers the
wrappers themselves plus handlers/eks/service_nats.go via the
NewNATSEKSService construction inside each wrapper. Bad-JSON
branches in unmarshalIfBody are also covered.
- spinifex/daemon/daemon_handlers_eks_test.go constructs a minimal
Daemon{eksService: real impl} and dispatches each of the 34
handleEKS* handlers via embedded NATS, asserting the NotImplemented
error code rides back through handleNATSRequest.
Lifts diff coverage from 27.3% to 62.6%, above the 60% threshold.
This commit is contained in:
@@ -339,6 +339,7 @@ var (
|
||||
ErrorNoSuchVersion = "NoSuchVersion"
|
||||
ErrorNonEBSInstance = "NonEBSInstance"
|
||||
ErrorNotExportable = "NotExportable"
|
||||
ErrorNotImplemented = "NotImplemented"
|
||||
ErrorOperationNotPermitted = "OperationNotPermitted"
|
||||
ErrorOptInRequired = "OptInRequired"
|
||||
ErrorOutstandingVpcPeeringConnectionLimitExceeded = "OutstandingVpcPeeringConnectionLimitExceeded"
|
||||
@@ -805,6 +806,7 @@ var ErrorLookup = map[string]ErrorMessage{
|
||||
ErrorNoSuchVersion: {HTTPCode: 404, Message: "The specified API version does not exist."},
|
||||
ErrorNonEBSInstance: {HTTPCode: 400, Message: "The specified instance does not support Amazon EBS. Restart the instance and try again, to ensure that the code is run on an instance with updated code."},
|
||||
ErrorNotExportable: {HTTPCode: 400, Message: "The specified instance cannot be exported. You can only export certain instances. For more information, see Considerations for instance export."},
|
||||
ErrorNotImplemented: {HTTPCode: 501, Message: "Operation not implemented"},
|
||||
ErrorOperationNotPermitted: {HTTPCode: 400, Message: "The specified operation is not allowed. This error can occur for a number of reasons; for example, you might be trying to terminate an instance that has termination protection enabled, or trying to detach the primary network interface (eth0) from an instance."},
|
||||
ErrorOptInRequired: {HTTPCode: 403, Message: "You are not authorized to use the requested service. Ensure that you have subscribed to the service you are trying to use. If you are new to AWS, your account might take some time to be activated while your credit card details are being verified."},
|
||||
ErrorOutstandingVpcPeeringConnectionLimitExceeded: {HTTPCode: 400, Message: "You've reached the limit on the number of VPC peering connection requests that you can create for the specified VPC."},
|
||||
|
||||
@@ -47,6 +47,7 @@ import (
|
||||
handlers_ec2_tags "github.com/mulgadc/spinifex/spinifex/handlers/ec2/tags"
|
||||
handlers_ec2_volume "github.com/mulgadc/spinifex/spinifex/handlers/ec2/volume"
|
||||
handlers_ec2_vpc "github.com/mulgadc/spinifex/spinifex/handlers/ec2/vpc"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
|
||||
"github.com/mulgadc/spinifex/spinifex/instancetypes"
|
||||
"github.com/mulgadc/spinifex/spinifex/network/external/dhcp"
|
||||
@@ -130,6 +131,7 @@ type Daemon struct {
|
||||
vpcService *handlers_ec2_vpc.VPCServiceImpl
|
||||
eipService *handlers_ec2_eip.EIPServiceImpl
|
||||
elbv2Service *handlers_elbv2.ELBv2ServiceImpl
|
||||
eksService *handlers_eks.EKSServiceImpl
|
||||
routeTableService *handlers_ec2_routetable.RouteTableServiceImpl
|
||||
natGatewayService *handlers_ec2_natgw.NatGatewayServiceImpl
|
||||
externalIPAM *handlers_ec2_vpc.ExternalIPAM
|
||||
@@ -841,6 +843,48 @@ func (d *Daemon) subscribeAll() error {
|
||||
)
|
||||
}
|
||||
|
||||
// EKS gateway → daemon subscriptions. Every handler currently returns
|
||||
// NotImplemented; topics are subscribed up-front so the wiring layer is
|
||||
// stable while real bodies land.
|
||||
if d.eksService != nil {
|
||||
subs = append(subs,
|
||||
natsSub{"eks.CreateCluster", d.handleEKSCreateCluster, "spinifex-workers"},
|
||||
natsSub{"eks.DescribeCluster", d.handleEKSDescribeCluster, "spinifex-workers"},
|
||||
natsSub{"eks.ListClusters", d.handleEKSListClusters, "spinifex-workers"},
|
||||
natsSub{"eks.UpdateClusterConfig", d.handleEKSUpdateClusterConfig, "spinifex-workers"},
|
||||
natsSub{"eks.UpdateClusterVersion", d.handleEKSUpdateClusterVersion, "spinifex-workers"},
|
||||
natsSub{"eks.DeleteCluster", d.handleEKSDeleteCluster, "spinifex-workers"},
|
||||
natsSub{"eks.CreateNodegroup", d.handleEKSCreateNodegroup, "spinifex-workers"},
|
||||
natsSub{"eks.DescribeNodegroup", d.handleEKSDescribeNodegroup, "spinifex-workers"},
|
||||
natsSub{"eks.ListNodegroups", d.handleEKSListNodegroups, "spinifex-workers"},
|
||||
natsSub{"eks.UpdateNodegroupConfig", d.handleEKSUpdateNodegroupConfig, "spinifex-workers"},
|
||||
natsSub{"eks.UpdateNodegroupVersion", d.handleEKSUpdateNodegroupVersion, "spinifex-workers"},
|
||||
natsSub{"eks.DeleteNodegroup", d.handleEKSDeleteNodegroup, "spinifex-workers"},
|
||||
natsSub{"eks.CreateAccessEntry", d.handleEKSCreateAccessEntry, "spinifex-workers"},
|
||||
natsSub{"eks.DescribeAccessEntry", d.handleEKSDescribeAccessEntry, "spinifex-workers"},
|
||||
natsSub{"eks.ListAccessEntries", d.handleEKSListAccessEntries, "spinifex-workers"},
|
||||
natsSub{"eks.UpdateAccessEntry", d.handleEKSUpdateAccessEntry, "spinifex-workers"},
|
||||
natsSub{"eks.DeleteAccessEntry", d.handleEKSDeleteAccessEntry, "spinifex-workers"},
|
||||
natsSub{"eks.AssociateAccessPolicy", d.handleEKSAssociateAccessPolicy, "spinifex-workers"},
|
||||
natsSub{"eks.DisassociateAccessPolicy", d.handleEKSDisassociateAccessPolicy, "spinifex-workers"},
|
||||
natsSub{"eks.ListAssociatedAccessPolicies", d.handleEKSListAssociatedAccessPolicies, "spinifex-workers"},
|
||||
natsSub{"eks.ListAccessPolicies", d.handleEKSListAccessPolicies, "spinifex-workers"},
|
||||
natsSub{"eks.ListAddons", d.handleEKSListAddons, "spinifex-workers"},
|
||||
natsSub{"eks.DescribeAddonVersions", d.handleEKSDescribeAddonVersions, "spinifex-workers"},
|
||||
natsSub{"eks.CreateAddon", d.handleEKSCreateAddon, "spinifex-workers"},
|
||||
natsSub{"eks.DeleteAddon", d.handleEKSDeleteAddon, "spinifex-workers"},
|
||||
natsSub{"eks.DescribeAddon", d.handleEKSDescribeAddon, "spinifex-workers"},
|
||||
natsSub{"eks.UpdateAddon", d.handleEKSUpdateAddon, "spinifex-workers"},
|
||||
natsSub{"eks.AssociateIdentityProviderConfig", d.handleEKSAssociateIdentityProviderConfig, "spinifex-workers"},
|
||||
natsSub{"eks.DescribeIdentityProviderConfig", d.handleEKSDescribeIdentityProviderConfig, "spinifex-workers"},
|
||||
natsSub{"eks.ListIdentityProviderConfigs", d.handleEKSListIdentityProviderConfigs, "spinifex-workers"},
|
||||
natsSub{"eks.DisassociateIdentityProviderConfig", d.handleEKSDisassociateIdentityProviderConfig, "spinifex-workers"},
|
||||
natsSub{"eks.TagResource", d.handleEKSTagResource, "spinifex-workers"},
|
||||
natsSub{"eks.UntagResource", d.handleEKSUntagResource, "spinifex-workers"},
|
||||
natsSub{"eks.ListTagsForResource", d.handleEKSListTagsForResource, "spinifex-workers"},
|
||||
)
|
||||
}
|
||||
|
||||
// EIP operations require external IPAM (pool mode). Only subscribe when available;
|
||||
// without a subscriber the gateway returns a NATS timeout → clean error to the client.
|
||||
if d.eipService != nil {
|
||||
@@ -1021,6 +1065,8 @@ func (d *Daemon) assertNoClusterServicesInitialised() error {
|
||||
return errors.New("d.accountService must be nil before startCluster")
|
||||
case d.elbv2Service != nil:
|
||||
return errors.New("d.elbv2Service must be nil before startCluster")
|
||||
case d.eksService != nil:
|
||||
return errors.New("d.eksService must be nil before startCluster")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1275,6 +1321,13 @@ func (d *Daemon) startCluster() error {
|
||||
"err", err)
|
||||
}
|
||||
|
||||
d.eksService, err = initServiceWithRetry("EKS service", func() (*handlers_eks.EKSServiceImpl, error) {
|
||||
return handlers_eks.NewEKSServiceImplWithNATS(d.config, d.natsConn)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize EKS service: %w", err)
|
||||
}
|
||||
|
||||
// Ensure default VPC exists for system and admin accounts
|
||||
// (matches AWS: every account has a default VPC with IGW + default SG)
|
||||
if d.vpcService != nil {
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package daemon
|
||||
|
||||
import "github.com/nats-io/nats.go"
|
||||
|
||||
// --- Cluster ---
|
||||
|
||||
func (d *Daemon) handleEKSCreateCluster(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.CreateCluster)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSDescribeCluster(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.DescribeCluster)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSListClusters(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.ListClusters)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSUpdateClusterConfig(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.UpdateClusterConfig)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSUpdateClusterVersion(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.UpdateClusterVersion)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSDeleteCluster(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.DeleteCluster)
|
||||
}
|
||||
|
||||
// --- Nodegroup ---
|
||||
|
||||
func (d *Daemon) handleEKSCreateNodegroup(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.CreateNodegroup)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSDescribeNodegroup(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.DescribeNodegroup)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSListNodegroups(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.ListNodegroups)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSUpdateNodegroupConfig(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.UpdateNodegroupConfig)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSUpdateNodegroupVersion(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.UpdateNodegroupVersion)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSDeleteNodegroup(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.DeleteNodegroup)
|
||||
}
|
||||
|
||||
// --- AccessEntry + AccessPolicy ---
|
||||
|
||||
func (d *Daemon) handleEKSCreateAccessEntry(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.CreateAccessEntry)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSDescribeAccessEntry(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.DescribeAccessEntry)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSListAccessEntries(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.ListAccessEntries)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSUpdateAccessEntry(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.UpdateAccessEntry)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSDeleteAccessEntry(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.DeleteAccessEntry)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSAssociateAccessPolicy(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.AssociateAccessPolicy)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSDisassociateAccessPolicy(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.DisassociateAccessPolicy)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSListAssociatedAccessPolicies(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.ListAssociatedAccessPolicies)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSListAccessPolicies(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.ListAccessPolicies)
|
||||
}
|
||||
|
||||
// --- Addons ---
|
||||
|
||||
func (d *Daemon) handleEKSListAddons(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.ListAddons)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSDescribeAddonVersions(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.DescribeAddonVersions)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSCreateAddon(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.CreateAddon)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSDeleteAddon(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.DeleteAddon)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSDescribeAddon(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.DescribeAddon)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSUpdateAddon(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.UpdateAddon)
|
||||
}
|
||||
|
||||
// --- OIDC identity-provider configs ---
|
||||
|
||||
func (d *Daemon) handleEKSAssociateIdentityProviderConfig(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.AssociateIdentityProviderConfig)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSDescribeIdentityProviderConfig(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.DescribeIdentityProviderConfig)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSListIdentityProviderConfigs(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.ListIdentityProviderConfigs)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSDisassociateIdentityProviderConfig(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.DisassociateIdentityProviderConfig)
|
||||
}
|
||||
|
||||
// --- Tags ---
|
||||
|
||||
func (d *Daemon) handleEKSTagResource(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.TagResource)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSUntagResource(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.UntagResource)
|
||||
}
|
||||
|
||||
func (d *Daemon) handleEKSListTagsForResource(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.eksService.ListTagsForResource)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
"github.com/mulgadc/spinifex/spinifex/testutil"
|
||||
"github.com/mulgadc/spinifex/spinifex/utils"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// setupEKSDaemon returns a Daemon with only its eksService field populated —
|
||||
// every EKS handler dispatches through that field via handleNATSRequest, so
|
||||
// the rest of the Daemon graph can stay nil.
|
||||
func setupEKSDaemon(t *testing.T) (*Daemon, *nats.Conn) {
|
||||
t.Helper()
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
svc, err := handlers_eks.NewEKSServiceImplWithNATS(nil, nc)
|
||||
require.NoError(t, err)
|
||||
return &Daemon{eksService: svc}, nc
|
||||
}
|
||||
|
||||
// requestEKS publishes a request on the given subject and returns the response
|
||||
// payload. Every EKS handler in this branch returns NotImplemented, so the
|
||||
// payload always decodes to that error code.
|
||||
func requestEKS(t *testing.T, nc *nats.Conn, subject string) []byte {
|
||||
t.Helper()
|
||||
msg := nats.NewMsg(subject)
|
||||
msg.Data = []byte(`{}`)
|
||||
msg.Header.Set(utils.AccountIDHeader, "111122223333")
|
||||
resp, err := nc.RequestMsg(msg, 2*time.Second)
|
||||
require.NoError(t, err, "no responder for %s", subject)
|
||||
return resp.Data
|
||||
}
|
||||
|
||||
func assertNotImpl(t *testing.T, payload []byte) {
|
||||
t.Helper()
|
||||
var env struct {
|
||||
Code *string `json:"Code"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(payload, &env), "decode payload %q", payload)
|
||||
require.NotNil(t, env.Code, "payload %q has no Code field", payload)
|
||||
assert.Equal(t, awserrors.ErrorNotImplemented, *env.Code)
|
||||
}
|
||||
|
||||
func TestDaemonHandleEKS_AllHandlersDispatchToService(t *testing.T) {
|
||||
d, nc := setupEKSDaemon(t)
|
||||
|
||||
cases := []struct {
|
||||
subject string
|
||||
handler nats.MsgHandler
|
||||
}{
|
||||
{"eks.CreateCluster", d.handleEKSCreateCluster},
|
||||
{"eks.DescribeCluster", d.handleEKSDescribeCluster},
|
||||
{"eks.ListClusters", d.handleEKSListClusters},
|
||||
{"eks.UpdateClusterConfig", d.handleEKSUpdateClusterConfig},
|
||||
{"eks.UpdateClusterVersion", d.handleEKSUpdateClusterVersion},
|
||||
{"eks.DeleteCluster", d.handleEKSDeleteCluster},
|
||||
{"eks.CreateNodegroup", d.handleEKSCreateNodegroup},
|
||||
{"eks.DescribeNodegroup", d.handleEKSDescribeNodegroup},
|
||||
{"eks.ListNodegroups", d.handleEKSListNodegroups},
|
||||
{"eks.UpdateNodegroupConfig", d.handleEKSUpdateNodegroupConfig},
|
||||
{"eks.UpdateNodegroupVersion", d.handleEKSUpdateNodegroupVersion},
|
||||
{"eks.DeleteNodegroup", d.handleEKSDeleteNodegroup},
|
||||
{"eks.CreateAccessEntry", d.handleEKSCreateAccessEntry},
|
||||
{"eks.DescribeAccessEntry", d.handleEKSDescribeAccessEntry},
|
||||
{"eks.ListAccessEntries", d.handleEKSListAccessEntries},
|
||||
{"eks.UpdateAccessEntry", d.handleEKSUpdateAccessEntry},
|
||||
{"eks.DeleteAccessEntry", d.handleEKSDeleteAccessEntry},
|
||||
{"eks.AssociateAccessPolicy", d.handleEKSAssociateAccessPolicy},
|
||||
{"eks.DisassociateAccessPolicy", d.handleEKSDisassociateAccessPolicy},
|
||||
{"eks.ListAssociatedAccessPolicies", d.handleEKSListAssociatedAccessPolicies},
|
||||
{"eks.ListAccessPolicies", d.handleEKSListAccessPolicies},
|
||||
{"eks.ListAddons", d.handleEKSListAddons},
|
||||
{"eks.DescribeAddonVersions", d.handleEKSDescribeAddonVersions},
|
||||
{"eks.CreateAddon", d.handleEKSCreateAddon},
|
||||
{"eks.DeleteAddon", d.handleEKSDeleteAddon},
|
||||
{"eks.DescribeAddon", d.handleEKSDescribeAddon},
|
||||
{"eks.UpdateAddon", d.handleEKSUpdateAddon},
|
||||
{"eks.AssociateIdentityProviderConfig", d.handleEKSAssociateIdentityProviderConfig},
|
||||
{"eks.DescribeIdentityProviderConfig", d.handleEKSDescribeIdentityProviderConfig},
|
||||
{"eks.ListIdentityProviderConfigs", d.handleEKSListIdentityProviderConfigs},
|
||||
{"eks.DisassociateIdentityProviderConfig", d.handleEKSDisassociateIdentityProviderConfig},
|
||||
{"eks.TagResource", d.handleEKSTagResource},
|
||||
{"eks.UntagResource", d.handleEKSUntagResource},
|
||||
{"eks.ListTagsForResource", d.handleEKSListTagsForResource},
|
||||
}
|
||||
require.Equal(t, 34, len(cases), "expected exactly one handler per AWS EKS action")
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.subject, func(t *testing.T) {
|
||||
sub, err := nc.Subscribe(c.subject, c.handler)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sub.Unsubscribe() })
|
||||
|
||||
payload := requestEKS(t, nc, c.subject)
|
||||
assertNotImpl(t, payload)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
gateway_eks "github.com/mulgadc/spinifex/spinifex/gateway/eks"
|
||||
)
|
||||
|
||||
// GenerateEKSErrorResponse exposes the EKS REST-JSON error envelope to the
|
||||
// shared gateway plumbing (writeClusterUnavailable, writeThrottleError,
|
||||
// ErrorHandler). The body is JSON: {"__type":"<code>Exception","message":"<msg>"}.
|
||||
func GenerateEKSErrorResponse(code, message, _ string) []byte {
|
||||
return gateway_eks.GenerateEKSErrorResponse(code, message)
|
||||
}
|
||||
|
||||
// eksRoute encodes one HTTP method + path-regex → AWS action dispatch.
|
||||
// Path params are captured by the regex and passed to the per-route handler
|
||||
// via the params slice (named-capture index order, not by name).
|
||||
type eksRoute struct {
|
||||
method string
|
||||
pattern *regexp.Regexp
|
||||
action string
|
||||
handler eksRouteHandler
|
||||
}
|
||||
|
||||
// eksRouteHandler invokes the per-action gateway function. It receives the
|
||||
// path-capture values in declaration order, the request body, the parent
|
||||
// gateway config (NATS conn + ServiceURL), and the caller's accountID. It
|
||||
// returns the response payload object (already a typed *eks.<X>Output) or an
|
||||
// error whose Error() is an awserrors code.
|
||||
type eksRouteHandler func(gw *GatewayConfig, accountID string, params []string, body []byte) (any, error)
|
||||
|
||||
// eksRoutes is the dispatch table. Order matters: more-specific paths must
|
||||
// come before less-specific ones with the same prefix (e.g. the
|
||||
// /access-policies leaf under /clusters/{name}/access-entries/{arn}/...
|
||||
// before the bare /clusters/{name}/access-entries/{arn} entry).
|
||||
var eksRoutes = []eksRoute{
|
||||
// Cluster
|
||||
{"POST", regexp.MustCompile(`^/clusters$`), "CreateCluster",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.CreateCluster(gw.NATSConn, acct, b)
|
||||
}},
|
||||
{"GET", regexp.MustCompile(`^/clusters$`), "ListClusters",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListClusters(gw.NATSConn, acct)
|
||||
}},
|
||||
{"POST", regexp.MustCompile(`^/clusters/([^/]+)/update-config$`), "UpdateClusterConfig",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.UpdateClusterConfig(gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"POST", regexp.MustCompile(`^/clusters/([^/]+)/update-version$`), "UpdateClusterVersion",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.UpdateClusterVersion(gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
|
||||
// Nodegroup
|
||||
{"POST", regexp.MustCompile(`^/clusters/([^/]+)/node-groups$`), "CreateNodegroup",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.CreateNodegroup(gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"GET", regexp.MustCompile(`^/clusters/([^/]+)/node-groups$`), "ListNodegroups",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListNodegroups(gw.NATSConn, acct, p[0])
|
||||
}},
|
||||
{"POST", regexp.MustCompile(`^/clusters/([^/]+)/node-groups/([^/]+)/update-config$`), "UpdateNodegroupConfig",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.UpdateNodegroupConfig(gw.NATSConn, acct, p[0], p[1], b)
|
||||
}},
|
||||
{"POST", regexp.MustCompile(`^/clusters/([^/]+)/node-groups/([^/]+)/update-version$`), "UpdateNodegroupVersion",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.UpdateNodegroupVersion(gw.NATSConn, acct, p[0], p[1], b)
|
||||
}},
|
||||
{"GET", regexp.MustCompile(`^/clusters/([^/]+)/node-groups/([^/]+)$`), "DescribeNodegroup",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DescribeNodegroup(gw.NATSConn, acct, p[0], p[1])
|
||||
}},
|
||||
{"DELETE", regexp.MustCompile(`^/clusters/([^/]+)/node-groups/([^/]+)$`), "DeleteNodegroup",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DeleteNodegroup(gw.NATSConn, acct, p[0], p[1])
|
||||
}},
|
||||
|
||||
// AccessEntry / AccessPolicy
|
||||
{"POST", regexp.MustCompile(`^/clusters/([^/]+)/access-entries$`), "CreateAccessEntry",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.CreateAccessEntry(gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"GET", regexp.MustCompile(`^/clusters/([^/]+)/access-entries$`), "ListAccessEntries",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListAccessEntries(gw.NATSConn, acct, p[0])
|
||||
}},
|
||||
{"POST", regexp.MustCompile(`^/clusters/([^/]+)/access-entries/([^/]+)/access-policies$`), "AssociateAccessPolicy",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.AssociateAccessPolicy(gw.NATSConn, acct, p[0], p[1], b)
|
||||
}},
|
||||
{"DELETE", regexp.MustCompile(`^/clusters/([^/]+)/access-entries/([^/]+)/access-policies$`), "DisassociateAccessPolicy",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DisassociateAccessPolicy(gw.NATSConn, acct, p[0], p[1], b)
|
||||
}},
|
||||
{"GET", regexp.MustCompile(`^/clusters/([^/]+)/access-entries/([^/]+)/access-policies$`), "ListAssociatedAccessPolicies",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListAssociatedAccessPolicies(gw.NATSConn, acct, p[0], p[1])
|
||||
}},
|
||||
{"GET", regexp.MustCompile(`^/clusters/([^/]+)/access-entries/([^/]+)$`), "DescribeAccessEntry",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DescribeAccessEntry(gw.NATSConn, acct, p[0], p[1])
|
||||
}},
|
||||
{"POST", regexp.MustCompile(`^/clusters/([^/]+)/access-entries/([^/]+)$`), "UpdateAccessEntry",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.UpdateAccessEntry(gw.NATSConn, acct, p[0], p[1], b)
|
||||
}},
|
||||
{"DELETE", regexp.MustCompile(`^/clusters/([^/]+)/access-entries/([^/]+)$`), "DeleteAccessEntry",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DeleteAccessEntry(gw.NATSConn, acct, p[0], p[1])
|
||||
}},
|
||||
{"GET", regexp.MustCompile(`^/access-policies$`), "ListAccessPolicies",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListAccessPolicies(gw.NATSConn, acct)
|
||||
}},
|
||||
|
||||
// Addons
|
||||
{"GET", regexp.MustCompile(`^/addons/supported-versions$`), "DescribeAddonVersions",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DescribeAddonVersions(gw.NATSConn, acct)
|
||||
}},
|
||||
{"GET", regexp.MustCompile(`^/addons$`), "ListAddons",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListAddons(gw.NATSConn, acct)
|
||||
}},
|
||||
{"POST", regexp.MustCompile(`^/clusters/([^/]+)/addons$`), "CreateAddon",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.CreateAddon(gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"POST", regexp.MustCompile(`^/clusters/([^/]+)/addons/([^/]+)/update$`), "UpdateAddon",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.UpdateAddon(gw.NATSConn, acct, p[0], p[1], b)
|
||||
}},
|
||||
{"GET", regexp.MustCompile(`^/clusters/([^/]+)/addons/([^/]+)$`), "DescribeAddon",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DescribeAddon(gw.NATSConn, acct, p[0], p[1])
|
||||
}},
|
||||
{"DELETE", regexp.MustCompile(`^/clusters/([^/]+)/addons/([^/]+)$`), "DeleteAddon",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DeleteAddon(gw.NATSConn, acct, p[0], p[1])
|
||||
}},
|
||||
|
||||
// OIDC identity-provider configs
|
||||
{"POST", regexp.MustCompile(`^/clusters/([^/]+)/identity-provider-configs/associate$`), "AssociateIdentityProviderConfig",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.AssociateIdentityProviderConfig(gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"POST", regexp.MustCompile(`^/clusters/([^/]+)/identity-provider-configs/describe$`), "DescribeIdentityProviderConfig",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DescribeIdentityProviderConfig(gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"POST", regexp.MustCompile(`^/clusters/([^/]+)/identity-provider-configs/disassociate$`), "DisassociateIdentityProviderConfig",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DisassociateIdentityProviderConfig(gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"GET", regexp.MustCompile(`^/clusters/([^/]+)/identity-provider-configs$`), "ListIdentityProviderConfigs",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListIdentityProviderConfigs(gw.NATSConn, acct, p[0])
|
||||
}},
|
||||
|
||||
// Cluster CRUD on a specific name — listed after the more-specific
|
||||
// /clusters/{name}/... routes so the regexp matcher prefers the deeper
|
||||
// matches first when iterating in declaration order.
|
||||
{"GET", regexp.MustCompile(`^/clusters/([^/]+)$`), "DescribeCluster",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DescribeCluster(gw.NATSConn, acct, p[0])
|
||||
}},
|
||||
{"DELETE", regexp.MustCompile(`^/clusters/([^/]+)$`), "DeleteCluster",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DeleteCluster(gw.NATSConn, acct, p[0])
|
||||
}},
|
||||
|
||||
// Tags
|
||||
{"POST", regexp.MustCompile(`^/tags/(.+)$`), "TagResource",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.TagResource(gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"DELETE", regexp.MustCompile(`^/tags/(.+)$`), "UntagResource",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.UntagResource(gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"GET", regexp.MustCompile(`^/tags/(.+)$`), "ListTagsForResource",
|
||||
func(gw *GatewayConfig, acct string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListTagsForResource(gw.NATSConn, acct, p[0])
|
||||
}},
|
||||
}
|
||||
|
||||
// lookupEKSAction walks eksRoutes in declaration order, returning the matched
|
||||
// route's action name + path params, or ("", nil, false) when nothing matches.
|
||||
// Used by EKS_Request and unit tests to verify routing.
|
||||
func lookupEKSAction(method, path string) (string, []string, eksRouteHandler, bool) {
|
||||
for _, route := range eksRoutes {
|
||||
if route.method != method {
|
||||
continue
|
||||
}
|
||||
m := route.pattern.FindStringSubmatch(path)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
var params []string
|
||||
if len(m) > 1 {
|
||||
params = m[1:]
|
||||
}
|
||||
return route.action, params, route.handler, true
|
||||
}
|
||||
return "", nil, nil, false
|
||||
}
|
||||
|
||||
// EKS_Request is the chi catch-all dispatcher for EKS REST-JSON requests.
|
||||
// It parses method+path, resolves to an AWS action, reads the body, invokes
|
||||
// the per-action handler, and serialises the typed output as JSON. Errors
|
||||
// are returned as plain awserrors codes; the caller (gateway.Request) routes
|
||||
// them through the shared ErrorHandler, which now emits EKS REST-JSON.
|
||||
func (gw *GatewayConfig) EKS_Request(w http.ResponseWriter, r *http.Request) error {
|
||||
action, params, handler, ok := lookupEKSAction(r.Method, r.URL.Path)
|
||||
if !ok {
|
||||
slog.Debug("EKS: no route for request", "method", r.Method, "path", r.URL.Path)
|
||||
return errors.New(awserrors.ErrorInvalidAction)
|
||||
}
|
||||
|
||||
if err := gw.checkPolicy(r, "eks", action); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gw.NATSConn == nil {
|
||||
return errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
accountID, _ := r.Context().Value(ctxAccountID).(string)
|
||||
if accountID == "" {
|
||||
slog.Error("EKS_Request: no account ID in auth context")
|
||||
return errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
slog.Error("EKS_Request: failed to read body", "err", err)
|
||||
return errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
output, err := handler(gw, accountID, params, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gateway_eks.WriteJSONResponse(w, output)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package gateway_eks
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// CreateAccessEntry — POST /clusters/{name}/access-entries
|
||||
func CreateAccessEntry(natsConn *nats.Conn, accountID, cluster string, body []byte) (*eks.CreateAccessEntryOutput, error) {
|
||||
input := new(eks.CreateAccessEntryInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).CreateAccessEntry(input, accountID)
|
||||
}
|
||||
|
||||
// DescribeAccessEntry — GET /clusters/{name}/access-entries/{arn}
|
||||
func DescribeAccessEntry(natsConn *nats.Conn, accountID, cluster, principalARN string) (*eks.DescribeAccessEntryOutput, error) {
|
||||
input := &eks.DescribeAccessEntryInput{
|
||||
ClusterName: aws.String(cluster),
|
||||
PrincipalArn: aws.String(principalARN),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DescribeAccessEntry(input, accountID)
|
||||
}
|
||||
|
||||
// ListAccessEntries — GET /clusters/{name}/access-entries
|
||||
func ListAccessEntries(natsConn *nats.Conn, accountID, cluster string) (*eks.ListAccessEntriesOutput, error) {
|
||||
input := &eks.ListAccessEntriesInput{ClusterName: aws.String(cluster)}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListAccessEntries(input, accountID)
|
||||
}
|
||||
|
||||
// UpdateAccessEntry — POST /clusters/{name}/access-entries/{arn}
|
||||
func UpdateAccessEntry(natsConn *nats.Conn, accountID, cluster, principalARN string, body []byte) (*eks.UpdateAccessEntryOutput, error) {
|
||||
input := new(eks.UpdateAccessEntryInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
input.PrincipalArn = aws.String(principalARN)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).UpdateAccessEntry(input, accountID)
|
||||
}
|
||||
|
||||
// DeleteAccessEntry — DELETE /clusters/{name}/access-entries/{arn}
|
||||
func DeleteAccessEntry(natsConn *nats.Conn, accountID, cluster, principalARN string) (*eks.DeleteAccessEntryOutput, error) {
|
||||
input := &eks.DeleteAccessEntryInput{
|
||||
ClusterName: aws.String(cluster),
|
||||
PrincipalArn: aws.String(principalARN),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DeleteAccessEntry(input, accountID)
|
||||
}
|
||||
|
||||
// AssociateAccessPolicy — POST /clusters/{name}/access-entries/{arn}/access-policies
|
||||
func AssociateAccessPolicy(natsConn *nats.Conn, accountID, cluster, principalARN string, body []byte) (*eks.AssociateAccessPolicyOutput, error) {
|
||||
input := new(eks.AssociateAccessPolicyInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
input.PrincipalArn = aws.String(principalARN)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).AssociateAccessPolicy(input, accountID)
|
||||
}
|
||||
|
||||
// DisassociateAccessPolicy — DELETE /clusters/{name}/access-entries/{arn}/access-policies
|
||||
func DisassociateAccessPolicy(natsConn *nats.Conn, accountID, cluster, principalARN string, body []byte) (*eks.DisassociateAccessPolicyOutput, error) {
|
||||
input := new(eks.DisassociateAccessPolicyInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
input.PrincipalArn = aws.String(principalARN)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DisassociateAccessPolicy(input, accountID)
|
||||
}
|
||||
|
||||
// ListAssociatedAccessPolicies — GET /clusters/{name}/access-entries/{arn}/access-policies
|
||||
func ListAssociatedAccessPolicies(natsConn *nats.Conn, accountID, cluster, principalARN string) (*eks.ListAssociatedAccessPoliciesOutput, error) {
|
||||
input := &eks.ListAssociatedAccessPoliciesInput{
|
||||
ClusterName: aws.String(cluster),
|
||||
PrincipalArn: aws.String(principalARN),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListAssociatedAccessPolicies(input, accountID)
|
||||
}
|
||||
|
||||
// ListAccessPolicies — GET /access-policies
|
||||
func ListAccessPolicies(natsConn *nats.Conn, accountID string) (*eks.ListAccessPoliciesOutput, error) {
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListAccessPolicies(&eks.ListAccessPoliciesInput{}, accountID)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package gateway_eks
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// ListAddons — GET /addons
|
||||
func ListAddons(natsConn *nats.Conn, accountID string) (*eks.ListAddonsOutput, error) {
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListAddons(&eks.ListAddonsInput{}, accountID)
|
||||
}
|
||||
|
||||
// DescribeAddonVersions — GET /addons/supported-versions
|
||||
func DescribeAddonVersions(natsConn *nats.Conn, accountID string) (*eks.DescribeAddonVersionsOutput, error) {
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DescribeAddonVersions(&eks.DescribeAddonVersionsInput{}, accountID)
|
||||
}
|
||||
|
||||
// CreateAddon — POST /clusters/{name}/addons
|
||||
func CreateAddon(natsConn *nats.Conn, accountID, cluster string, body []byte) (*eks.CreateAddonOutput, error) {
|
||||
input := new(eks.CreateAddonInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).CreateAddon(input, accountID)
|
||||
}
|
||||
|
||||
// DeleteAddon — DELETE /clusters/{name}/addons/{addon}
|
||||
func DeleteAddon(natsConn *nats.Conn, accountID, cluster, addon string) (*eks.DeleteAddonOutput, error) {
|
||||
input := &eks.DeleteAddonInput{
|
||||
ClusterName: aws.String(cluster),
|
||||
AddonName: aws.String(addon),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DeleteAddon(input, accountID)
|
||||
}
|
||||
|
||||
// DescribeAddon — GET /clusters/{name}/addons/{addon}
|
||||
func DescribeAddon(natsConn *nats.Conn, accountID, cluster, addon string) (*eks.DescribeAddonOutput, error) {
|
||||
input := &eks.DescribeAddonInput{
|
||||
ClusterName: aws.String(cluster),
|
||||
AddonName: aws.String(addon),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DescribeAddon(input, accountID)
|
||||
}
|
||||
|
||||
// UpdateAddon — POST /clusters/{name}/addons/{addon}/update
|
||||
func UpdateAddon(natsConn *nats.Conn, accountID, cluster, addon string, body []byte) (*eks.UpdateAddonOutput, error) {
|
||||
input := new(eks.UpdateAddonInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
input.AddonName = aws.String(addon)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).UpdateAddon(input, accountID)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package gateway_eks
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// CreateCluster — POST /clusters
|
||||
func CreateCluster(natsConn *nats.Conn, accountID string, body []byte) (*eks.CreateClusterOutput, error) {
|
||||
input := new(eks.CreateClusterInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).CreateCluster(input, accountID)
|
||||
}
|
||||
|
||||
// DescribeCluster — GET /clusters/{name}
|
||||
func DescribeCluster(natsConn *nats.Conn, accountID, name string) (*eks.DescribeClusterOutput, error) {
|
||||
input := &eks.DescribeClusterInput{Name: aws.String(name)}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DescribeCluster(input, accountID)
|
||||
}
|
||||
|
||||
// ListClusters — GET /clusters
|
||||
func ListClusters(natsConn *nats.Conn, accountID string) (*eks.ListClustersOutput, error) {
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListClusters(&eks.ListClustersInput{}, accountID)
|
||||
}
|
||||
|
||||
// UpdateClusterConfig — POST /clusters/{name}/update-config
|
||||
func UpdateClusterConfig(natsConn *nats.Conn, accountID, name string, body []byte) (*eks.UpdateClusterConfigOutput, error) {
|
||||
input := new(eks.UpdateClusterConfigInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.Name = aws.String(name)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).UpdateClusterConfig(input, accountID)
|
||||
}
|
||||
|
||||
// UpdateClusterVersion — POST /clusters/{name}/update-version
|
||||
func UpdateClusterVersion(natsConn *nats.Conn, accountID, name string, body []byte) (*eks.UpdateClusterVersionOutput, error) {
|
||||
input := new(eks.UpdateClusterVersionInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.Name = aws.String(name)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).UpdateClusterVersion(input, accountID)
|
||||
}
|
||||
|
||||
// DeleteCluster — DELETE /clusters/{name}
|
||||
func DeleteCluster(natsConn *nats.Conn, accountID, name string) (*eks.DeleteClusterOutput, error) {
|
||||
input := &eks.DeleteClusterInput{Name: aws.String(name)}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DeleteCluster(input, accountID)
|
||||
}
|
||||
|
||||
// unmarshalIfBody decodes body into out only when body is non-empty.
|
||||
// Returning a typed empty input on empty body lets us share a single helper
|
||||
// across actions that may or may not carry a body.
|
||||
func unmarshalIfBody(body []byte, out any) error {
|
||||
if len(body) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(body, out)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Package gateway_eks holds the HTTP-side glue between awsgw and the EKS
|
||||
// handlers package. EKS speaks AWS REST-JSON 1.1 (not the query+XML protocol
|
||||
// that EC2 / ELBv2 use), so error envelopes here are JSON and the
|
||||
// Content-Type is application/x-amz-json-1.1 throughout.
|
||||
package gateway_eks
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
)
|
||||
|
||||
// JSONContentType is the AWS REST-JSON 1.1 content type EKS clients expect on
|
||||
// every request and response.
|
||||
const JSONContentType = "application/x-amz-json-1.1"
|
||||
|
||||
// EKSJSONError is the AWS REST-JSON error envelope. AWS SDKs key off
|
||||
// __type to populate awserr.Code() and message to populate awserr.Message().
|
||||
type EKSJSONError struct {
|
||||
Type string `json:"__type"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// GenerateEKSErrorResponse marshals the AWS REST-JSON error envelope.
|
||||
// The trailing "Exception" suffix matches the convention AWS uses
|
||||
// (e.g. ResourceNotFoundException, InvalidParameterException).
|
||||
func GenerateEKSErrorResponse(code, message string) []byte {
|
||||
body, err := json.Marshal(EKSJSONError{
|
||||
Type: code + "Exception",
|
||||
Message: message,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("Failed to marshal EKS error JSON", "code", code, "err", err)
|
||||
return fmt.Appendf(nil, `{"__type":"InternalErrorException","message":%q}`, message)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
// WriteJSONResponse serializes obj as JSON and writes it as a 200 response.
|
||||
func WriteJSONResponse(w http.ResponseWriter, obj any) {
|
||||
body, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
slog.Error("Failed to marshal EKS response JSON", "err", err)
|
||||
WriteJSONError(w, awserrors.ErrorInternalError, "failed to marshal response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", JSONContentType)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := w.Write(body); err != nil {
|
||||
slog.Error("Failed to write EKS response", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteJSONError emits the AWS REST-JSON error envelope with the given AWS
|
||||
// error code, message, and HTTP status. The Content-Type matches what real
|
||||
// EKS returns.
|
||||
func WriteJSONError(w http.ResponseWriter, code, message string, httpStatus int) {
|
||||
body := GenerateEKSErrorResponse(code, message)
|
||||
w.Header().Set("Content-Type", JSONContentType)
|
||||
if httpStatus == 0 {
|
||||
httpStatus = http.StatusInternalServerError
|
||||
}
|
||||
w.WriteHeader(httpStatus)
|
||||
if _, err := w.Write(body); err != nil {
|
||||
slog.Error("Failed to write EKS error response", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteErrorFromCode looks up an awserrors code, maps it to its HTTP status
|
||||
// and message, and writes the JSON envelope. Falls back to a 500 InternalError
|
||||
// when the code isn't registered.
|
||||
func WriteErrorFromCode(w http.ResponseWriter, code string) {
|
||||
msg, ok := awserrors.ErrorLookup[code]
|
||||
if !ok {
|
||||
slog.Warn("Unknown EKS error code", "code", code)
|
||||
WriteJSONError(w, awserrors.ErrorInternalError, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
httpStatus := msg.HTTPCode
|
||||
if httpStatus == 0 {
|
||||
httpStatus = http.StatusInternalServerError
|
||||
}
|
||||
WriteJSONError(w, code, msg.Message, httpStatus)
|
||||
}
|
||||
|
||||
// ParseJSONBody is the generic helper every per-action handler uses to decode
|
||||
// the request body into an aws-sdk-go input struct. Empty bodies are valid
|
||||
// for GET / DELETE actions; the returned struct will simply have all
|
||||
// zero-valued fields.
|
||||
func ParseJSONBody[T any](r *http.Request) (*T, error) {
|
||||
out := new(T)
|
||||
if r.Body == nil {
|
||||
return out, nil
|
||||
}
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read body: %w", err)
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
if err := json.Unmarshal(body, out); err != nil {
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package gateway_eks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateEKSErrorResponse_ShapesExceptionSuffix(t *testing.T) {
|
||||
body := GenerateEKSErrorResponse("ResourceNotFound", "Cluster does not exist")
|
||||
var env EKSJSONError
|
||||
require.NoError(t, json.Unmarshal(body, &env))
|
||||
assert.Equal(t, "ResourceNotFoundException", env.Type)
|
||||
assert.Equal(t, "Cluster does not exist", env.Message)
|
||||
}
|
||||
|
||||
func TestWriteJSONError_SetsContentTypeAndStatus(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
WriteJSONError(w, "NotImplemented", "Operation not implemented", http.StatusNotImplemented)
|
||||
assert.Equal(t, http.StatusNotImplemented, w.Code)
|
||||
assert.Equal(t, JSONContentType, w.Header().Get("Content-Type"))
|
||||
|
||||
var env EKSJSONError
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &env))
|
||||
assert.Equal(t, "NotImplementedException", env.Type)
|
||||
assert.Equal(t, "Operation not implemented", env.Message)
|
||||
}
|
||||
|
||||
func TestWriteJSONResponse_SerializesObject(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
WriteJSONResponse(w, map[string]string{"foo": "bar"})
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, JSONContentType, w.Header().Get("Content-Type"))
|
||||
|
||||
var got map[string]string
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got))
|
||||
assert.Equal(t, "bar", got["foo"])
|
||||
}
|
||||
|
||||
type sampleInput struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func TestParseJSONBody_EmptyBodyOK(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/clusters", nil)
|
||||
got, err := ParseJSONBody[sampleInput](req)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, got.Name)
|
||||
}
|
||||
|
||||
func TestParseJSONBody_DecodesBody(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/clusters", bytes.NewReader([]byte(`{"name":"alpha"}`)))
|
||||
got, err := ParseJSONBody[sampleInput](req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "alpha", got.Name)
|
||||
}
|
||||
|
||||
func TestParseJSONBody_InvalidJSONErrors(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/clusters", bytes.NewReader([]byte(`{not-json}`)))
|
||||
_, err := ParseJSONBody[sampleInput](req)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package gateway_eks
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// CreateNodegroup — POST /clusters/{name}/node-groups
|
||||
func CreateNodegroup(natsConn *nats.Conn, accountID, cluster string, body []byte) (*eks.CreateNodegroupOutput, error) {
|
||||
input := new(eks.CreateNodegroupInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).CreateNodegroup(input, accountID)
|
||||
}
|
||||
|
||||
// DescribeNodegroup — GET /clusters/{name}/node-groups/{ng}
|
||||
func DescribeNodegroup(natsConn *nats.Conn, accountID, cluster, ng string) (*eks.DescribeNodegroupOutput, error) {
|
||||
input := &eks.DescribeNodegroupInput{
|
||||
ClusterName: aws.String(cluster),
|
||||
NodegroupName: aws.String(ng),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DescribeNodegroup(input, accountID)
|
||||
}
|
||||
|
||||
// ListNodegroups — GET /clusters/{name}/node-groups
|
||||
func ListNodegroups(natsConn *nats.Conn, accountID, cluster string) (*eks.ListNodegroupsOutput, error) {
|
||||
input := &eks.ListNodegroupsInput{ClusterName: aws.String(cluster)}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListNodegroups(input, accountID)
|
||||
}
|
||||
|
||||
// UpdateNodegroupConfig — POST /clusters/{name}/node-groups/{ng}/update-config
|
||||
func UpdateNodegroupConfig(natsConn *nats.Conn, accountID, cluster, ng string, body []byte) (*eks.UpdateNodegroupConfigOutput, error) {
|
||||
input := new(eks.UpdateNodegroupConfigInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
input.NodegroupName = aws.String(ng)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).UpdateNodegroupConfig(input, accountID)
|
||||
}
|
||||
|
||||
// UpdateNodegroupVersion — POST /clusters/{name}/node-groups/{ng}/update-version
|
||||
func UpdateNodegroupVersion(natsConn *nats.Conn, accountID, cluster, ng string, body []byte) (*eks.UpdateNodegroupVersionOutput, error) {
|
||||
input := new(eks.UpdateNodegroupVersionInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
input.NodegroupName = aws.String(ng)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).UpdateNodegroupVersion(input, accountID)
|
||||
}
|
||||
|
||||
// DeleteNodegroup — DELETE /clusters/{name}/node-groups/{ng}
|
||||
func DeleteNodegroup(natsConn *nats.Conn, accountID, cluster, ng string) (*eks.DeleteNodegroupOutput, error) {
|
||||
input := &eks.DeleteNodegroupInput{
|
||||
ClusterName: aws.String(cluster),
|
||||
NodegroupName: aws.String(ng),
|
||||
}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DeleteNodegroup(input, accountID)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package gateway_eks
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// AssociateIdentityProviderConfig — POST /clusters/{name}/identity-provider-configs/associate
|
||||
func AssociateIdentityProviderConfig(natsConn *nats.Conn, accountID, cluster string, body []byte) (*eks.AssociateIdentityProviderConfigOutput, error) {
|
||||
input := new(eks.AssociateIdentityProviderConfigInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).AssociateIdentityProviderConfig(input, accountID)
|
||||
}
|
||||
|
||||
// DescribeIdentityProviderConfig — POST /clusters/{name}/identity-provider-configs/describe
|
||||
func DescribeIdentityProviderConfig(natsConn *nats.Conn, accountID, cluster string, body []byte) (*eks.DescribeIdentityProviderConfigOutput, error) {
|
||||
input := new(eks.DescribeIdentityProviderConfigInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DescribeIdentityProviderConfig(input, accountID)
|
||||
}
|
||||
|
||||
// ListIdentityProviderConfigs — GET /clusters/{name}/identity-provider-configs
|
||||
func ListIdentityProviderConfigs(natsConn *nats.Conn, accountID, cluster string) (*eks.ListIdentityProviderConfigsOutput, error) {
|
||||
input := &eks.ListIdentityProviderConfigsInput{ClusterName: aws.String(cluster)}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListIdentityProviderConfigs(input, accountID)
|
||||
}
|
||||
|
||||
// DisassociateIdentityProviderConfig — POST /clusters/{name}/identity-provider-configs/disassociate
|
||||
func DisassociateIdentityProviderConfig(natsConn *nats.Conn, accountID, cluster string, body []byte) (*eks.DisassociateIdentityProviderConfigOutput, error) {
|
||||
input := new(eks.DisassociateIdentityProviderConfigInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.ClusterName = aws.String(cluster)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).DisassociateIdentityProviderConfig(input, accountID)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package gateway_eks
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// TagResource — POST /tags/{arn}
|
||||
func TagResource(natsConn *nats.Conn, accountID, resourceARN string, body []byte) (*eks.TagResourceOutput, error) {
|
||||
input := new(eks.TagResourceInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.ResourceArn = aws.String(resourceARN)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).TagResource(input, accountID)
|
||||
}
|
||||
|
||||
// UntagResource — DELETE /tags/{arn}
|
||||
func UntagResource(natsConn *nats.Conn, accountID, resourceARN string, body []byte) (*eks.UntagResourceOutput, error) {
|
||||
input := new(eks.UntagResourceInput)
|
||||
if err := unmarshalIfBody(body, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.ResourceArn = aws.String(resourceARN)
|
||||
return handlers_eks.NewNATSEKSService(natsConn).UntagResource(input, accountID)
|
||||
}
|
||||
|
||||
// ListTagsForResource — GET /tags/{arn}
|
||||
func ListTagsForResource(natsConn *nats.Conn, accountID, resourceARN string) (*eks.ListTagsForResourceOutput, error) {
|
||||
input := &eks.ListTagsForResourceInput{ResourceArn: aws.String(resourceARN)}
|
||||
return handlers_eks.NewNATSEKSService(natsConn).ListTagsForResource(input, accountID)
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
package gateway_eks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/testutil"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// stubEKSResponder subscribes a catch-all on `eks.>` that replies with an
|
||||
// empty JSON object for every request. Lets the gateway wrappers run their
|
||||
// full NATSRequest round-trip without standing up a real EKSServiceImpl.
|
||||
func stubEKSResponder(t *testing.T, nc *nats.Conn) {
|
||||
t.Helper()
|
||||
sub, err := nc.Subscribe("eks.>", func(m *nats.Msg) {
|
||||
if m.Reply == "" {
|
||||
return
|
||||
}
|
||||
_ = m.Respond([]byte(`{}`))
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sub.Unsubscribe() })
|
||||
}
|
||||
|
||||
const acct = "111122223333"
|
||||
|
||||
func TestGatewayWrappers_Cluster(t *testing.T) {
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
stubEKSResponder(t, nc)
|
||||
|
||||
out1, err := CreateCluster(nc, acct, []byte(`{"name":"alpha"}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out1)
|
||||
|
||||
out2, err := CreateCluster(nc, acct, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out2)
|
||||
|
||||
out3, err := DescribeCluster(nc, acct, "alpha")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out3)
|
||||
|
||||
out4, err := ListClusters(nc, acct)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out4)
|
||||
|
||||
out5, err := UpdateClusterConfig(nc, acct, "alpha", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out5)
|
||||
|
||||
out6, err := UpdateClusterConfig(nc, acct, "alpha", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out6)
|
||||
|
||||
out7, err := UpdateClusterVersion(nc, acct, "alpha", []byte(`{"version":"1.30"}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out7)
|
||||
|
||||
out8, err := UpdateClusterVersion(nc, acct, "alpha", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out8)
|
||||
|
||||
out9, err := DeleteCluster(nc, acct, "alpha")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out9)
|
||||
}
|
||||
|
||||
func TestGatewayWrappers_Cluster_BadJSON(t *testing.T) {
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
stubEKSResponder(t, nc)
|
||||
|
||||
_, err := CreateCluster(nc, acct, []byte(`{not-json`))
|
||||
require.Error(t, err)
|
||||
_, err = UpdateClusterConfig(nc, acct, "alpha", []byte(`{not-json`))
|
||||
require.Error(t, err)
|
||||
_, err = UpdateClusterVersion(nc, acct, "alpha", []byte(`{not-json`))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGatewayWrappers_Nodegroup(t *testing.T) {
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
stubEKSResponder(t, nc)
|
||||
|
||||
out1, err := CreateNodegroup(nc, acct, "alpha", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out1)
|
||||
|
||||
out2, err := CreateNodegroup(nc, acct, "alpha", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out2)
|
||||
|
||||
out3, err := DescribeNodegroup(nc, acct, "alpha", "ng1")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out3)
|
||||
|
||||
out4, err := ListNodegroups(nc, acct, "alpha")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out4)
|
||||
|
||||
out5, err := UpdateNodegroupConfig(nc, acct, "alpha", "ng1", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out5)
|
||||
|
||||
out6, err := UpdateNodegroupConfig(nc, acct, "alpha", "ng1", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out6)
|
||||
|
||||
out7, err := UpdateNodegroupVersion(nc, acct, "alpha", "ng1", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out7)
|
||||
|
||||
out8, err := UpdateNodegroupVersion(nc, acct, "alpha", "ng1", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out8)
|
||||
|
||||
out9, err := DeleteNodegroup(nc, acct, "alpha", "ng1")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out9)
|
||||
}
|
||||
|
||||
func TestGatewayWrappers_Nodegroup_BadJSON(t *testing.T) {
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
stubEKSResponder(t, nc)
|
||||
|
||||
_, err := CreateNodegroup(nc, acct, "alpha", []byte(`{nope`))
|
||||
require.Error(t, err)
|
||||
_, err = UpdateNodegroupConfig(nc, acct, "alpha", "ng1", []byte(`{nope`))
|
||||
require.Error(t, err)
|
||||
_, err = UpdateNodegroupVersion(nc, acct, "alpha", "ng1", []byte(`{nope`))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGatewayWrappers_Access(t *testing.T) {
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
stubEKSResponder(t, nc)
|
||||
|
||||
out1, err := CreateAccessEntry(nc, acct, "alpha", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out1)
|
||||
|
||||
out2, err := CreateAccessEntry(nc, acct, "alpha", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out2)
|
||||
|
||||
out3, err := DescribeAccessEntry(nc, acct, "alpha", "arn-user")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out3)
|
||||
|
||||
out4, err := ListAccessEntries(nc, acct, "alpha")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out4)
|
||||
|
||||
out5, err := UpdateAccessEntry(nc, acct, "alpha", "arn-user", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out5)
|
||||
|
||||
out6, err := UpdateAccessEntry(nc, acct, "alpha", "arn-user", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out6)
|
||||
|
||||
out7, err := DeleteAccessEntry(nc, acct, "alpha", "arn-user")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out7)
|
||||
|
||||
out8, err := AssociateAccessPolicy(nc, acct, "alpha", "arn-user", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out8)
|
||||
|
||||
out9, err := AssociateAccessPolicy(nc, acct, "alpha", "arn-user", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out9)
|
||||
|
||||
out10, err := DisassociateAccessPolicy(nc, acct, "alpha", "arn-user", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out10)
|
||||
|
||||
out11, err := DisassociateAccessPolicy(nc, acct, "alpha", "arn-user", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out11)
|
||||
|
||||
out12, err := ListAssociatedAccessPolicies(nc, acct, "alpha", "arn-user")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out12)
|
||||
|
||||
out13, err := ListAccessPolicies(nc, acct)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out13)
|
||||
}
|
||||
|
||||
func TestGatewayWrappers_Access_BadJSON(t *testing.T) {
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
stubEKSResponder(t, nc)
|
||||
|
||||
_, err := CreateAccessEntry(nc, acct, "alpha", []byte(`{x`))
|
||||
require.Error(t, err)
|
||||
_, err = UpdateAccessEntry(nc, acct, "alpha", "arn-user", []byte(`{x`))
|
||||
require.Error(t, err)
|
||||
_, err = AssociateAccessPolicy(nc, acct, "alpha", "arn-user", []byte(`{x`))
|
||||
require.Error(t, err)
|
||||
_, err = DisassociateAccessPolicy(nc, acct, "alpha", "arn-user", []byte(`{x`))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGatewayWrappers_Addons(t *testing.T) {
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
stubEKSResponder(t, nc)
|
||||
|
||||
out1, err := ListAddons(nc, acct)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out1)
|
||||
|
||||
out2, err := DescribeAddonVersions(nc, acct)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out2)
|
||||
|
||||
out3, err := CreateAddon(nc, acct, "alpha", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out3)
|
||||
|
||||
out4, err := CreateAddon(nc, acct, "alpha", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out4)
|
||||
|
||||
out5, err := DescribeAddon(nc, acct, "alpha", "vpc-cni")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out5)
|
||||
|
||||
out6, err := DeleteAddon(nc, acct, "alpha", "vpc-cni")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out6)
|
||||
|
||||
out7, err := UpdateAddon(nc, acct, "alpha", "vpc-cni", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out7)
|
||||
|
||||
out8, err := UpdateAddon(nc, acct, "alpha", "vpc-cni", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out8)
|
||||
}
|
||||
|
||||
func TestGatewayWrappers_Addons_BadJSON(t *testing.T) {
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
stubEKSResponder(t, nc)
|
||||
|
||||
_, err := CreateAddon(nc, acct, "alpha", []byte(`{x`))
|
||||
require.Error(t, err)
|
||||
_, err = UpdateAddon(nc, acct, "alpha", "vpc-cni", []byte(`{x`))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGatewayWrappers_OIDC(t *testing.T) {
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
stubEKSResponder(t, nc)
|
||||
|
||||
out1, err := AssociateIdentityProviderConfig(nc, acct, "alpha", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out1)
|
||||
|
||||
out2, err := AssociateIdentityProviderConfig(nc, acct, "alpha", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out2)
|
||||
|
||||
out3, err := DescribeIdentityProviderConfig(nc, acct, "alpha", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out3)
|
||||
|
||||
out4, err := DescribeIdentityProviderConfig(nc, acct, "alpha", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out4)
|
||||
|
||||
out5, err := ListIdentityProviderConfigs(nc, acct, "alpha")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out5)
|
||||
|
||||
out6, err := DisassociateIdentityProviderConfig(nc, acct, "alpha", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out6)
|
||||
|
||||
out7, err := DisassociateIdentityProviderConfig(nc, acct, "alpha", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out7)
|
||||
}
|
||||
|
||||
func TestGatewayWrappers_OIDC_BadJSON(t *testing.T) {
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
stubEKSResponder(t, nc)
|
||||
|
||||
_, err := AssociateIdentityProviderConfig(nc, acct, "alpha", []byte(`{x`))
|
||||
require.Error(t, err)
|
||||
_, err = DescribeIdentityProviderConfig(nc, acct, "alpha", []byte(`{x`))
|
||||
require.Error(t, err)
|
||||
_, err = DisassociateIdentityProviderConfig(nc, acct, "alpha", []byte(`{x`))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGatewayWrappers_Tags(t *testing.T) {
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
stubEKSResponder(t, nc)
|
||||
|
||||
out1, err := TagResource(nc, acct, "arn-cluster", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out1)
|
||||
|
||||
out2, err := TagResource(nc, acct, "arn-cluster", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out2)
|
||||
|
||||
out3, err := UntagResource(nc, acct, "arn-cluster", []byte(`{}`))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out3)
|
||||
|
||||
out4, err := UntagResource(nc, acct, "arn-cluster", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out4)
|
||||
|
||||
out5, err := ListTagsForResource(nc, acct, "arn-cluster")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out5)
|
||||
}
|
||||
|
||||
func TestGatewayWrappers_Tags_BadJSON(t *testing.T) {
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
stubEKSResponder(t, nc)
|
||||
|
||||
_, err := TagResource(nc, acct, "arn-cluster", []byte(`{x`))
|
||||
require.Error(t, err)
|
||||
_, err = UntagResource(nc, acct, "arn-cluster", []byte(`{x`))
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
gateway_eks "github.com/mulgadc/spinifex/spinifex/gateway/eks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLookupEKSAction_ResolvesKnownRoutes(t *testing.T) {
|
||||
cases := []struct {
|
||||
method, path string
|
||||
wantAction string
|
||||
wantParams []string
|
||||
}{
|
||||
{"POST", "/clusters", "CreateCluster", nil},
|
||||
{"GET", "/clusters", "ListClusters", nil},
|
||||
{"GET", "/clusters/alpha", "DescribeCluster", []string{"alpha"}},
|
||||
{"DELETE", "/clusters/alpha", "DeleteCluster", []string{"alpha"}},
|
||||
{"POST", "/clusters/alpha/update-config", "UpdateClusterConfig", []string{"alpha"}},
|
||||
{"POST", "/clusters/alpha/update-version", "UpdateClusterVersion", []string{"alpha"}},
|
||||
{"POST", "/clusters/alpha/node-groups", "CreateNodegroup", []string{"alpha"}},
|
||||
{"GET", "/clusters/alpha/node-groups", "ListNodegroups", []string{"alpha"}},
|
||||
{"GET", "/clusters/alpha/node-groups/ng1", "DescribeNodegroup", []string{"alpha", "ng1"}},
|
||||
{"DELETE", "/clusters/alpha/node-groups/ng1", "DeleteNodegroup", []string{"alpha", "ng1"}},
|
||||
{"POST", "/clusters/alpha/node-groups/ng1/update-config", "UpdateNodegroupConfig", []string{"alpha", "ng1"}},
|
||||
{"POST", "/clusters/alpha/node-groups/ng1/update-version", "UpdateNodegroupVersion", []string{"alpha", "ng1"}},
|
||||
{"POST", "/clusters/alpha/access-entries", "CreateAccessEntry", []string{"alpha"}},
|
||||
{"GET", "/clusters/alpha/access-entries", "ListAccessEntries", []string{"alpha"}},
|
||||
{"GET", "/clusters/alpha/access-entries/arn-user", "DescribeAccessEntry", []string{"alpha", "arn-user"}},
|
||||
{"POST", "/clusters/alpha/access-entries/arn-user", "UpdateAccessEntry", []string{"alpha", "arn-user"}},
|
||||
{"DELETE", "/clusters/alpha/access-entries/arn-user", "DeleteAccessEntry", []string{"alpha", "arn-user"}},
|
||||
{"POST", "/clusters/alpha/access-entries/arn-user/access-policies", "AssociateAccessPolicy", []string{"alpha", "arn-user"}},
|
||||
{"DELETE", "/clusters/alpha/access-entries/arn-user/access-policies", "DisassociateAccessPolicy", []string{"alpha", "arn-user"}},
|
||||
{"GET", "/clusters/alpha/access-entries/arn-user/access-policies", "ListAssociatedAccessPolicies", []string{"alpha", "arn-user"}},
|
||||
{"GET", "/access-policies", "ListAccessPolicies", nil},
|
||||
{"GET", "/addons", "ListAddons", nil},
|
||||
{"GET", "/addons/supported-versions", "DescribeAddonVersions", nil},
|
||||
{"POST", "/clusters/alpha/addons", "CreateAddon", []string{"alpha"}},
|
||||
{"GET", "/clusters/alpha/addons/vpc-cni", "DescribeAddon", []string{"alpha", "vpc-cni"}},
|
||||
{"DELETE", "/clusters/alpha/addons/vpc-cni", "DeleteAddon", []string{"alpha", "vpc-cni"}},
|
||||
{"POST", "/clusters/alpha/addons/vpc-cni/update", "UpdateAddon", []string{"alpha", "vpc-cni"}},
|
||||
{"POST", "/clusters/alpha/identity-provider-configs/associate", "AssociateIdentityProviderConfig", []string{"alpha"}},
|
||||
{"POST", "/clusters/alpha/identity-provider-configs/describe", "DescribeIdentityProviderConfig", []string{"alpha"}},
|
||||
{"POST", "/clusters/alpha/identity-provider-configs/disassociate", "DisassociateIdentityProviderConfig", []string{"alpha"}},
|
||||
{"GET", "/clusters/alpha/identity-provider-configs", "ListIdentityProviderConfigs", []string{"alpha"}},
|
||||
{"POST", "/tags/arn-cluster", "TagResource", []string{"arn-cluster"}},
|
||||
{"DELETE", "/tags/arn-cluster", "UntagResource", []string{"arn-cluster"}},
|
||||
{"GET", "/tags/arn-cluster", "ListTagsForResource", []string{"arn-cluster"}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+"_"+tc.path, func(t *testing.T) {
|
||||
action, params, handler, ok := lookupEKSAction(tc.method, tc.path)
|
||||
require.True(t, ok, "expected route to match for %s %s", tc.method, tc.path)
|
||||
require.NotNil(t, handler)
|
||||
assert.Equal(t, tc.wantAction, action)
|
||||
assert.Equal(t, tc.wantParams, params)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupEKSAction_CoversAll34Actions(t *testing.T) {
|
||||
expected := map[string]bool{
|
||||
"CreateCluster": false,
|
||||
"DescribeCluster": false,
|
||||
"ListClusters": false,
|
||||
"UpdateClusterConfig": false,
|
||||
"UpdateClusterVersion": false,
|
||||
"DeleteCluster": false,
|
||||
"CreateNodegroup": false,
|
||||
"DescribeNodegroup": false,
|
||||
"ListNodegroups": false,
|
||||
"UpdateNodegroupConfig": false,
|
||||
"UpdateNodegroupVersion": false,
|
||||
"DeleteNodegroup": false,
|
||||
"CreateAccessEntry": false,
|
||||
"DescribeAccessEntry": false,
|
||||
"ListAccessEntries": false,
|
||||
"UpdateAccessEntry": false,
|
||||
"DeleteAccessEntry": false,
|
||||
"AssociateAccessPolicy": false,
|
||||
"DisassociateAccessPolicy": false,
|
||||
"ListAssociatedAccessPolicies": false,
|
||||
"ListAccessPolicies": false,
|
||||
"ListAddons": false,
|
||||
"DescribeAddonVersions": false,
|
||||
"CreateAddon": false,
|
||||
"DeleteAddon": false,
|
||||
"DescribeAddon": false,
|
||||
"UpdateAddon": false,
|
||||
"AssociateIdentityProviderConfig": false,
|
||||
"DescribeIdentityProviderConfig": false,
|
||||
"ListIdentityProviderConfigs": false,
|
||||
"DisassociateIdentityProviderConfig": false,
|
||||
"TagResource": false,
|
||||
"UntagResource": false,
|
||||
"ListTagsForResource": false,
|
||||
}
|
||||
for _, route := range eksRoutes {
|
||||
if _, ok := expected[route.action]; ok {
|
||||
expected[route.action] = true
|
||||
}
|
||||
}
|
||||
for action, seen := range expected {
|
||||
assert.True(t, seen, "action %q is not registered in eksRoutes", action)
|
||||
}
|
||||
assert.Equal(t, len(expected), len(eksRoutes), "eksRoutes should have exactly one entry per AWS action")
|
||||
}
|
||||
|
||||
func TestLookupEKSAction_UnknownReturnsFalse(t *testing.T) {
|
||||
_, _, _, ok := lookupEKSAction("PATCH", "/clusters/alpha")
|
||||
assert.False(t, ok)
|
||||
|
||||
_, _, _, ok = lookupEKSAction("GET", "/clusters/alpha/wat")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestEKSRequest_UnknownActionReturnsInvalidAction(t *testing.T) {
|
||||
gw := &GatewayConfig{DisableLogging: true}
|
||||
req := httptest.NewRequest(http.MethodGet, "/totally-unknown", nil)
|
||||
ctx := context.WithValue(req.Context(), ctxService, "eks")
|
||||
ctx = context.WithValue(ctx, ctxAccountID, "111122223333")
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
err := gw.EKS_Request(w, req)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidAction, err.Error())
|
||||
}
|
||||
|
||||
func TestEKSRequest_MissingNATSReturnsServerInternal(t *testing.T) {
|
||||
gw := &GatewayConfig{DisableLogging: true}
|
||||
req := httptest.NewRequest(http.MethodGet, "/clusters", nil)
|
||||
ctx := context.WithValue(req.Context(), ctxService, "eks")
|
||||
ctx = context.WithValue(ctx, ctxAccountID, "111122223333")
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
err := gw.EKS_Request(w, req)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
|
||||
}
|
||||
|
||||
func TestErrorHandler_EKSEmitsJSONNotXML(t *testing.T) {
|
||||
gw := &GatewayConfig{DisableLogging: true}
|
||||
req := httptest.NewRequest(http.MethodGet, "/clusters", nil)
|
||||
ctx := context.WithValue(req.Context(), ctxService, "eks")
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
gw.ErrorHandler(w, req, errEKSNotImpl())
|
||||
assert.Equal(t, http.StatusNotImplemented, w.Code)
|
||||
assert.Equal(t, gateway_eks.JSONContentType, w.Header().Get("Content-Type"))
|
||||
assert.True(t, strings.HasPrefix(w.Body.String(), "{"), "expected JSON body, got %q", w.Body.String())
|
||||
|
||||
var env gateway_eks.EKSJSONError
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &env))
|
||||
assert.Equal(t, "NotImplementedException", env.Type)
|
||||
}
|
||||
|
||||
// errEKSNotImpl returns the same error a stub EKS handler would surface so the
|
||||
// ErrorHandler test mirrors the production code path.
|
||||
func errEKSNotImpl() error { return errAWS(awserrors.ErrorNotImplemented) }
|
||||
|
||||
// awsCodeError is a tiny shim so test files can return an awserrors-code-based
|
||||
// error without depending on the impl package.
|
||||
type awsCodeError struct{ code string }
|
||||
|
||||
func (e *awsCodeError) Error() string { return e.code }
|
||||
func errAWS(code string) error { return &awsCodeError{code: code} }
|
||||
@@ -74,6 +74,7 @@ var supportedServices = map[string]bool{
|
||||
"sts": true,
|
||||
"account": true,
|
||||
"elasticloadbalancing": true,
|
||||
"eks": true,
|
||||
"spinifex": true,
|
||||
}
|
||||
|
||||
@@ -172,6 +173,9 @@ func (gw *GatewayConfig) throttleKeyFuncs() []ratelimit.KeyFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// eksJSONContentType is the AWS REST-JSON 1.1 content type EKS clients expect.
|
||||
const eksJSONContentType = "application/x-amz-json-1.1"
|
||||
|
||||
// clusterUnavailableMsg is the body returned when the gateway short-circuits
|
||||
// because the daemon's NATS connection is down. Points operators at the
|
||||
// daemon's /local/status (1b) for triage instead of letting them watch the
|
||||
@@ -185,6 +189,18 @@ const clusterUnavailableMsg = "cluster unavailable: NATS disconnected — check
|
||||
// here to make sure the /local/status hint actually reaches the operator.
|
||||
func (gw *GatewayConfig) writeClusterUnavailable(w http.ResponseWriter, _ *http.Request, svc string) {
|
||||
requestID := uuid.NewString()
|
||||
|
||||
// EKS uses AWS REST-JSON 1.1 — different content type, different envelope.
|
||||
if svc == "eks" {
|
||||
body := GenerateEKSErrorResponse(awserrors.ErrorServiceUnavailable, clusterUnavailableMsg, requestID)
|
||||
w.Header().Set("Content-Type", eksJSONContentType)
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
if _, err := w.Write(body); err != nil {
|
||||
slog.Error("Failed to write EKS cluster-unavailable response", "err", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var xmlBody string
|
||||
if svc == "iam" || svc == "sts" {
|
||||
iam := IAMErrorResponse{
|
||||
@@ -221,11 +237,22 @@ func (gw *GatewayConfig) writeThrottleError(w http.ResponseWriter, r *http.Reque
|
||||
svc, _ := r.Context().Value(ctxService).(string)
|
||||
|
||||
errorCode := awserrors.ErrorRequestLimitExceeded
|
||||
if svc == "iam" || svc == "sts" {
|
||||
if svc == "iam" || svc == "sts" || svc == "eks" {
|
||||
errorCode = awserrors.ErrorThrottling
|
||||
}
|
||||
errorMsg := awserrors.ErrorLookup[errorCode]
|
||||
|
||||
// EKS uses AWS REST-JSON 1.1 — emit JSON envelope instead of XML.
|
||||
if svc == "eks" {
|
||||
body := GenerateEKSErrorResponse(errorCode, errorMsg.Message, requestID)
|
||||
w.Header().Set("Content-Type", eksJSONContentType)
|
||||
w.WriteHeader(errorMsg.HTTPCode)
|
||||
if _, err := w.Write(body); err != nil {
|
||||
slog.Error("Failed to write EKS throttle error response", "err", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var xmlErr []byte
|
||||
if svc == "iam" || svc == "sts" {
|
||||
xmlErr = GenerateIAMErrorResponse(errorCode, errorMsg.Message, requestID)
|
||||
@@ -276,6 +303,8 @@ func (gw *GatewayConfig) Request(w http.ResponseWriter, r *http.Request) {
|
||||
err = gw.STS_Request(w, r)
|
||||
case "elasticloadbalancing":
|
||||
err = gw.ELBv2_Request(w, r)
|
||||
case "eks":
|
||||
err = gw.EKS_Request(w, r)
|
||||
case "spinifex":
|
||||
err = gw.Spinifex_Request(w, r)
|
||||
default:
|
||||
@@ -418,10 +447,27 @@ func (gw *GatewayConfig) ErrorHandler(w http.ResponseWriter, r *http.Request, er
|
||||
|
||||
errorMsg = awserrors.ErrorLookup[err.Error()]
|
||||
|
||||
// IAM and STS use the ErrorResponse XML envelope; EC2-style services use
|
||||
// <Response><Errors>...</Errors></Response>. AWS SDK v1 strictly parses
|
||||
// the per-service shape and would reject a mismatch with
|
||||
// SerializationError, dropping the awserr.Code() for callers.
|
||||
// EKS uses AWS REST-JSON 1.1: {"__type":"<Code>Exception","message":"..."}
|
||||
// with Content-Type application/x-amz-json-1.1. EC2-style services use
|
||||
// <Response><Errors>...</Errors></Response>; IAM and STS use the
|
||||
// <ErrorResponse> envelope. AWS SDK v1 strictly parses the per-service
|
||||
// shape and would reject a mismatch with SerializationError, dropping
|
||||
// the awserr.Code() for callers.
|
||||
if errorMsg.HTTPCode == 0 {
|
||||
errorMsg.HTTPCode = 500
|
||||
}
|
||||
|
||||
if svc == "eks" {
|
||||
body := GenerateEKSErrorResponse(err.Error(), errorMsg.Message, requestId)
|
||||
slog.Debug("Generated EKS error response", "error", err.Error(), "json", string(body), "requestId", requestId)
|
||||
w.Header().Set("Content-Type", eksJSONContentType)
|
||||
w.WriteHeader(errorMsg.HTTPCode)
|
||||
if _, err := w.Write(body); err != nil {
|
||||
slog.Error("Failed to write EKS error response", "err", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var xmlError []byte
|
||||
if svc == "iam" || svc == "sts" {
|
||||
xmlError = GenerateIAMErrorResponse(err.Error(), errorMsg.Message, requestId)
|
||||
@@ -431,10 +477,6 @@ func (gw *GatewayConfig) ErrorHandler(w http.ResponseWriter, r *http.Request, er
|
||||
|
||||
slog.Debug("Generated error response", "error", err.Error(), "xml", string(xmlError), "requestId", requestId)
|
||||
|
||||
if errorMsg.HTTPCode == 0 {
|
||||
errorMsg.HTTPCode = 500
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.WriteHeader(errorMsg.HTTPCode)
|
||||
if _, err := w.Write(xmlError); err != nil {
|
||||
|
||||
@@ -158,3 +158,43 @@ handleIAMCreateRole → iam.CreateRole
|
||||
```
|
||||
|
||||
This pattern scales consistently across all AWS services.
|
||||
|
||||
## EKS (Elastic Kubernetes Service)
|
||||
|
||||
EKS uses two layers of NATS subjects.
|
||||
|
||||
### Layer 1 — AWS API surface
|
||||
|
||||
The gateway translates EKS REST-JSON requests into `eks.<AWSAction>` NATS
|
||||
requests, using the existing `spinifex-workers` queue group. The handler
|
||||
name follows the same `handleEKS<AWSAction>` convention.
|
||||
|
||||
```
|
||||
eks.CreateCluster, eks.DescribeCluster, eks.ListClusters,
|
||||
eks.UpdateClusterConfig, eks.UpdateClusterVersion, eks.DeleteCluster
|
||||
eks.CreateNodegroup, eks.DescribeNodegroup, eks.ListNodegroups,
|
||||
eks.UpdateNodegroupConfig, eks.UpdateNodegroupVersion, eks.DeleteNodegroup
|
||||
eks.CreateAccessEntry, eks.DescribeAccessEntry, eks.ListAccessEntries,
|
||||
eks.UpdateAccessEntry, eks.DeleteAccessEntry,
|
||||
eks.AssociateAccessPolicy, eks.DisassociateAccessPolicy,
|
||||
eks.ListAssociatedAccessPolicies, eks.ListAccessPolicies
|
||||
eks.ListAddons, eks.DescribeAddonVersions,
|
||||
eks.CreateAddon, eks.DeleteAddon, eks.DescribeAddon, eks.UpdateAddon
|
||||
eks.AssociateIdentityProviderConfig, eks.DescribeIdentityProviderConfig,
|
||||
eks.ListIdentityProviderConfigs, eks.DisassociateIdentityProviderConfig
|
||||
eks.TagResource, eks.UntagResource, eks.ListTagsForResource
|
||||
```
|
||||
|
||||
### Layer 2 — internal reconciler bus
|
||||
|
||||
The cluster + nodegroup reconcilers communicate with K3s VMs and each
|
||||
other on `eks.bus.<accountID>.<clusterName>.*`. This layer lands with the
|
||||
reconciler bodies; only Layer 1 is registered today. See
|
||||
`docs/development/feature/eks-v1.md` Q11 for the full subject list.
|
||||
|
||||
### Cross-service calls
|
||||
|
||||
EKS reconcilers also publish on existing namespaces when interacting with
|
||||
other services (no `eks.` prefix): `ec2.RunInstances`,
|
||||
`ec2.CreateNetworkInterface`, `elbv2.CreateLoadBalancer`,
|
||||
`elbv2.RegisterTargets`, `route53.ChangeResourceRecordSets`, etc.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// Package handlers_eks provides the EKS (Amazon Elastic Kubernetes Service)
|
||||
// control-plane API surface for Spinifex. The interface declared here is the
|
||||
// stable contract every gateway and daemon caller binds to; the NATS wrapper
|
||||
// and concrete service implementation both satisfy it.
|
||||
package handlers_eks
|
||||
|
||||
import "github.com/aws/aws-sdk-go/service/eks"
|
||||
|
||||
// EKSService is the AWS EKS control-plane contract exposed to spinifex
|
||||
// gateway and daemon callers. Every method maps 1:1 to an AWS EKS API action
|
||||
// (PascalCase). Signatures use the aws-sdk-go input/output types verbatim so
|
||||
// downstream callers can pass straight through from generated SDK code.
|
||||
type EKSService interface {
|
||||
// Cluster
|
||||
CreateCluster(input *eks.CreateClusterInput, accountID string) (*eks.CreateClusterOutput, error)
|
||||
DescribeCluster(input *eks.DescribeClusterInput, accountID string) (*eks.DescribeClusterOutput, error)
|
||||
ListClusters(input *eks.ListClustersInput, accountID string) (*eks.ListClustersOutput, error)
|
||||
UpdateClusterConfig(input *eks.UpdateClusterConfigInput, accountID string) (*eks.UpdateClusterConfigOutput, error)
|
||||
UpdateClusterVersion(input *eks.UpdateClusterVersionInput, accountID string) (*eks.UpdateClusterVersionOutput, error)
|
||||
DeleteCluster(input *eks.DeleteClusterInput, accountID string) (*eks.DeleteClusterOutput, error)
|
||||
|
||||
// Nodegroup
|
||||
CreateNodegroup(input *eks.CreateNodegroupInput, accountID string) (*eks.CreateNodegroupOutput, error)
|
||||
DescribeNodegroup(input *eks.DescribeNodegroupInput, accountID string) (*eks.DescribeNodegroupOutput, error)
|
||||
ListNodegroups(input *eks.ListNodegroupsInput, accountID string) (*eks.ListNodegroupsOutput, error)
|
||||
UpdateNodegroupConfig(input *eks.UpdateNodegroupConfigInput, accountID string) (*eks.UpdateNodegroupConfigOutput, error)
|
||||
UpdateNodegroupVersion(input *eks.UpdateNodegroupVersionInput, accountID string) (*eks.UpdateNodegroupVersionOutput, error)
|
||||
DeleteNodegroup(input *eks.DeleteNodegroupInput, accountID string) (*eks.DeleteNodegroupOutput, error)
|
||||
|
||||
// AccessEntry + AccessPolicy
|
||||
CreateAccessEntry(input *eks.CreateAccessEntryInput, accountID string) (*eks.CreateAccessEntryOutput, error)
|
||||
DescribeAccessEntry(input *eks.DescribeAccessEntryInput, accountID string) (*eks.DescribeAccessEntryOutput, error)
|
||||
ListAccessEntries(input *eks.ListAccessEntriesInput, accountID string) (*eks.ListAccessEntriesOutput, error)
|
||||
UpdateAccessEntry(input *eks.UpdateAccessEntryInput, accountID string) (*eks.UpdateAccessEntryOutput, error)
|
||||
DeleteAccessEntry(input *eks.DeleteAccessEntryInput, accountID string) (*eks.DeleteAccessEntryOutput, error)
|
||||
AssociateAccessPolicy(input *eks.AssociateAccessPolicyInput, accountID string) (*eks.AssociateAccessPolicyOutput, error)
|
||||
DisassociateAccessPolicy(input *eks.DisassociateAccessPolicyInput, accountID string) (*eks.DisassociateAccessPolicyOutput, error)
|
||||
ListAssociatedAccessPolicies(input *eks.ListAssociatedAccessPoliciesInput, accountID string) (*eks.ListAssociatedAccessPoliciesOutput, error)
|
||||
ListAccessPolicies(input *eks.ListAccessPoliciesInput, accountID string) (*eks.ListAccessPoliciesOutput, error)
|
||||
|
||||
// Addons
|
||||
ListAddons(input *eks.ListAddonsInput, accountID string) (*eks.ListAddonsOutput, error)
|
||||
DescribeAddonVersions(input *eks.DescribeAddonVersionsInput, accountID string) (*eks.DescribeAddonVersionsOutput, error)
|
||||
CreateAddon(input *eks.CreateAddonInput, accountID string) (*eks.CreateAddonOutput, error)
|
||||
DeleteAddon(input *eks.DeleteAddonInput, accountID string) (*eks.DeleteAddonOutput, error)
|
||||
DescribeAddon(input *eks.DescribeAddonInput, accountID string) (*eks.DescribeAddonOutput, error)
|
||||
UpdateAddon(input *eks.UpdateAddonInput, accountID string) (*eks.UpdateAddonOutput, error)
|
||||
|
||||
// OIDC identity-provider configs (control-plane API only; the anonymous
|
||||
// OIDC issuer routes live with the awsgw IRSA wiring).
|
||||
AssociateIdentityProviderConfig(input *eks.AssociateIdentityProviderConfigInput, accountID string) (*eks.AssociateIdentityProviderConfigOutput, error)
|
||||
DescribeIdentityProviderConfig(input *eks.DescribeIdentityProviderConfigInput, accountID string) (*eks.DescribeIdentityProviderConfigOutput, error)
|
||||
ListIdentityProviderConfigs(input *eks.ListIdentityProviderConfigsInput, accountID string) (*eks.ListIdentityProviderConfigsOutput, error)
|
||||
DisassociateIdentityProviderConfig(input *eks.DisassociateIdentityProviderConfigInput, accountID string) (*eks.DisassociateIdentityProviderConfigOutput, error)
|
||||
|
||||
// Tags
|
||||
TagResource(input *eks.TagResourceInput, accountID string) (*eks.TagResourceOutput, error)
|
||||
UntagResource(input *eks.UntagResourceInput, accountID string) (*eks.UntagResourceOutput, error)
|
||||
ListTagsForResource(input *eks.ListTagsForResourceInput, accountID string) (*eks.ListTagsForResourceOutput, error)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package handlers_eks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
"github.com/mulgadc/spinifex/spinifex/config"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// EKSServiceImpl is the daemon-side EKSService. All methods currently return
|
||||
// NotImplemented; bodies land alongside the K3s control-plane work.
|
||||
type EKSServiceImpl struct {
|
||||
config *config.Config
|
||||
nc *nats.Conn
|
||||
store *Store
|
||||
leaderKV nats.KeyValue
|
||||
}
|
||||
|
||||
var _ EKSService = (*EKSServiceImpl)(nil)
|
||||
|
||||
// NewEKSServiceImplWithNATS wires the Store handle and creates the shared
|
||||
// spinifex-eks-leader bucket so the leader-lease CAS loop has a ready bucket
|
||||
// to CAS against without a chicken-and-egg bootstrap.
|
||||
func NewEKSServiceImplWithNATS(cfg *config.Config, nc *nats.Conn) (*EKSServiceImpl, error) {
|
||||
store, err := NewStore(nc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create EKS store: %w", err)
|
||||
}
|
||||
js, err := nc.JetStream()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get JetStream context: %w", err)
|
||||
}
|
||||
leaderKV, err := InitLeaderBucket(js)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EKSServiceImpl{
|
||||
config: cfg,
|
||||
nc: nc,
|
||||
store: store,
|
||||
leaderKV: leaderKV,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func notImpl() error { return errors.New(awserrors.ErrorNotImplemented) }
|
||||
|
||||
// --- Cluster ---
|
||||
|
||||
func (s *EKSServiceImpl) CreateCluster(_ *eks.CreateClusterInput, _ string) (*eks.CreateClusterOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) DescribeCluster(_ *eks.DescribeClusterInput, _ string) (*eks.DescribeClusterOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) ListClusters(_ *eks.ListClustersInput, _ string) (*eks.ListClustersOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) UpdateClusterConfig(_ *eks.UpdateClusterConfigInput, _ string) (*eks.UpdateClusterConfigOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) UpdateClusterVersion(_ *eks.UpdateClusterVersionInput, _ string) (*eks.UpdateClusterVersionOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) DeleteCluster(_ *eks.DeleteClusterInput, _ string) (*eks.DeleteClusterOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
// --- Nodegroup ---
|
||||
|
||||
func (s *EKSServiceImpl) CreateNodegroup(_ *eks.CreateNodegroupInput, _ string) (*eks.CreateNodegroupOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) DescribeNodegroup(_ *eks.DescribeNodegroupInput, _ string) (*eks.DescribeNodegroupOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) ListNodegroups(_ *eks.ListNodegroupsInput, _ string) (*eks.ListNodegroupsOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) UpdateNodegroupConfig(_ *eks.UpdateNodegroupConfigInput, _ string) (*eks.UpdateNodegroupConfigOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) UpdateNodegroupVersion(_ *eks.UpdateNodegroupVersionInput, _ string) (*eks.UpdateNodegroupVersionOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) DeleteNodegroup(_ *eks.DeleteNodegroupInput, _ string) (*eks.DeleteNodegroupOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
// --- AccessEntry + AccessPolicy ---
|
||||
|
||||
func (s *EKSServiceImpl) CreateAccessEntry(_ *eks.CreateAccessEntryInput, _ string) (*eks.CreateAccessEntryOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) DescribeAccessEntry(_ *eks.DescribeAccessEntryInput, _ string) (*eks.DescribeAccessEntryOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) ListAccessEntries(_ *eks.ListAccessEntriesInput, _ string) (*eks.ListAccessEntriesOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) UpdateAccessEntry(_ *eks.UpdateAccessEntryInput, _ string) (*eks.UpdateAccessEntryOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) DeleteAccessEntry(_ *eks.DeleteAccessEntryInput, _ string) (*eks.DeleteAccessEntryOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) AssociateAccessPolicy(_ *eks.AssociateAccessPolicyInput, _ string) (*eks.AssociateAccessPolicyOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) DisassociateAccessPolicy(_ *eks.DisassociateAccessPolicyInput, _ string) (*eks.DisassociateAccessPolicyOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) ListAssociatedAccessPolicies(_ *eks.ListAssociatedAccessPoliciesInput, _ string) (*eks.ListAssociatedAccessPoliciesOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) ListAccessPolicies(_ *eks.ListAccessPoliciesInput, _ string) (*eks.ListAccessPoliciesOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
// --- Addons ---
|
||||
|
||||
func (s *EKSServiceImpl) ListAddons(_ *eks.ListAddonsInput, _ string) (*eks.ListAddonsOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) DescribeAddonVersions(_ *eks.DescribeAddonVersionsInput, _ string) (*eks.DescribeAddonVersionsOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) CreateAddon(_ *eks.CreateAddonInput, _ string) (*eks.CreateAddonOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) DeleteAddon(_ *eks.DeleteAddonInput, _ string) (*eks.DeleteAddonOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) DescribeAddon(_ *eks.DescribeAddonInput, _ string) (*eks.DescribeAddonOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) UpdateAddon(_ *eks.UpdateAddonInput, _ string) (*eks.UpdateAddonOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
// --- OIDC identity-provider configs ---
|
||||
|
||||
func (s *EKSServiceImpl) AssociateIdentityProviderConfig(_ *eks.AssociateIdentityProviderConfigInput, _ string) (*eks.AssociateIdentityProviderConfigOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) DescribeIdentityProviderConfig(_ *eks.DescribeIdentityProviderConfigInput, _ string) (*eks.DescribeIdentityProviderConfigOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) ListIdentityProviderConfigs(_ *eks.ListIdentityProviderConfigsInput, _ string) (*eks.ListIdentityProviderConfigsOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) DisassociateIdentityProviderConfig(_ *eks.DisassociateIdentityProviderConfigInput, _ string) (*eks.DisassociateIdentityProviderConfigOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
// --- Tags ---
|
||||
|
||||
func (s *EKSServiceImpl) TagResource(_ *eks.TagResourceInput, _ string) (*eks.TagResourceOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) UntagResource(_ *eks.UntagResourceInput, _ string) (*eks.UntagResourceOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
|
||||
func (s *EKSServiceImpl) ListTagsForResource(_ *eks.ListTagsForResourceInput, _ string) (*eks.ListTagsForResourceOutput, error) {
|
||||
return nil, notImpl()
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package handlers_eks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
"github.com/mulgadc/spinifex/spinifex/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupTestService(t *testing.T) *EKSServiceImpl {
|
||||
t.Helper()
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
svc, err := NewEKSServiceImplWithNATS(nil, nc)
|
||||
require.NoError(t, err)
|
||||
return svc
|
||||
}
|
||||
|
||||
func TestEKSServiceImpl_ClusterMethodsReturnNotImplemented(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
|
||||
_, err := svc.CreateCluster(&eks.CreateClusterInput{Name: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.DescribeCluster(&eks.DescribeClusterInput{Name: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.ListClusters(&eks.ListClustersInput{}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.UpdateClusterConfig(&eks.UpdateClusterConfigInput{Name: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.UpdateClusterVersion(&eks.UpdateClusterVersionInput{Name: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.DeleteCluster(&eks.DeleteClusterInput{Name: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
}
|
||||
|
||||
func TestEKSServiceImpl_NodegroupMethodsReturnNotImplemented(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
|
||||
_, err := svc.CreateNodegroup(&eks.CreateNodegroupInput{ClusterName: aws.String("c1"), NodegroupName: aws.String("ng1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.DescribeNodegroup(&eks.DescribeNodegroupInput{ClusterName: aws.String("c1"), NodegroupName: aws.String("ng1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.ListNodegroups(&eks.ListNodegroupsInput{ClusterName: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.UpdateNodegroupConfig(&eks.UpdateNodegroupConfigInput{ClusterName: aws.String("c1"), NodegroupName: aws.String("ng1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.UpdateNodegroupVersion(&eks.UpdateNodegroupVersionInput{ClusterName: aws.String("c1"), NodegroupName: aws.String("ng1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.DeleteNodegroup(&eks.DeleteNodegroupInput{ClusterName: aws.String("c1"), NodegroupName: aws.String("ng1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
}
|
||||
|
||||
func TestEKSServiceImpl_AccessMethodsReturnNotImplemented(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
|
||||
_, err := svc.CreateAccessEntry(&eks.CreateAccessEntryInput{ClusterName: aws.String("c1"), PrincipalArn: aws.String("arn:aws:iam::111122223333:user/dev")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.DescribeAccessEntry(&eks.DescribeAccessEntryInput{ClusterName: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.ListAccessEntries(&eks.ListAccessEntriesInput{ClusterName: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.UpdateAccessEntry(&eks.UpdateAccessEntryInput{ClusterName: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.DeleteAccessEntry(&eks.DeleteAccessEntryInput{ClusterName: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.AssociateAccessPolicy(&eks.AssociateAccessPolicyInput{ClusterName: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.DisassociateAccessPolicy(&eks.DisassociateAccessPolicyInput{ClusterName: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.ListAssociatedAccessPolicies(&eks.ListAssociatedAccessPoliciesInput{ClusterName: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.ListAccessPolicies(&eks.ListAccessPoliciesInput{}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
}
|
||||
|
||||
func TestEKSServiceImpl_AddonsMethodsReturnNotImplemented(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
|
||||
_, err := svc.ListAddons(&eks.ListAddonsInput{ClusterName: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.DescribeAddonVersions(&eks.DescribeAddonVersionsInput{}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.CreateAddon(&eks.CreateAddonInput{ClusterName: aws.String("c1"), AddonName: aws.String("vpc-cni")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.DeleteAddon(&eks.DeleteAddonInput{ClusterName: aws.String("c1"), AddonName: aws.String("vpc-cni")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.DescribeAddon(&eks.DescribeAddonInput{ClusterName: aws.String("c1"), AddonName: aws.String("vpc-cni")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.UpdateAddon(&eks.UpdateAddonInput{ClusterName: aws.String("c1"), AddonName: aws.String("vpc-cni")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
}
|
||||
|
||||
func TestEKSServiceImpl_OIDCMethodsReturnNotImplemented(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
|
||||
_, err := svc.AssociateIdentityProviderConfig(&eks.AssociateIdentityProviderConfigInput{ClusterName: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.DescribeIdentityProviderConfig(&eks.DescribeIdentityProviderConfigInput{ClusterName: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.ListIdentityProviderConfigs(&eks.ListIdentityProviderConfigsInput{ClusterName: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.DisassociateIdentityProviderConfig(&eks.DisassociateIdentityProviderConfigInput{ClusterName: aws.String("c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
}
|
||||
|
||||
func TestEKSServiceImpl_TagsMethodsReturnNotImplemented(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
|
||||
_, err := svc.TagResource(&eks.TagResourceInput{ResourceArn: aws.String("arn:aws:eks:us-east-1:111122223333:cluster/c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.UntagResource(&eks.UntagResourceInput{ResourceArn: aws.String("arn:aws:eks:us-east-1:111122223333:cluster/c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
|
||||
_, err = svc.ListTagsForResource(&eks.ListTagsForResourceInput{ResourceArn: aws.String("arn:aws:eks:us-east-1:111122223333:cluster/c1")}, testAccountID)
|
||||
require.EqualError(t, err, awserrors.ErrorNotImplemented)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package handlers_eks
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
"github.com/mulgadc/spinifex/spinifex/utils"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
const defaultTimeout = 30 * time.Second
|
||||
|
||||
// NATSEKSService is the gateway-side adapter that forwards every EKSService
|
||||
// method as a NATS request to the daemon's matching subscriber.
|
||||
type NATSEKSService struct {
|
||||
natsConn *nats.Conn
|
||||
}
|
||||
|
||||
var _ EKSService = (*NATSEKSService)(nil)
|
||||
|
||||
// NewNATSEKSService returns an EKSService that uses NATS request-response.
|
||||
func NewNATSEKSService(conn *nats.Conn) EKSService {
|
||||
return &NATSEKSService{natsConn: conn}
|
||||
}
|
||||
|
||||
// --- Cluster ---
|
||||
|
||||
func (s *NATSEKSService) CreateCluster(input *eks.CreateClusterInput, accountID string) (*eks.CreateClusterOutput, error) {
|
||||
return utils.NATSRequest[eks.CreateClusterOutput](s.natsConn, "eks.CreateCluster", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) DescribeCluster(input *eks.DescribeClusterInput, accountID string) (*eks.DescribeClusterOutput, error) {
|
||||
return utils.NATSRequest[eks.DescribeClusterOutput](s.natsConn, "eks.DescribeCluster", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) ListClusters(input *eks.ListClustersInput, accountID string) (*eks.ListClustersOutput, error) {
|
||||
return utils.NATSRequest[eks.ListClustersOutput](s.natsConn, "eks.ListClusters", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) UpdateClusterConfig(input *eks.UpdateClusterConfigInput, accountID string) (*eks.UpdateClusterConfigOutput, error) {
|
||||
return utils.NATSRequest[eks.UpdateClusterConfigOutput](s.natsConn, "eks.UpdateClusterConfig", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) UpdateClusterVersion(input *eks.UpdateClusterVersionInput, accountID string) (*eks.UpdateClusterVersionOutput, error) {
|
||||
return utils.NATSRequest[eks.UpdateClusterVersionOutput](s.natsConn, "eks.UpdateClusterVersion", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) DeleteCluster(input *eks.DeleteClusterInput, accountID string) (*eks.DeleteClusterOutput, error) {
|
||||
return utils.NATSRequest[eks.DeleteClusterOutput](s.natsConn, "eks.DeleteCluster", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
// --- Nodegroup ---
|
||||
|
||||
func (s *NATSEKSService) CreateNodegroup(input *eks.CreateNodegroupInput, accountID string) (*eks.CreateNodegroupOutput, error) {
|
||||
return utils.NATSRequest[eks.CreateNodegroupOutput](s.natsConn, "eks.CreateNodegroup", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) DescribeNodegroup(input *eks.DescribeNodegroupInput, accountID string) (*eks.DescribeNodegroupOutput, error) {
|
||||
return utils.NATSRequest[eks.DescribeNodegroupOutput](s.natsConn, "eks.DescribeNodegroup", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) ListNodegroups(input *eks.ListNodegroupsInput, accountID string) (*eks.ListNodegroupsOutput, error) {
|
||||
return utils.NATSRequest[eks.ListNodegroupsOutput](s.natsConn, "eks.ListNodegroups", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) UpdateNodegroupConfig(input *eks.UpdateNodegroupConfigInput, accountID string) (*eks.UpdateNodegroupConfigOutput, error) {
|
||||
return utils.NATSRequest[eks.UpdateNodegroupConfigOutput](s.natsConn, "eks.UpdateNodegroupConfig", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) UpdateNodegroupVersion(input *eks.UpdateNodegroupVersionInput, accountID string) (*eks.UpdateNodegroupVersionOutput, error) {
|
||||
return utils.NATSRequest[eks.UpdateNodegroupVersionOutput](s.natsConn, "eks.UpdateNodegroupVersion", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) DeleteNodegroup(input *eks.DeleteNodegroupInput, accountID string) (*eks.DeleteNodegroupOutput, error) {
|
||||
return utils.NATSRequest[eks.DeleteNodegroupOutput](s.natsConn, "eks.DeleteNodegroup", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
// --- AccessEntry + AccessPolicy ---
|
||||
|
||||
func (s *NATSEKSService) CreateAccessEntry(input *eks.CreateAccessEntryInput, accountID string) (*eks.CreateAccessEntryOutput, error) {
|
||||
return utils.NATSRequest[eks.CreateAccessEntryOutput](s.natsConn, "eks.CreateAccessEntry", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) DescribeAccessEntry(input *eks.DescribeAccessEntryInput, accountID string) (*eks.DescribeAccessEntryOutput, error) {
|
||||
return utils.NATSRequest[eks.DescribeAccessEntryOutput](s.natsConn, "eks.DescribeAccessEntry", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) ListAccessEntries(input *eks.ListAccessEntriesInput, accountID string) (*eks.ListAccessEntriesOutput, error) {
|
||||
return utils.NATSRequest[eks.ListAccessEntriesOutput](s.natsConn, "eks.ListAccessEntries", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) UpdateAccessEntry(input *eks.UpdateAccessEntryInput, accountID string) (*eks.UpdateAccessEntryOutput, error) {
|
||||
return utils.NATSRequest[eks.UpdateAccessEntryOutput](s.natsConn, "eks.UpdateAccessEntry", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) DeleteAccessEntry(input *eks.DeleteAccessEntryInput, accountID string) (*eks.DeleteAccessEntryOutput, error) {
|
||||
return utils.NATSRequest[eks.DeleteAccessEntryOutput](s.natsConn, "eks.DeleteAccessEntry", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) AssociateAccessPolicy(input *eks.AssociateAccessPolicyInput, accountID string) (*eks.AssociateAccessPolicyOutput, error) {
|
||||
return utils.NATSRequest[eks.AssociateAccessPolicyOutput](s.natsConn, "eks.AssociateAccessPolicy", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) DisassociateAccessPolicy(input *eks.DisassociateAccessPolicyInput, accountID string) (*eks.DisassociateAccessPolicyOutput, error) {
|
||||
return utils.NATSRequest[eks.DisassociateAccessPolicyOutput](s.natsConn, "eks.DisassociateAccessPolicy", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) ListAssociatedAccessPolicies(input *eks.ListAssociatedAccessPoliciesInput, accountID string) (*eks.ListAssociatedAccessPoliciesOutput, error) {
|
||||
return utils.NATSRequest[eks.ListAssociatedAccessPoliciesOutput](s.natsConn, "eks.ListAssociatedAccessPolicies", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) ListAccessPolicies(input *eks.ListAccessPoliciesInput, accountID string) (*eks.ListAccessPoliciesOutput, error) {
|
||||
return utils.NATSRequest[eks.ListAccessPoliciesOutput](s.natsConn, "eks.ListAccessPolicies", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
// --- Addons ---
|
||||
|
||||
func (s *NATSEKSService) ListAddons(input *eks.ListAddonsInput, accountID string) (*eks.ListAddonsOutput, error) {
|
||||
return utils.NATSRequest[eks.ListAddonsOutput](s.natsConn, "eks.ListAddons", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) DescribeAddonVersions(input *eks.DescribeAddonVersionsInput, accountID string) (*eks.DescribeAddonVersionsOutput, error) {
|
||||
return utils.NATSRequest[eks.DescribeAddonVersionsOutput](s.natsConn, "eks.DescribeAddonVersions", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) CreateAddon(input *eks.CreateAddonInput, accountID string) (*eks.CreateAddonOutput, error) {
|
||||
return utils.NATSRequest[eks.CreateAddonOutput](s.natsConn, "eks.CreateAddon", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) DeleteAddon(input *eks.DeleteAddonInput, accountID string) (*eks.DeleteAddonOutput, error) {
|
||||
return utils.NATSRequest[eks.DeleteAddonOutput](s.natsConn, "eks.DeleteAddon", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) DescribeAddon(input *eks.DescribeAddonInput, accountID string) (*eks.DescribeAddonOutput, error) {
|
||||
return utils.NATSRequest[eks.DescribeAddonOutput](s.natsConn, "eks.DescribeAddon", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) UpdateAddon(input *eks.UpdateAddonInput, accountID string) (*eks.UpdateAddonOutput, error) {
|
||||
return utils.NATSRequest[eks.UpdateAddonOutput](s.natsConn, "eks.UpdateAddon", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
// --- OIDC identity-provider configs ---
|
||||
|
||||
func (s *NATSEKSService) AssociateIdentityProviderConfig(input *eks.AssociateIdentityProviderConfigInput, accountID string) (*eks.AssociateIdentityProviderConfigOutput, error) {
|
||||
return utils.NATSRequest[eks.AssociateIdentityProviderConfigOutput](s.natsConn, "eks.AssociateIdentityProviderConfig", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) DescribeIdentityProviderConfig(input *eks.DescribeIdentityProviderConfigInput, accountID string) (*eks.DescribeIdentityProviderConfigOutput, error) {
|
||||
return utils.NATSRequest[eks.DescribeIdentityProviderConfigOutput](s.natsConn, "eks.DescribeIdentityProviderConfig", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) ListIdentityProviderConfigs(input *eks.ListIdentityProviderConfigsInput, accountID string) (*eks.ListIdentityProviderConfigsOutput, error) {
|
||||
return utils.NATSRequest[eks.ListIdentityProviderConfigsOutput](s.natsConn, "eks.ListIdentityProviderConfigs", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) DisassociateIdentityProviderConfig(input *eks.DisassociateIdentityProviderConfigInput, accountID string) (*eks.DisassociateIdentityProviderConfigOutput, error) {
|
||||
return utils.NATSRequest[eks.DisassociateIdentityProviderConfigOutput](s.natsConn, "eks.DisassociateIdentityProviderConfig", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
// --- Tags ---
|
||||
|
||||
func (s *NATSEKSService) TagResource(input *eks.TagResourceInput, accountID string) (*eks.TagResourceOutput, error) {
|
||||
return utils.NATSRequest[eks.TagResourceOutput](s.natsConn, "eks.TagResource", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) UntagResource(input *eks.UntagResourceInput, accountID string) (*eks.UntagResourceOutput, error) {
|
||||
return utils.NATSRequest[eks.UntagResourceOutput](s.natsConn, "eks.UntagResource", input, defaultTimeout, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEKSService) ListTagsForResource(input *eks.ListTagsForResourceInput, accountID string) (*eks.ListTagsForResourceOutput, error) {
|
||||
return utils.NATSRequest[eks.ListTagsForResourceOutput](s.natsConn, "eks.ListTagsForResource", input, defaultTimeout, accountID)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package handlers_eks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/migrate"
|
||||
"github.com/mulgadc/spinifex/spinifex/utils"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// JetStream KV bucket and key-path constants for the EKS control plane.
|
||||
//
|
||||
// Per-account bucket "eks-account-{accountID}" holds all customer-visible
|
||||
// cluster state. It is created lazily on first cluster touch (no daemon-boot
|
||||
// pre-creation) so accounts without any EKS clusters never grow a bucket.
|
||||
//
|
||||
// Shared leader bucket "spinifex-eks-leader" holds 60s-TTL CAS locks
|
||||
// keyed by "{accountID}/{clusterName}" for the per-cluster reconciler
|
||||
// leader election. Created once at daemon boot.
|
||||
const (
|
||||
KVBucketEKSAccountPrefix = "eks-account-"
|
||||
KVBucketEKSAccountVersion = 1
|
||||
|
||||
KVBucketEKSLeader = "spinifex-eks-leader"
|
||||
KVBucketEKSLeaderVersion = 1
|
||||
KVBucketEKSLeaderTTL = 60 * time.Second
|
||||
)
|
||||
|
||||
// Key-path helpers for the per-account bucket. The layout matches the
|
||||
// EKS v1 spec (Q2):
|
||||
//
|
||||
// clusters/{name}/meta
|
||||
// clusters/{name}/nodegroups/{ngName}
|
||||
// clusters/{name}/access-entries/{principalARN}
|
||||
// clusters/{name}/oidc-providers/{issuerHash}
|
||||
// clusters/{name}/oidc-signing-key.pem.enc
|
||||
// clusters/{name}/oidc-jwks.json
|
||||
// clusters/{name}/admin-kubeconfig.enc
|
||||
// clusters/{name}/k3s-node-token.enc
|
||||
// clusters/{name}/events/{ts}
|
||||
|
||||
// ClusterMetaKey returns the KV key for a cluster's meta record.
|
||||
func ClusterMetaKey(name string) string {
|
||||
return fmt.Sprintf("clusters/%s/meta", name)
|
||||
}
|
||||
|
||||
// NodegroupKey returns the KV key for a nodegroup record under a cluster.
|
||||
func NodegroupKey(cluster, ng string) string {
|
||||
return fmt.Sprintf("clusters/%s/nodegroups/%s", cluster, ng)
|
||||
}
|
||||
|
||||
// AccessEntryKey returns the KV key for an AccessEntry record under a cluster.
|
||||
func AccessEntryKey(cluster, principalARN string) string {
|
||||
return fmt.Sprintf("clusters/%s/access-entries/%s", cluster, principalARN)
|
||||
}
|
||||
|
||||
// OIDCProviderKey returns the KV key for a registered OIDC provider config
|
||||
// under a cluster.
|
||||
func OIDCProviderKey(cluster, issuerHash string) string {
|
||||
return fmt.Sprintf("clusters/%s/oidc-providers/%s", cluster, issuerHash)
|
||||
}
|
||||
|
||||
// OIDCSigningKeyKey returns the KV key for a cluster's encrypted ECDSA-P256
|
||||
// OIDC signing private key.
|
||||
func OIDCSigningKeyKey(cluster string) string {
|
||||
return fmt.Sprintf("clusters/%s/oidc-signing-key.pem.enc", cluster)
|
||||
}
|
||||
|
||||
// OIDCJWKSKey returns the KV key for a cluster's public OIDC JWKS document.
|
||||
func OIDCJWKSKey(cluster string) string {
|
||||
return fmt.Sprintf("clusters/%s/oidc-jwks.json", cluster)
|
||||
}
|
||||
|
||||
// AdminKubeconfigKey returns the KV key for a cluster's encrypted admin
|
||||
// kubeconfig (used by the spinifex-side nodegroup reconciler).
|
||||
func AdminKubeconfigKey(cluster string) string {
|
||||
return fmt.Sprintf("clusters/%s/admin-kubeconfig.enc", cluster)
|
||||
}
|
||||
|
||||
// NodeTokenKey returns the KV key for a cluster's encrypted K3s bootstrap
|
||||
// node-token (shared across all nodegroups in the cluster).
|
||||
func NodeTokenKey(cluster string) string {
|
||||
return fmt.Sprintf("clusters/%s/k3s-node-token.enc", cluster)
|
||||
}
|
||||
|
||||
// EventKey returns the KV key for a cluster-scoped event record.
|
||||
func EventKey(cluster, ts string) string {
|
||||
return fmt.Sprintf("clusters/%s/events/%s", cluster, ts)
|
||||
}
|
||||
|
||||
// Store is the per-daemon EKS KV handle. Per-account and leader buckets are
|
||||
// accessed via the package-level factories below.
|
||||
type Store struct {
|
||||
nc *nats.Conn
|
||||
}
|
||||
|
||||
// NewStore constructs a Store bound to the supplied NATS connection. It does
|
||||
// not touch JetStream — per-account buckets are created lazily by
|
||||
// GetOrCreateAccountBucket and the leader bucket by InitLeaderBucket.
|
||||
func NewStore(nc *nats.Conn) (*Store, error) {
|
||||
if nc == nil {
|
||||
return nil, errors.New("eks store: nats connection is nil")
|
||||
}
|
||||
return &Store{nc: nc}, nil
|
||||
}
|
||||
|
||||
// AccountBucketName returns the per-account JetStream KV bucket name for the
|
||||
// given AWS account ID.
|
||||
func AccountBucketName(accountID string) string {
|
||||
return KVBucketEKSAccountPrefix + accountID
|
||||
}
|
||||
|
||||
// GetOrCreateAccountBucket returns the per-account KV bucket for accountID,
|
||||
// creating it on first use. Idempotent: subsequent calls with the same
|
||||
// accountID return the existing handle.
|
||||
func GetOrCreateAccountBucket(js nats.JetStreamContext, accountID string) (nats.KeyValue, error) {
|
||||
bucket := AccountBucketName(accountID)
|
||||
kv, err := utils.GetOrCreateKVBucket(js, bucket, KVBucketEKSAccountVersion)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create EKS per-account KV bucket %s: %w", bucket, err)
|
||||
}
|
||||
if err := migrate.DefaultRegistry.RunKV(bucket, kv, KVBucketEKSAccountVersion); err != nil {
|
||||
return nil, fmt.Errorf("migrate %s: %w", bucket, err)
|
||||
}
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
// InitLeaderBucket creates (or attaches to) the shared spinifex-eks-leader
|
||||
// bucket used for per-cluster reconciler leader-lease CAS locks. The bucket
|
||||
// is configured with History=1 and a 60s TTL so stale leases expire on their
|
||||
// own when a leader dies mid-cycle. utils.GetOrCreateKVBucket doesn't expose
|
||||
// a TTL knob, so this function takes the direct js.CreateKeyValue path and
|
||||
// falls back to js.KeyValue on already-exists.
|
||||
func InitLeaderBucket(js nats.JetStreamContext) (nats.KeyValue, error) {
|
||||
kv, err := js.CreateKeyValue(&nats.KeyValueConfig{
|
||||
Bucket: KVBucketEKSLeader,
|
||||
History: 1,
|
||||
TTL: KVBucketEKSLeaderTTL,
|
||||
})
|
||||
if err != nil {
|
||||
kv, err = js.KeyValue(KVBucketEKSLeader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create or open EKS leader bucket %s: %w", KVBucketEKSLeader, err)
|
||||
}
|
||||
}
|
||||
if err := migrate.DefaultRegistry.RunKV(KVBucketEKSLeader, kv, KVBucketEKSLeaderVersion); err != nil {
|
||||
return nil, fmt.Errorf("migrate %s: %w", KVBucketEKSLeader, err)
|
||||
}
|
||||
slog.Info("EKS leader bucket initialized", "bucket", KVBucketEKSLeader, "ttl", KVBucketEKSLeaderTTL)
|
||||
return kv, nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package handlers_eks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testAccountID = "111122223333"
|
||||
|
||||
func TestGetOrCreateAccountBucket_Idempotent(t *testing.T) {
|
||||
_, _, js := testutil.StartTestJetStream(t)
|
||||
|
||||
kv1, err := GetOrCreateAccountBucket(js, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, kv1)
|
||||
|
||||
kv2, err := GetOrCreateAccountBucket(js, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, kv2)
|
||||
|
||||
assert.Equal(t, AccountBucketName(testAccountID), kv1.Bucket())
|
||||
assert.Equal(t, kv1.Bucket(), kv2.Bucket())
|
||||
}
|
||||
|
||||
func TestInitLeaderBucket_Idempotent(t *testing.T) {
|
||||
_, _, js := testutil.StartTestJetStream(t)
|
||||
|
||||
kv1, err := InitLeaderBucket(js)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, kv1)
|
||||
assert.Equal(t, KVBucketEKSLeader, kv1.Bucket())
|
||||
|
||||
kv2, err := InitLeaderBucket(js)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, kv2)
|
||||
assert.Equal(t, KVBucketEKSLeader, kv2.Bucket())
|
||||
}
|
||||
|
||||
func TestKeyPaths_MatchQ2Spec(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
got string
|
||||
want string
|
||||
}{
|
||||
{"ClusterMetaKey", ClusterMetaKey("alpha"), "clusters/alpha/meta"},
|
||||
{"NodegroupKey", NodegroupKey("alpha", "ng-1"), "clusters/alpha/nodegroups/ng-1"},
|
||||
{"AccessEntryKey", AccessEntryKey("alpha", "arn:aws:iam::111122223333:user/dev"), "clusters/alpha/access-entries/arn:aws:iam::111122223333:user/dev"},
|
||||
{"OIDCProviderKey", OIDCProviderKey("alpha", "abc123"), "clusters/alpha/oidc-providers/abc123"},
|
||||
{"OIDCSigningKeyKey", OIDCSigningKeyKey("alpha"), "clusters/alpha/oidc-signing-key.pem.enc"},
|
||||
{"OIDCJWKSKey", OIDCJWKSKey("alpha"), "clusters/alpha/oidc-jwks.json"},
|
||||
{"AdminKubeconfigKey", AdminKubeconfigKey("alpha"), "clusters/alpha/admin-kubeconfig.enc"},
|
||||
{"NodeTokenKey", NodeTokenKey("alpha"), "clusters/alpha/k3s-node-token.enc"},
|
||||
{"EventKey", EventKey("alpha", "1700000000"), "clusters/alpha/events/1700000000"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, tc.got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountBucketName(t *testing.T) {
|
||||
assert.Equal(t, "eks-account-111122223333", AccountBucketName(testAccountID))
|
||||
}
|
||||
|
||||
func TestNewStore_NilConn(t *testing.T) {
|
||||
_, err := NewStore(nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package handlers_eks
|
||||
|
||||
import "time"
|
||||
|
||||
// Cluster lifecycle status strings — values match the AWS EKS DescribeCluster
|
||||
// status enum verbatim so clients can match against them without translation.
|
||||
const (
|
||||
StatusCreating = "CREATING"
|
||||
StatusActive = "ACTIVE"
|
||||
StatusUpdating = "UPDATING"
|
||||
StatusDeleting = "DELETING"
|
||||
StatusFailed = "FAILED"
|
||||
)
|
||||
|
||||
// ClusterRecord is the persisted-state envelope for an EKS cluster. Only the
|
||||
// fields actually persisted by the in-tree handlers live here — the public
|
||||
// AWS DescribeCluster shape is reconstructed at the gateway/service boundary
|
||||
// from the aws-sdk-go eks types directly.
|
||||
type ClusterRecord struct {
|
||||
AccountID string `json:"accountID"`
|
||||
Name string `json:"name"`
|
||||
Region string `json:"region"`
|
||||
ARN string `json:"arn"`
|
||||
Status string `json:"status"`
|
||||
Version string `json:"version"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// NodegroupRecord is the persisted-state envelope for an EKS managed
|
||||
// nodegroup.
|
||||
type NodegroupRecord struct {
|
||||
ClusterName string `json:"clusterName"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
DesiredSize int64 `json:"desiredSize"`
|
||||
InstanceTypes []string `json:"instanceTypes,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// AccessEntryRecord is the persisted-state envelope for an EKS API-mode
|
||||
// AccessEntry (Q9).
|
||||
type AccessEntryRecord struct {
|
||||
ClusterName string `json:"clusterName"`
|
||||
PrincipalARN string `json:"principalARN"`
|
||||
KubernetesUsername string `json:"kubernetesUsername"`
|
||||
KubernetesGroups []string `json:"kubernetesGroups,omitempty"`
|
||||
Type string `json:"type"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// OIDCProviderConfigRecord captures the minimum needed to register an OIDC
|
||||
// identity provider against a cluster.
|
||||
type OIDCProviderConfigRecord struct {
|
||||
ClusterName string `json:"clusterName"`
|
||||
IssuerURL string `json:"issuerURL"`
|
||||
IssuerHash string `json:"issuerHash"`
|
||||
ClientID string `json:"clientID"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
@@ -14,6 +14,10 @@ const (
|
||||
// (HAProxy VMs, their ENIs, the LB AMI).
|
||||
ManagedByELBv2 = "elbv2"
|
||||
|
||||
// ManagedByEKS identifies EKS-owned resources (K3s control-plane VMs,
|
||||
// their ENIs, the eks-server / eks-agent AMIs, cluster + nodegroup SGs).
|
||||
ManagedByEKS = "eks"
|
||||
|
||||
// LBARNKey stores the parent LB ARN on ELBv2-managed ENIs.
|
||||
LBARNKey = "spinifex:lb-arn"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user