mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
feat: IAM account scoping (#61)
This commit is contained in:
@@ -33,6 +33,19 @@ else
|
||||
GO_BUILD_MOD :=
|
||||
endif
|
||||
|
||||
# Quiet-mode filters (active when QUIET=1, set by preflight via recursive make)
|
||||
ifdef QUIET
|
||||
_Q = @
|
||||
_COVQ = 2>&1 | grep -Ev '^\s*(ok|PASS|\?|=== RUN|--- PASS:)\s' | grep -v 'coverage: 0\.0%' || true
|
||||
_RACEQ = 2>&1 | { grep -Ev '^\s*(ok|PASS|\?|=== RUN|--- PASS:)\s' || true; }; exit $${PIPESTATUS[0]}
|
||||
_SECQ = >
|
||||
else
|
||||
_Q =
|
||||
_COVQ = || true
|
||||
_RACEQ =
|
||||
_SECQ = 2>&1 | tee
|
||||
endif
|
||||
|
||||
build:
|
||||
$(MAKE) go_build
|
||||
|
||||
@@ -52,7 +65,8 @@ go_run:
|
||||
|
||||
# Preflight — runs the same checks as GitHub Actions (format + lint + security + tests).
|
||||
# Use this before committing to catch CI failures locally.
|
||||
preflight: check-format check-modernize vet security-check test-cover diff-coverage test-race
|
||||
preflight:
|
||||
@$(MAKE) --no-print-directory QUIET=1 check-format check-modernize vet security-check test-cover diff-coverage test-race
|
||||
@echo -e "\n ✅ Preflight passed — safe to commit."
|
||||
|
||||
# Run unit tests
|
||||
@@ -64,33 +78,20 @@ test:
|
||||
# Note: go test may exit non-zero due to Go version mismatch in coverage instrumentation
|
||||
# for packages without test files. We check actual test results + coverage threshold instead.
|
||||
COVERPROFILE ?= coverage.out
|
||||
MIN_COVERAGE ?= 50.0
|
||||
MIN_COVERAGE ?= 60.0
|
||||
test-cover:
|
||||
@echo -e "\n....Running tests with coverage for $(GO_PROJECT_NAME)...."
|
||||
LOG_IGNORE=1 go test -timeout 120s -coverprofile=$(COVERPROFILE) -covermode=atomic ./hive/... || true
|
||||
@echo ""
|
||||
@echo "=== Total Coverage ==="
|
||||
@go tool cover -func=$(COVERPROFILE) | tail -1
|
||||
@TOTAL=$$(go tool cover -func=$(COVERPROFILE) | tail -1 | awk '{print $$NF}' | tr -d '%'); \
|
||||
if [ -z "$$TOTAL" ] || [ ! -s $(COVERPROFILE) ]; then \
|
||||
echo "ERROR: No coverage data generated — tests may have failed to compile"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
PASS=$$(echo "$$TOTAL >= $(MIN_COVERAGE)" | bc -l); \
|
||||
if [ "$$PASS" != "1" ]; then \
|
||||
echo "ERROR: Total coverage $${TOTAL}% is below minimum $(MIN_COVERAGE)%"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo "Coverage $${TOTAL}% meets minimum $(MIN_COVERAGE)% threshold"
|
||||
$(_Q)LOG_IGNORE=1 go test -timeout 120s -coverprofile=$(COVERPROFILE) -covermode=atomic ./hive/... $(_COVQ)
|
||||
@scripts/check-coverage.sh $(COVERPROFILE) $(MIN_COVERAGE) $(QUIET)
|
||||
|
||||
# Run unit tests with race detector
|
||||
test-race:
|
||||
@echo -e "\n....Running tests with race detector for $(GO_PROJECT_NAME)...."
|
||||
LOG_IGNORE=1 go test -race -timeout 300s ./hive/...
|
||||
$(_Q)LOG_IGNORE=1 go test -race -timeout 300s ./hive/... $(_RACEQ)
|
||||
|
||||
# Check that new/changed code meets coverage threshold (runs tests first)
|
||||
diff-coverage: test-cover
|
||||
@scripts/diff-coverage.sh $(COVERPROFILE)
|
||||
@QUIET=$(QUIET) scripts/diff-coverage.sh $(COVERPROFILE)
|
||||
|
||||
bench:
|
||||
@echo -e "\n....Running benchmarks for $(GO_PROJECT_NAME)...."
|
||||
@@ -158,7 +159,7 @@ check-format:
|
||||
# Go vet (fails on issues, matches CI)
|
||||
vet:
|
||||
@echo "Running go vet..."
|
||||
go vet ./...
|
||||
$(_Q)go vet ./...
|
||||
@echo " go vet ok"
|
||||
|
||||
# Excluded: newexpr (replaces aws.String with new, not idiomatic for AWS SDK)
|
||||
@@ -186,11 +187,11 @@ check-modernize:
|
||||
# Reports are also saved to tests/ for review.
|
||||
security-check:
|
||||
@echo -e "\n....Running security checks for $(GO_PROJECT_NAME)...."
|
||||
set -o pipefail && go tool govulncheck ./... 2>&1 | tee tests/govulncheck-report.txt
|
||||
$(_Q)set -o pipefail && go tool govulncheck ./... $(_SECQ) tests/govulncheck-report.txt $(if $(QUIET),|| { cat tests/govulncheck-report.txt; exit 1; })
|
||||
@echo " govulncheck ok"
|
||||
set -o pipefail && go tool gosec -quiet -exclude=G204,G304,G402 -exclude-generated -exclude-dir=cmd ./... 2>&1 | tee tests/gosec-report.txt
|
||||
$(_Q)set -o pipefail && go tool gosec -quiet -exclude=G204,G304,G402 -exclude-generated -exclude-dir=cmd ./... $(_SECQ) tests/gosec-report.txt $(if $(QUIET),|| { cat tests/gosec-report.txt; exit 1; })
|
||||
@echo " gosec ok"
|
||||
set -o pipefail && go tool staticcheck -checks="all,-ST1000,-ST1003,-ST1016,-ST1020,-ST1021,-ST1022,-SA1019,-SA9005" ./... 2>&1 | tee tests/staticcheck-report.txt
|
||||
$(_Q)set -o pipefail && go tool staticcheck -checks="all,-ST1000,-ST1003,-ST1016,-ST1020,-ST1021,-ST1022,-SA1019,-SA9005" ./... $(_SECQ) tests/staticcheck-report.txt $(if $(QUIET),|| { cat tests/staticcheck-report.txt; exit 1; })
|
||||
@echo " staticcheck ok"
|
||||
|
||||
# Docker E2E tests (mirrors GitHub Actions e2e.yml)
|
||||
|
||||
@@ -486,7 +486,7 @@ func (d *Daemon) Start() error {
|
||||
|
||||
// Ensure default VPC exists (matches AWS: every account has a default VPC)
|
||||
if d.vpcService != nil {
|
||||
if err := d.vpcService.EnsureDefaultVPC(); err != nil {
|
||||
if err := d.vpcService.EnsureDefaultVPC(handlers_ec2_vpc.GlobalAccountID); err != nil {
|
||||
slog.Warn("Failed to ensure default VPC", "error", err)
|
||||
}
|
||||
}
|
||||
@@ -1361,7 +1361,7 @@ func (d *Daemon) stopInstance(instances map[string]*vm.VM, deleteVolume bool) er
|
||||
slog.Info("Deleting volume with DeleteOnTermination=true", "name", ebsRequest.Name, "id", instance.ID)
|
||||
_, err := d.volumeService.DeleteVolume(&ec2.DeleteVolumeInput{
|
||||
VolumeId: &ebsRequest.Name,
|
||||
})
|
||||
}, instance.AccountID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to delete volume on termination", "name", ebsRequest.Name, "id", instance.ID, "err", err)
|
||||
} else {
|
||||
@@ -1382,7 +1382,7 @@ func (d *Daemon) stopInstance(instances map[string]*vm.VM, deleteVolume bool) er
|
||||
if deleteVolume && instance.ENIId != "" && d.vpcService != nil {
|
||||
_, eniErr := d.vpcService.DeleteNetworkInterface(&ec2.DeleteNetworkInterfaceInput{
|
||||
NetworkInterfaceId: &instance.ENIId,
|
||||
})
|
||||
}, instance.AccountID)
|
||||
if eniErr != nil {
|
||||
slog.Warn("Failed to delete ENI on termination", "eni", instance.ENIId, "err", eniErr)
|
||||
} else {
|
||||
|
||||
@@ -22,8 +22,9 @@ func respondWithError(msg *nats.Msg, errCode string) {
|
||||
}
|
||||
|
||||
// handleNATSRequest is a generic helper for the common unmarshal → service → marshal → respond pattern.
|
||||
// used for basic requests that don't modify any daemon state, just return the result
|
||||
func handleNATSRequest[I any, O any](msg *nats.Msg, serviceFn func(*I) (*O, error)) {
|
||||
// It extracts the account ID from the NATS message header and passes it to the service function.
|
||||
func handleNATSRequest[I any, O any](msg *nats.Msg, serviceFn func(*I, string) (*O, error)) {
|
||||
accountID := utils.AccountIDFromMsg(msg)
|
||||
input := new(I)
|
||||
if errResp := utils.UnmarshalJsonPayload(input, msg.Data); errResp != nil {
|
||||
if err := msg.Respond(errResp); err != nil {
|
||||
@@ -31,7 +32,7 @@ func handleNATSRequest[I any, O any](msg *nats.Msg, serviceFn func(*I) (*O, erro
|
||||
}
|
||||
return
|
||||
}
|
||||
output, err := serviceFn(input)
|
||||
output, err := serviceFn(input, accountID)
|
||||
if err != nil {
|
||||
if err := msg.Respond(utils.GenerateErrorPayload(awserrors.ValidErrorCode(err.Error()))); err != nil {
|
||||
slog.Error("Failed to respond to NATS request", "err", err)
|
||||
@@ -72,6 +73,14 @@ func (d *Daemon) handleEC2Events(msg *nats.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the caller owns this instance (skip for pre-Phase4 instances with no AccountID)
|
||||
callerAccountID := utils.AccountIDFromMsg(msg)
|
||||
if callerAccountID != "" && instance.AccountID != "" && instance.AccountID != callerAccountID {
|
||||
slog.Warn("Account does not own this instance", "instanceId", command.ID, "callerAccount", callerAccountID, "ownerAccount", instance.AccountID)
|
||||
respondWithError(msg, awserrors.ErrorInvalidInstanceIDNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case command.Attributes.AttachVolume:
|
||||
d.handleAttachVolume(msg, command, instance)
|
||||
|
||||
@@ -32,6 +32,7 @@ func (d *Daemon) handleEC2GetConsoleOutput(msg *nats.Msg) {
|
||||
}
|
||||
|
||||
instanceID := *input.InstanceId
|
||||
accountID := utils.AccountIDFromMsg(msg)
|
||||
|
||||
// Find the instance on this node
|
||||
d.Instances.Mu.Lock()
|
||||
@@ -43,6 +44,12 @@ func (d *Daemon) handleEC2GetConsoleOutput(msg *nats.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the caller owns this instance
|
||||
if accountID != "" && instance.AccountID != "" && instance.AccountID != accountID {
|
||||
respondWithError(msg, awserrors.ErrorInvalidInstanceIDNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
logPath := instance.Config.ConsoleLogPath
|
||||
var outputData []byte
|
||||
var modTime time.Time
|
||||
|
||||
@@ -29,6 +29,8 @@ func (d *Daemon) handleEC2CreateImage(msg *nats.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
accountID := utils.AccountIDFromMsg(msg)
|
||||
|
||||
if input.InstanceId == nil || *input.InstanceId == "" {
|
||||
respondWithError(msg, awserrors.ErrorMissingParameter)
|
||||
return
|
||||
@@ -63,6 +65,13 @@ func (d *Daemon) handleEC2CreateImage(msg *nats.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the caller owns this instance
|
||||
if accountID != "" && instance.AccountID != "" && instance.AccountID != accountID {
|
||||
slog.Warn("CreateImage: account does not own instance", "instanceId", instanceID, "accountID", accountID, "ownerAccountID", instance.AccountID)
|
||||
respondWithError(msg, awserrors.ErrorInvalidInstanceIDNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if status != vm.StateRunning && status != vm.StateStopped {
|
||||
slog.Warn("CreateImage: instance not in valid state", "instanceId", instanceID, "status", status)
|
||||
respondWithError(msg, awserrors.ErrorIncorrectInstanceState)
|
||||
@@ -82,7 +91,7 @@ func (d *Daemon) handleEC2CreateImage(msg *nats.Msg) {
|
||||
IsRunning: status == vm.StateRunning,
|
||||
}
|
||||
|
||||
output, err := d.imageService.CreateImageFromInstance(params)
|
||||
output, err := d.imageService.CreateImageFromInstance(params, accountID)
|
||||
if err != nil {
|
||||
slog.Error("CreateImage: service failed", "instanceId", instanceID, "err", err)
|
||||
respondWithError(msg, awserrors.ValidErrorCode(err.Error()))
|
||||
|
||||
@@ -23,6 +23,14 @@ func (d *Daemon) handleEC2RunInstances(msg *nats.Msg) {
|
||||
slog.Debug("Received message on subject", "subject", msg.Subject)
|
||||
slog.Debug("Message data", "data", string(msg.Data))
|
||||
|
||||
// Extract account ID from NATS header
|
||||
accountID := utils.AccountIDFromMsg(msg)
|
||||
if accountID == "" {
|
||||
slog.Error("handleEC2RunInstances: missing account ID in NATS header")
|
||||
respondWithError(msg, awserrors.ErrorServerInternal)
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize runInstancesInput before unmarshaling into it
|
||||
runInstancesInput := &ec2.RunInstancesInput{}
|
||||
errResp := utils.UnmarshalJsonPayload(runInstancesInput, msg.Data)
|
||||
@@ -56,15 +64,38 @@ func (d *Daemon) handleEC2RunInstances(msg *nats.Msg) {
|
||||
respondWithError(msg, awserrors.ErrorServerInternal)
|
||||
return
|
||||
}
|
||||
if _, err := d.imageService.GetAMIConfig(*runInstancesInput.ImageId); err != nil {
|
||||
amiMeta, err := d.imageService.GetAMIConfig(*runInstancesInput.ImageId)
|
||||
if err != nil {
|
||||
slog.Error("handleEC2RunInstances AMI not found", "imageId", *runInstancesInput.ImageId, "err", err)
|
||||
respondWithError(msg, awserrors.ErrorInvalidAMIIDNotFound)
|
||||
return
|
||||
}
|
||||
// Verify the caller can use this AMI: must own it or it must be a system/pre-phase4 AMI.
|
||||
// System AMIs have non-account-ID owner aliases (e.g. "self", "hive", empty).
|
||||
amiOwner := amiMeta.ImageOwnerAlias
|
||||
if amiOwner != "" && amiOwner != accountID {
|
||||
// Check if this is a system/pre-phase4 AMI (non-account-ID owner)
|
||||
isSystem := len(amiOwner) != 12
|
||||
if !isSystem {
|
||||
allDigits := true
|
||||
for _, c := range amiOwner {
|
||||
if c < '0' || c > '9' {
|
||||
allDigits = false
|
||||
break
|
||||
}
|
||||
}
|
||||
isSystem = !allDigits
|
||||
}
|
||||
if !isSystem {
|
||||
slog.Warn("handleEC2RunInstances AMI not owned by caller", "imageId", *runInstancesInput.ImageId, "amiOwner", amiOwner, "accountID", accountID)
|
||||
respondWithError(msg, awserrors.ErrorInvalidAMIIDNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate key pair exists (if specified)
|
||||
if runInstancesInput.KeyName != nil && *runInstancesInput.KeyName != "" {
|
||||
if err := d.keyService.ValidateKeyPairExists(*runInstancesInput.KeyName); err != nil {
|
||||
if err := d.keyService.ValidateKeyPairExists(accountID, *runInstancesInput.KeyName); err != nil {
|
||||
slog.Error("handleEC2RunInstances key pair not found", "keyName", *runInstancesInput.KeyName, "err", err)
|
||||
respondWithError(msg, awserrors.ErrorInvalidKeyPairNotFound)
|
||||
return
|
||||
@@ -142,7 +173,7 @@ func (d *Daemon) handleEC2RunInstances(msg *nats.Msg) {
|
||||
eniOut, eniErr := d.vpcService.CreateNetworkInterface(&ec2.CreateNetworkInterfaceInput{
|
||||
SubnetId: runInstancesInput.SubnetId,
|
||||
Description: aws.String("Primary network interface for " + instance.ID),
|
||||
})
|
||||
}, accountID)
|
||||
if eniErr != nil {
|
||||
slog.Error("handleEC2RunInstances auto-create ENI failed", "instanceId", instance.ID, "subnetId", *runInstancesInput.SubnetId, "err", eniErr)
|
||||
lastRunErr = eniErr
|
||||
@@ -205,12 +236,13 @@ func (d *Daemon) handleEC2RunInstances(msg *nats.Msg) {
|
||||
// Build reservation with all instances
|
||||
reservation := ec2.Reservation{}
|
||||
reservation.SetReservationId(utils.GenerateResourceID("r"))
|
||||
reservation.SetOwnerId("123456789012") // TODO: Use actual owner ID from config
|
||||
reservation.SetOwnerId(accountID)
|
||||
reservation.Instances = allEC2Instances
|
||||
|
||||
// Store reservation reference in all VMs
|
||||
// Store reservation reference and account ID in all VMs
|
||||
for _, instance := range instances {
|
||||
instance.Reservation = &reservation
|
||||
instance.AccountID = accountID
|
||||
}
|
||||
|
||||
// Respond to NATS immediately with reservation (instances are provisioning)
|
||||
@@ -443,10 +475,13 @@ func (d *Daemon) handleStopOrTerminateInstance(msg *nats.Msg, command qmp.Comman
|
||||
}
|
||||
|
||||
// handleEC2DescribeInstances processes incoming EC2 DescribeInstances requests
|
||||
// This handler responds with all instances running on this node
|
||||
// This handler responds with instances running on this node owned by the caller's account
|
||||
func (d *Daemon) handleEC2DescribeInstances(msg *nats.Msg) {
|
||||
slog.Debug("Received message", "subject", msg.Subject, "data", string(msg.Data))
|
||||
|
||||
// Extract account ID from NATS header for scoping
|
||||
accountID := utils.AccountIDFromMsg(msg)
|
||||
|
||||
// Initialize describeInstancesInput before unmarshaling into it
|
||||
describeInstancesInput := &ec2.DescribeInstancesInput{}
|
||||
errResp := utils.UnmarshalJsonPayload(describeInstancesInput, msg.Data)
|
||||
@@ -459,7 +494,7 @@ func (d *Daemon) handleEC2DescribeInstances(msg *nats.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("Processing DescribeInstances request from this node")
|
||||
slog.Info("Processing DescribeInstances request from this node", "accountID", accountID)
|
||||
|
||||
d.Instances.Mu.Lock()
|
||||
defer d.Instances.Mu.Unlock()
|
||||
@@ -483,6 +518,12 @@ func (d *Daemon) handleEC2DescribeInstances(msg *nats.Msg) {
|
||||
|
||||
// Iterate through all instances on this node
|
||||
for _, instance := range d.Instances.VMS {
|
||||
// Skip instances not owned by the caller's account.
|
||||
// Pre-Phase4 instances (empty AccountID) are visible to all accounts.
|
||||
if accountID != "" && instance.AccountID != "" && instance.AccountID != accountID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if filtering by instance IDs and this instance is not in the filter
|
||||
if len(instanceIDFilter) > 0 && !instanceIDFilter[instance.ID] {
|
||||
continue
|
||||
@@ -613,6 +654,8 @@ type startStoppedInstanceRequest struct {
|
||||
// handleEC2StartStoppedInstance picks up a stopped instance from shared KV,
|
||||
// re-launches it on this daemon node, and removes it from shared KV.
|
||||
func (d *Daemon) handleEC2StartStoppedInstance(msg *nats.Msg) {
|
||||
// Extract account ID from NATS header for ownership verification
|
||||
callerAccountID := utils.AccountIDFromMsg(msg)
|
||||
|
||||
var req startStoppedInstanceRequest
|
||||
if err := json.Unmarshal(msg.Data, &req); err != nil {
|
||||
@@ -652,6 +695,13 @@ func (d *Daemon) handleEC2StartStoppedInstance(msg *nats.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the caller owns this instance
|
||||
if callerAccountID != "" && instance.AccountID != "" && instance.AccountID != callerAccountID {
|
||||
slog.Warn("handleEC2StartStoppedInstance: account does not own instance", "instanceId", req.InstanceID, "callerAccount", callerAccountID, "ownerAccount", instance.AccountID)
|
||||
respondWithError(msg, awserrors.ErrorInvalidInstanceIDNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Reset node-local fields that are stale after cross-node migration
|
||||
instance.ResetNodeLocalState()
|
||||
|
||||
@@ -719,6 +769,8 @@ type terminateStoppedInstanceRequest struct {
|
||||
// handleEC2TerminateStoppedInstance picks up a stopped instance from shared KV,
|
||||
// deletes its volumes, and removes it from shared KV.
|
||||
func (d *Daemon) handleEC2TerminateStoppedInstance(msg *nats.Msg) {
|
||||
// Extract account ID from NATS header for ownership verification
|
||||
callerAccountID := utils.AccountIDFromMsg(msg)
|
||||
|
||||
var req terminateStoppedInstanceRequest
|
||||
if err := json.Unmarshal(msg.Data, &req); err != nil {
|
||||
@@ -758,6 +810,13 @@ func (d *Daemon) handleEC2TerminateStoppedInstance(msg *nats.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the caller owns this instance
|
||||
if callerAccountID != "" && instance.AccountID != "" && instance.AccountID != callerAccountID {
|
||||
slog.Warn("handleEC2TerminateStoppedInstance: account does not own instance", "instanceId", req.InstanceID, "callerAccount", callerAccountID, "ownerAccount", instance.AccountID)
|
||||
respondWithError(msg, awserrors.ErrorInvalidInstanceIDNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete volumes — no QEMU shutdown or unmount needed (already done during stop)
|
||||
instance.EBSRequests.Mu.Lock()
|
||||
for _, ebsRequest := range instance.EBSRequests.Requests {
|
||||
@@ -786,7 +845,7 @@ func (d *Daemon) handleEC2TerminateStoppedInstance(msg *nats.Msg) {
|
||||
slog.Info("handleEC2TerminateStoppedInstance: deleting volume with DeleteOnTermination=true", "name", ebsRequest.Name)
|
||||
_, err := d.volumeService.DeleteVolume(&ec2.DeleteVolumeInput{
|
||||
VolumeId: &ebsRequest.Name,
|
||||
})
|
||||
}, instance.AccountID)
|
||||
if err != nil {
|
||||
slog.Error("handleEC2TerminateStoppedInstance: failed to delete volume", "name", ebsRequest.Name, "err", err)
|
||||
}
|
||||
@@ -815,6 +874,9 @@ func (d *Daemon) handleEC2DescribeStoppedInstances(msg *nats.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract account ID from NATS header for scoping
|
||||
accountID := utils.AccountIDFromMsg(msg)
|
||||
|
||||
// Parse optional filters from the request
|
||||
describeInput := &ec2.DescribeInstancesInput{}
|
||||
if len(msg.Data) > 0 {
|
||||
@@ -847,6 +909,12 @@ func (d *Daemon) handleEC2DescribeStoppedInstances(msg *nats.Msg) {
|
||||
reservationMap := make(map[string]*ec2.Reservation)
|
||||
|
||||
for _, instance := range instances {
|
||||
// Skip instances not owned by the caller's account.
|
||||
// Pre-Phase4 instances (empty AccountID) are visible to all accounts.
|
||||
if accountID != "" && instance.AccountID != "" && instance.AccountID != accountID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply instance ID filter
|
||||
if len(instanceIDFilter) > 0 && !instanceIDFilter[instance.ID] {
|
||||
continue
|
||||
@@ -909,6 +977,8 @@ func (d *Daemon) handleEC2DescribeStoppedInstances(msg *nats.Msg) {
|
||||
// handleEC2ModifyInstanceAttribute modifies attributes of a stopped instance in shared KV.
|
||||
// All supported attributes (InstanceType, UserData) require the instance to be stopped.
|
||||
func (d *Daemon) handleEC2ModifyInstanceAttribute(msg *nats.Msg) {
|
||||
// Extract account ID from NATS header for ownership verification
|
||||
callerAccountID := utils.AccountIDFromMsg(msg)
|
||||
|
||||
var input ec2.ModifyInstanceAttributeInput
|
||||
if err := json.Unmarshal(msg.Data, &input); err != nil {
|
||||
@@ -949,6 +1019,13 @@ func (d *Daemon) handleEC2ModifyInstanceAttribute(msg *nats.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the caller owns this instance
|
||||
if callerAccountID != "" && instance.AccountID != "" && instance.AccountID != callerAccountID {
|
||||
slog.Warn("handleEC2ModifyInstanceAttribute: account does not own instance", "instanceId", instanceID, "callerAccount", callerAccountID, "ownerAccount", instance.AccountID)
|
||||
respondWithError(msg, awserrors.ErrorInvalidInstanceIDNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Apply the requested attribute change
|
||||
if input.InstanceType != nil && input.InstanceType.Value != nil {
|
||||
newType := *input.InstanceType.Value
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
import "github.com/nats-io/nats.go"
|
||||
|
||||
func (d *Daemon) handleEC2CreateTags(msg *nats.Msg) {
|
||||
handleNATSRequest(msg, d.tagsService.CreateTags)
|
||||
|
||||
@@ -34,6 +34,17 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testAccountID is the default account ID used in daemon tests.
|
||||
const testAccountID = "123456789012"
|
||||
|
||||
// natsRequest sends a NATS request with the X-Account-ID header set.
|
||||
func natsRequest(nc *nats.Conn, subject string, data []byte, timeout time.Duration) (*nats.Msg, error) {
|
||||
msg := nats.NewMsg(subject)
|
||||
msg.Data = data
|
||||
msg.Header.Set("X-Account-ID", testAccountID)
|
||||
return nc.RequestMsg(msg, timeout)
|
||||
}
|
||||
|
||||
// createFullTestDaemonWithStore creates a test daemon with ALL services initialized
|
||||
// and returns the shared memory store for seeding test data.
|
||||
func createFullTestDaemonWithStore(t *testing.T, natsURL string) (*Daemon, *objectstore.MemoryObjectStore) {
|
||||
@@ -42,8 +53,8 @@ func createFullTestDaemonWithStore(t *testing.T, natsURL string) (*Daemon, *obje
|
||||
memStore := objectstore.NewMemoryObjectStore()
|
||||
cfg := daemon.config
|
||||
|
||||
daemon.keyService = handlers_ec2_key.NewKeyServiceImplWithStore(memStore, cfg.Predastore.Bucket, "123456789")
|
||||
daemon.imageService = handlers_ec2_image.NewImageServiceImplWithStore(memStore, cfg.Predastore.Bucket, "123456789")
|
||||
daemon.keyService = handlers_ec2_key.NewKeyServiceImplWithStore(memStore, cfg.Predastore.Bucket)
|
||||
daemon.imageService = handlers_ec2_image.NewImageServiceImplWithStore(memStore, cfg.Predastore.Bucket)
|
||||
daemon.volumeService = handlers_ec2_volume.NewVolumeServiceImplWithStore(cfg, memStore, daemon.natsConn)
|
||||
daemon.snapshotService = handlers_ec2_snapshot.NewSnapshotServiceImplWithStore(cfg, memStore, daemon.natsConn)
|
||||
daemon.tagsService = handlers_ec2_tags.NewTagsServiceImplWithStore(cfg, memStore)
|
||||
@@ -117,7 +128,7 @@ func TestHandleNATSRequest_ValidRequest(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer nc.Close()
|
||||
|
||||
serviceFn := func(in *testInput) (*testOutput, error) {
|
||||
serviceFn := func(in *testInput, accountID string) (*testOutput, error) {
|
||||
return &testOutput{Greeting: "hello " + in.Name}, nil
|
||||
}
|
||||
|
||||
@@ -144,7 +155,7 @@ func TestHandleNATSRequest_MalformedJSON(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer nc.Close()
|
||||
|
||||
serviceFn := func(in *testInput) (*testOutput, error) {
|
||||
serviceFn := func(in *testInput, accountID string) (*testOutput, error) {
|
||||
return &testOutput{Greeting: "hello"}, nil
|
||||
}
|
||||
|
||||
@@ -170,7 +181,7 @@ func TestHandleNATSRequest_ServiceError(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer nc.Close()
|
||||
|
||||
serviceFn := func(in *testInput) (*testOutput, error) {
|
||||
serviceFn := func(in *testInput, accountID string) (*testOutput, error) {
|
||||
return nil, fmt.Errorf("something went wrong")
|
||||
}
|
||||
|
||||
@@ -205,7 +216,7 @@ func TestHandleEC2CreateKeyPair_RoundTrip(t *testing.T) {
|
||||
KeyName: aws.String("test-key-001"),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.CreateKeyPair", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.CreateKeyPair", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, reply)
|
||||
|
||||
@@ -293,7 +304,7 @@ func TestHandleEC2DescribeKeyPairs_RoundTrip(t *testing.T) {
|
||||
|
||||
input := &ec2.DescribeKeyPairsInput{}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.DescribeKeyPairs", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.DescribeKeyPairs", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, reply)
|
||||
|
||||
@@ -378,7 +389,7 @@ func TestHandleEC2RunInstances_InvalidAMI(t *testing.T) {
|
||||
MaxCount: aws.Int64(1),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.RunInstances", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.RunInstances", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should return InvalidAMIID.NotFound, not ServerInternal
|
||||
@@ -405,7 +416,7 @@ func TestHandleEC2RunInstances_InvalidKeyPair(t *testing.T) {
|
||||
MaxCount: aws.Int64(1),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.RunInstances", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.RunInstances", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should return InvalidKeyPair.NotFound, not proceed to launch
|
||||
@@ -424,7 +435,7 @@ func TestHandleEC2RunInstances_ValidKeyPairPassesValidation(t *testing.T) {
|
||||
bucket := daemon.config.Predastore.Bucket
|
||||
_, err := memStore.PutObject(&awss3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String("keys/123456789/my-key"),
|
||||
Key: aws.String("keys/" + testAccountID + "/my-key"),
|
||||
Body: strings.NewReader("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITest"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -432,7 +443,7 @@ func TestHandleEC2RunInstances_ValidKeyPairPassesValidation(t *testing.T) {
|
||||
metadataJSON := `{"KeyPairId":"key-abc123","KeyName":"my-key","KeyFingerprint":"SHA256:test"}`
|
||||
_, err = memStore.PutObject(&awss3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String("keys/123456789/key-abc123.json"),
|
||||
Key: aws.String("keys/" + testAccountID + "/key-abc123.json"),
|
||||
Body: strings.NewReader(metadataJSON),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -449,7 +460,7 @@ func TestHandleEC2RunInstances_ValidKeyPairPassesValidation(t *testing.T) {
|
||||
MaxCount: aws.Int64(1),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.RunInstances", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.RunInstances", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should NOT contain InvalidKeyPair.NotFound — key pair validation should pass
|
||||
@@ -474,7 +485,7 @@ func TestHandleEC2RunInstances_EmptyKeyNameSkipsValidation(t *testing.T) {
|
||||
MaxCount: aws.Int64(1),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.RunInstances", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.RunInstances", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should NOT contain InvalidKeyPair.NotFound
|
||||
@@ -509,7 +520,7 @@ func TestHandleEC2RunInstances_ServiceErrorPropagated(t *testing.T) {
|
||||
MaxCount: aws.Int64(1),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.RunInstances", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.RunInstances", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should propagate the specific AWS error from the service layer,
|
||||
@@ -735,7 +746,7 @@ func TestHandleEC2GetEbsEncryptionByDefault(t *testing.T) {
|
||||
|
||||
input := &ec2.GetEbsEncryptionByDefaultInput{}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.GetEbsEncryptionByDefault", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.GetEbsEncryptionByDefault", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.GetEbsEncryptionByDefaultOutput
|
||||
@@ -755,7 +766,7 @@ func TestHandleEC2GetSerialConsoleAccessStatus(t *testing.T) {
|
||||
|
||||
input := &ec2.GetSerialConsoleAccessStatusInput{}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.GetSerialConsoleAccessStatus", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.GetSerialConsoleAccessStatus", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.GetSerialConsoleAccessStatusOutput
|
||||
@@ -775,7 +786,7 @@ func TestHandleEC2EnableEbsEncryptionByDefault(t *testing.T) {
|
||||
|
||||
input := &ec2.EnableEbsEncryptionByDefaultInput{}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.EnableEbsEncryptionByDefault", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.EnableEbsEncryptionByDefault", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.EnableEbsEncryptionByDefaultOutput
|
||||
@@ -796,7 +807,7 @@ func TestHandleEC2DisableEbsEncryptionByDefault(t *testing.T) {
|
||||
|
||||
input := &ec2.DisableEbsEncryptionByDefaultInput{}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.DisableEbsEncryptionByDefault", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.DisableEbsEncryptionByDefault", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.DisableEbsEncryptionByDefaultOutput
|
||||
@@ -817,7 +828,7 @@ func TestHandleEC2EnableSerialConsoleAccess(t *testing.T) {
|
||||
|
||||
input := &ec2.EnableSerialConsoleAccessInput{}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.EnableSerialConsoleAccess", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.EnableSerialConsoleAccess", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.EnableSerialConsoleAccessOutput
|
||||
@@ -838,7 +849,7 @@ func TestHandleEC2DisableSerialConsoleAccess(t *testing.T) {
|
||||
|
||||
input := &ec2.DisableSerialConsoleAccessInput{}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.DisableSerialConsoleAccess", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.DisableSerialConsoleAccess", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.DisableSerialConsoleAccessOutput
|
||||
@@ -1853,7 +1864,7 @@ func TestDelegateHandlers_RoundTrip(t *testing.T) {
|
||||
reqData, err := json.Marshal(tt.input)
|
||||
require.NoError(t, err)
|
||||
|
||||
reply, err := daemon.natsConn.Request(tt.topic, reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, tt.topic, reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, reply)
|
||||
|
||||
@@ -2167,7 +2178,7 @@ func TestDelegateHandlers_VPC(t *testing.T) {
|
||||
reqData, err := json.Marshal(tt.input)
|
||||
require.NoError(t, err)
|
||||
|
||||
reply, err := daemon.natsConn.Request(tt.topic, reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, tt.topic, reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, reply)
|
||||
|
||||
@@ -2234,7 +2245,7 @@ func TestDelegateHandlers_IGW(t *testing.T) {
|
||||
reqData, err := json.Marshal(tt.input)
|
||||
require.NoError(t, err)
|
||||
|
||||
reply, err := daemon.natsConn.Request(tt.topic, reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, tt.topic, reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, reply)
|
||||
|
||||
@@ -2254,7 +2265,7 @@ func TestHandleEC2CreateVpc_SuccessPath(t *testing.T) {
|
||||
|
||||
input := &ec2.CreateVpcInput{CidrBlock: aws.String("10.100.0.0/16")}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.CreateVpc", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.CreateVpc", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.CreateVpcOutput
|
||||
@@ -2280,7 +2291,7 @@ func TestHandleEC2CreateAndDescribeVpc_RoundTrip(t *testing.T) {
|
||||
// Create a VPC
|
||||
createInput := &ec2.CreateVpcInput{CidrBlock: aws.String("10.200.0.0/16")}
|
||||
reqData, _ := json.Marshal(createInput)
|
||||
reply, err := daemon.natsConn.Request("ec2.CreateVpc", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.CreateVpc", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var createOutput ec2.CreateVpcOutput
|
||||
@@ -2290,7 +2301,7 @@ func TestHandleEC2CreateAndDescribeVpc_RoundTrip(t *testing.T) {
|
||||
// Describe VPCs and verify the created VPC appears
|
||||
describeInput := &ec2.DescribeVpcsInput{}
|
||||
reqData, _ = json.Marshal(describeInput)
|
||||
reply, err = daemon.natsConn.Request("ec2.DescribeVpcs", reqData, 5*time.Second)
|
||||
reply, err = natsRequest(daemon.natsConn, "ec2.DescribeVpcs", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var describeOutput ec2.DescribeVpcsOutput
|
||||
@@ -2315,7 +2326,7 @@ func TestHandleEC2CreateInternetGateway_SuccessPath(t *testing.T) {
|
||||
|
||||
input := &ec2.CreateInternetGatewayInput{}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.CreateInternetGateway", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.CreateInternetGateway", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output ec2.CreateInternetGatewayOutput
|
||||
@@ -2340,7 +2351,7 @@ func TestHandleEC2CreateSubnet_SuccessPath(t *testing.T) {
|
||||
// Create a VPC first
|
||||
vpcInput := &ec2.CreateVpcInput{CidrBlock: aws.String("10.50.0.0/16")}
|
||||
reqData, _ := json.Marshal(vpcInput)
|
||||
reply, err := daemon.natsConn.Request("ec2.CreateVpc", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.CreateVpc", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var vpcOutput ec2.CreateVpcOutput
|
||||
@@ -2353,7 +2364,7 @@ func TestHandleEC2CreateSubnet_SuccessPath(t *testing.T) {
|
||||
CidrBlock: aws.String("10.50.1.0/24"),
|
||||
}
|
||||
reqData, _ = json.Marshal(subnetInput)
|
||||
reply, err = daemon.natsConn.Request("ec2.CreateSubnet", reqData, 5*time.Second)
|
||||
reply, err = natsRequest(daemon.natsConn, "ec2.CreateSubnet", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var subnetOutput ec2.CreateSubnetOutput
|
||||
@@ -2426,7 +2437,7 @@ func TestDelegateHandlers_EIGW(t *testing.T) {
|
||||
reqData, err := json.Marshal(tt.input)
|
||||
require.NoError(t, err)
|
||||
|
||||
reply, err := daemon.natsConn.Request(tt.topic, reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, tt.topic, reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, reply)
|
||||
|
||||
@@ -3106,7 +3117,7 @@ func TestHandleEC2RunInstances_InsufficientCapacity(t *testing.T) {
|
||||
MaxCount: aws.Int64(9999),
|
||||
}
|
||||
reqData, _ := json.Marshal(input)
|
||||
reply, err := daemon.natsConn.Request("ec2.RunInstances", reqData, 5*time.Second)
|
||||
reply, err := natsRequest(daemon.natsConn, "ec2.RunInstances", reqData, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var errResp map[string]any
|
||||
|
||||
@@ -52,6 +52,14 @@ func (d *Daemon) handleAttachVolume(msg *nats.Msg, command qmp.Command, instance
|
||||
return
|
||||
}
|
||||
|
||||
// Account scoping: verify the caller owns this volume
|
||||
callerAccountID := utils.AccountIDFromMsg(msg)
|
||||
if callerAccountID != "" && volCfg.VolumeMetadata.TenantID != "" && volCfg.VolumeMetadata.TenantID != callerAccountID {
|
||||
slog.Warn("AttachVolume: account does not own volume", "volumeId", volumeID, "callerAccount", callerAccountID, "ownerAccount", volCfg.VolumeMetadata.TenantID)
|
||||
respondWithError(msg, awserrors.ErrorInvalidVolumeNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if volCfg.VolumeMetadata.State != "available" {
|
||||
slog.Error("AttachVolume: volume not available", "volumeId", volumeID, "state", volCfg.VolumeMetadata.State)
|
||||
respondWithError(msg, awserrors.ErrorVolumeInUse)
|
||||
@@ -446,6 +454,8 @@ func (d *Daemon) handleEC2ModifyVolume(msg *nats.Msg) {
|
||||
slog.Debug("Received message", "subject", msg.Subject)
|
||||
slog.Debug("Message data", "data", string(msg.Data))
|
||||
|
||||
accountID := utils.AccountIDFromMsg(msg)
|
||||
|
||||
modifyVolumeInput := &ec2.ModifyVolumeInput{}
|
||||
errResp := utils.UnmarshalJsonPayload(modifyVolumeInput, msg.Data)
|
||||
|
||||
@@ -457,9 +467,9 @@ func (d *Daemon) handleEC2ModifyVolume(msg *nats.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("Processing ModifyVolume request", "volumeId", modifyVolumeInput.VolumeId)
|
||||
slog.Info("Processing ModifyVolume request", "volumeId", modifyVolumeInput.VolumeId, "accountID", accountID)
|
||||
|
||||
output, err := d.volumeService.ModifyVolume(modifyVolumeInput)
|
||||
output, err := d.volumeService.ModifyVolume(modifyVolumeInput, accountID)
|
||||
|
||||
if err != nil {
|
||||
slog.Error("handleEC2ModifyVolume service.ModifyVolume failed", "err", err)
|
||||
|
||||
@@ -1316,7 +1316,7 @@ func TestRunInstances_CountValidation(t *testing.T) {
|
||||
}
|
||||
inputJSON, _ := json.Marshal(input)
|
||||
|
||||
resp, err := daemon.natsConn.Request(topic, inputJSON, 5*time.Second)
|
||||
resp, err := natsRequest(daemon.natsConn, topic, inputJSON, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should return validation error
|
||||
@@ -1336,7 +1336,7 @@ func TestRunInstances_CountValidation(t *testing.T) {
|
||||
}
|
||||
inputJSON, _ := json.Marshal(input)
|
||||
|
||||
resp, err := daemon.natsConn.Request(topic, inputJSON, 5*time.Second)
|
||||
resp, err := natsRequest(daemon.natsConn, topic, inputJSON, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var errResp map[string]any
|
||||
@@ -1355,7 +1355,7 @@ func TestRunInstances_CountValidation(t *testing.T) {
|
||||
}
|
||||
inputJSON, _ := json.Marshal(input)
|
||||
|
||||
resp, err := daemon.natsConn.Request(topic, inputJSON, 5*time.Second)
|
||||
resp, err := natsRequest(daemon.natsConn, topic, inputJSON, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
var errResp map[string]any
|
||||
|
||||
+120
-111
@@ -4,6 +4,7 @@ package gateway
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
@@ -26,17 +27,18 @@ import (
|
||||
|
||||
// EC2Handler processes parsed query args and returns XML response bytes.
|
||||
// The action parameter is the EC2 API action name, passed from the map key.
|
||||
type EC2Handler func(action string, q map[string]string, gw *GatewayConfig) ([]byte, error)
|
||||
// accountID is the caller's AWS account ID extracted from SigV4 auth context.
|
||||
type EC2Handler func(action string, q map[string]string, gw *GatewayConfig, accountID string) ([]byte, error)
|
||||
|
||||
// ec2Handler creates a type-safe EC2Handler that allocates the typed input struct,
|
||||
// parses query params into it, calls the handler, and marshals the output to XML.
|
||||
func ec2Handler[In any](handler func(*In, *GatewayConfig) (any, error)) EC2Handler {
|
||||
return func(action string, q map[string]string, gw *GatewayConfig) ([]byte, error) {
|
||||
func ec2Handler[In any](handler func(*In, *GatewayConfig, string) (any, error)) EC2Handler {
|
||||
return func(action string, q map[string]string, gw *GatewayConfig, accountID string) ([]byte, error) {
|
||||
input := new(In)
|
||||
if err := awsec2query.QueryParamsToStruct(q, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output, err := handler(input, gw)
|
||||
output, err := handler(input, gw, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -50,173 +52,173 @@ func ec2Handler[In any](handler func(*In, *GatewayConfig) (any, error)) EC2Handl
|
||||
}
|
||||
|
||||
var ec2Actions = map[string]EC2Handler{
|
||||
"DescribeInstances": ec2Handler(func(input *ec2.DescribeInstancesInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_instance.DescribeInstances(input, gw.NATSConn, gw.DiscoverActiveNodes())
|
||||
"DescribeInstances": ec2Handler(func(input *ec2.DescribeInstancesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_instance.DescribeInstances(input, gw.NATSConn, gw.DiscoverActiveNodes(), accountID)
|
||||
}),
|
||||
"RunInstances": ec2Handler(func(input *ec2.RunInstancesInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_instance.RunInstances(input, gw.NATSConn)
|
||||
"RunInstances": ec2Handler(func(input *ec2.RunInstancesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_instance.RunInstances(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"StartInstances": ec2Handler(func(input *ec2.StartInstancesInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_instance.StartInstances(input, gw.NATSConn)
|
||||
"StartInstances": ec2Handler(func(input *ec2.StartInstancesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_instance.StartInstances(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"StopInstances": ec2Handler(func(input *ec2.StopInstancesInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_instance.StopInstances(input, gw.NATSConn)
|
||||
"StopInstances": ec2Handler(func(input *ec2.StopInstancesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_instance.StopInstances(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"TerminateInstances": ec2Handler(func(input *ec2.TerminateInstancesInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_instance.TerminateInstances(input, gw.NATSConn)
|
||||
"TerminateInstances": ec2Handler(func(input *ec2.TerminateInstancesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_instance.TerminateInstances(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeInstanceTypes": ec2Handler(func(input *ec2.DescribeInstanceTypesInput, gw *GatewayConfig) (any, error) {
|
||||
"DescribeInstanceTypes": ec2Handler(func(input *ec2.DescribeInstanceTypesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_instance.DescribeInstanceTypes(input, gw.NATSConn, gw.ExpectedNodes)
|
||||
}),
|
||||
"GetConsoleOutput": ec2Handler(func(input *ec2.GetConsoleOutputInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_instance.GetConsoleOutput(input, gw.NATSConn)
|
||||
"GetConsoleOutput": ec2Handler(func(input *ec2.GetConsoleOutputInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_instance.GetConsoleOutput(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"ModifyInstanceAttribute": ec2Handler(func(input *ec2.ModifyInstanceAttributeInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_instance.ModifyInstanceAttribute(input, gw.NATSConn)
|
||||
"ModifyInstanceAttribute": ec2Handler(func(input *ec2.ModifyInstanceAttributeInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_instance.ModifyInstanceAttribute(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"CreateKeyPair": ec2Handler(func(input *ec2.CreateKeyPairInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_key.CreateKeyPair(input, gw.NATSConn)
|
||||
"CreateKeyPair": ec2Handler(func(input *ec2.CreateKeyPairInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_key.CreateKeyPair(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DeleteKeyPair": ec2Handler(func(input *ec2.DeleteKeyPairInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_key.DeleteKeyPair(input, gw.NATSConn)
|
||||
"DeleteKeyPair": ec2Handler(func(input *ec2.DeleteKeyPairInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_key.DeleteKeyPair(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeKeyPairs": ec2Handler(func(input *ec2.DescribeKeyPairsInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_key.DescribeKeyPairs(input, gw.NATSConn)
|
||||
"DescribeKeyPairs": ec2Handler(func(input *ec2.DescribeKeyPairsInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_key.DescribeKeyPairs(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"ImportKeyPair": func(action string, q map[string]string, gw *GatewayConfig) ([]byte, error) {
|
||||
"ImportKeyPair": func(action string, q map[string]string, gw *GatewayConfig, accountID string) ([]byte, error) {
|
||||
// Workaround: parser leaves Base64 padding URL-encoded
|
||||
if strings.HasSuffix(q["PublicKeyMaterial"], "%3D%3D") {
|
||||
q["PublicKeyMaterial"] = strings.Replace(q["PublicKeyMaterial"], "%3D%3D", "==", 1)
|
||||
}
|
||||
return ec2Handler(func(input *ec2.ImportKeyPairInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_key.ImportKeyPair(input, gw.NATSConn)
|
||||
})(action, q, gw)
|
||||
return ec2Handler(func(input *ec2.ImportKeyPairInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_key.ImportKeyPair(input, gw.NATSConn, accountID)
|
||||
})(action, q, gw, accountID)
|
||||
},
|
||||
"DescribeImages": ec2Handler(func(input *ec2.DescribeImagesInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_image.DescribeImages(input, gw.NATSConn)
|
||||
"DescribeImages": ec2Handler(func(input *ec2.DescribeImagesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_image.DescribeImages(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"CreateImage": ec2Handler(func(input *ec2.CreateImageInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_image.CreateImage(input, gw.NATSConn)
|
||||
"CreateImage": ec2Handler(func(input *ec2.CreateImageInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_image.CreateImage(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeRegions": ec2Handler(func(input *ec2.DescribeRegionsInput, gw *GatewayConfig) (any, error) {
|
||||
"DescribeRegions": ec2Handler(func(input *ec2.DescribeRegionsInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_zone.DescribeRegions(input, gw.Region)
|
||||
}),
|
||||
"DescribeAvailabilityZones": ec2Handler(func(input *ec2.DescribeAvailabilityZonesInput, gw *GatewayConfig) (any, error) {
|
||||
"DescribeAvailabilityZones": ec2Handler(func(input *ec2.DescribeAvailabilityZonesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_zone.DescribeAvailabilityZones(input, gw.Region, gw.AZ)
|
||||
}),
|
||||
"DescribeVolumes": ec2Handler(func(input *ec2.DescribeVolumesInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_volume.DescribeVolumes(input, gw.NATSConn)
|
||||
"DescribeVolumes": ec2Handler(func(input *ec2.DescribeVolumesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_volume.DescribeVolumes(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"ModifyVolume": ec2Handler(func(input *ec2.ModifyVolumeInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_volume.ModifyVolume(input, gw.NATSConn)
|
||||
"ModifyVolume": ec2Handler(func(input *ec2.ModifyVolumeInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_volume.ModifyVolume(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"CreateVolume": ec2Handler(func(input *ec2.CreateVolumeInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_volume.CreateVolume(input, gw.NATSConn)
|
||||
"CreateVolume": ec2Handler(func(input *ec2.CreateVolumeInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_volume.CreateVolume(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DeleteVolume": ec2Handler(func(input *ec2.DeleteVolumeInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_volume.DeleteVolume(input, gw.NATSConn)
|
||||
"DeleteVolume": ec2Handler(func(input *ec2.DeleteVolumeInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_volume.DeleteVolume(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"AttachVolume": ec2Handler(func(input *ec2.AttachVolumeInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_volume.AttachVolume(input, gw.NATSConn)
|
||||
"AttachVolume": ec2Handler(func(input *ec2.AttachVolumeInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_volume.AttachVolume(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeVolumeStatus": ec2Handler(func(input *ec2.DescribeVolumeStatusInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_volume.DescribeVolumeStatus(input, gw.NATSConn)
|
||||
"DescribeVolumeStatus": ec2Handler(func(input *ec2.DescribeVolumeStatusInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_volume.DescribeVolumeStatus(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DetachVolume": ec2Handler(func(input *ec2.DetachVolumeInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_volume.DetachVolume(input, gw.NATSConn)
|
||||
"DetachVolume": ec2Handler(func(input *ec2.DetachVolumeInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_volume.DetachVolume(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeAccountAttributes": ec2Handler(func(input *ec2.DescribeAccountAttributesInput, gw *GatewayConfig) (any, error) {
|
||||
"DescribeAccountAttributes": ec2Handler(func(input *ec2.DescribeAccountAttributesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_account.DescribeAccountAttributes(input)
|
||||
}),
|
||||
"EnableEbsEncryptionByDefault": ec2Handler(func(input *ec2.EnableEbsEncryptionByDefaultInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_account.EnableEbsEncryptionByDefault(input, gw.NATSConn)
|
||||
"EnableEbsEncryptionByDefault": ec2Handler(func(input *ec2.EnableEbsEncryptionByDefaultInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_account.EnableEbsEncryptionByDefault(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DisableEbsEncryptionByDefault": ec2Handler(func(input *ec2.DisableEbsEncryptionByDefaultInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_account.DisableEbsEncryptionByDefault(input, gw.NATSConn)
|
||||
"DisableEbsEncryptionByDefault": ec2Handler(func(input *ec2.DisableEbsEncryptionByDefaultInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_account.DisableEbsEncryptionByDefault(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"GetEbsEncryptionByDefault": ec2Handler(func(input *ec2.GetEbsEncryptionByDefaultInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_account.GetEbsEncryptionByDefault(input, gw.NATSConn)
|
||||
"GetEbsEncryptionByDefault": ec2Handler(func(input *ec2.GetEbsEncryptionByDefaultInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_account.GetEbsEncryptionByDefault(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"GetSerialConsoleAccessStatus": ec2Handler(func(input *ec2.GetSerialConsoleAccessStatusInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_account.GetSerialConsoleAccessStatus(input, gw.NATSConn)
|
||||
"GetSerialConsoleAccessStatus": ec2Handler(func(input *ec2.GetSerialConsoleAccessStatusInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_account.GetSerialConsoleAccessStatus(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"EnableSerialConsoleAccess": ec2Handler(func(input *ec2.EnableSerialConsoleAccessInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_account.EnableSerialConsoleAccess(input, gw.NATSConn)
|
||||
"EnableSerialConsoleAccess": ec2Handler(func(input *ec2.EnableSerialConsoleAccessInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_account.EnableSerialConsoleAccess(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DisableSerialConsoleAccess": ec2Handler(func(input *ec2.DisableSerialConsoleAccessInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_account.DisableSerialConsoleAccess(input, gw.NATSConn)
|
||||
"DisableSerialConsoleAccess": ec2Handler(func(input *ec2.DisableSerialConsoleAccessInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_account.DisableSerialConsoleAccess(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"CreateTags": ec2Handler(func(input *ec2.CreateTagsInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_tags.CreateTags(input, gw.NATSConn)
|
||||
"CreateTags": ec2Handler(func(input *ec2.CreateTagsInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_tags.CreateTags(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DeleteTags": ec2Handler(func(input *ec2.DeleteTagsInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_tags.DeleteTags(input, gw.NATSConn)
|
||||
"DeleteTags": ec2Handler(func(input *ec2.DeleteTagsInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_tags.DeleteTags(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeTags": ec2Handler(func(input *ec2.DescribeTagsInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_tags.DescribeTags(input, gw.NATSConn)
|
||||
"DescribeTags": ec2Handler(func(input *ec2.DescribeTagsInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_tags.DescribeTags(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"CreateSnapshot": ec2Handler(func(input *ec2.CreateSnapshotInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_snapshot.CreateSnapshot(input, gw.NATSConn)
|
||||
"CreateSnapshot": ec2Handler(func(input *ec2.CreateSnapshotInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_snapshot.CreateSnapshot(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DeleteSnapshot": ec2Handler(func(input *ec2.DeleteSnapshotInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_snapshot.DeleteSnapshot(input, gw.NATSConn)
|
||||
"DeleteSnapshot": ec2Handler(func(input *ec2.DeleteSnapshotInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_snapshot.DeleteSnapshot(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeSnapshots": ec2Handler(func(input *ec2.DescribeSnapshotsInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_snapshot.DescribeSnapshots(input, gw.NATSConn)
|
||||
"DescribeSnapshots": ec2Handler(func(input *ec2.DescribeSnapshotsInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_snapshot.DescribeSnapshots(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"CopySnapshot": ec2Handler(func(input *ec2.CopySnapshotInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_snapshot.CopySnapshot(input, gw.NATSConn)
|
||||
"CopySnapshot": ec2Handler(func(input *ec2.CopySnapshotInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_snapshot.CopySnapshot(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"CreateInternetGateway": ec2Handler(func(input *ec2.CreateInternetGatewayInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_igw.CreateInternetGateway(input, gw.NATSConn)
|
||||
"CreateInternetGateway": ec2Handler(func(input *ec2.CreateInternetGatewayInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_igw.CreateInternetGateway(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DeleteInternetGateway": ec2Handler(func(input *ec2.DeleteInternetGatewayInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_igw.DeleteInternetGateway(input, gw.NATSConn)
|
||||
"DeleteInternetGateway": ec2Handler(func(input *ec2.DeleteInternetGatewayInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_igw.DeleteInternetGateway(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeInternetGateways": ec2Handler(func(input *ec2.DescribeInternetGatewaysInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_igw.DescribeInternetGateways(input, gw.NATSConn)
|
||||
"DescribeInternetGateways": ec2Handler(func(input *ec2.DescribeInternetGatewaysInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_igw.DescribeInternetGateways(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"AttachInternetGateway": ec2Handler(func(input *ec2.AttachInternetGatewayInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_igw.AttachInternetGateway(input, gw.NATSConn)
|
||||
"AttachInternetGateway": ec2Handler(func(input *ec2.AttachInternetGatewayInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_igw.AttachInternetGateway(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DetachInternetGateway": ec2Handler(func(input *ec2.DetachInternetGatewayInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_igw.DetachInternetGateway(input, gw.NATSConn)
|
||||
"DetachInternetGateway": ec2Handler(func(input *ec2.DetachInternetGatewayInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_igw.DetachInternetGateway(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"CreateEgressOnlyInternetGateway": ec2Handler(func(input *ec2.CreateEgressOnlyInternetGatewayInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_eigw.CreateEgressOnlyInternetGateway(input, gw.NATSConn)
|
||||
"CreateEgressOnlyInternetGateway": ec2Handler(func(input *ec2.CreateEgressOnlyInternetGatewayInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_eigw.CreateEgressOnlyInternetGateway(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DeleteEgressOnlyInternetGateway": ec2Handler(func(input *ec2.DeleteEgressOnlyInternetGatewayInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_eigw.DeleteEgressOnlyInternetGateway(input, gw.NATSConn)
|
||||
"DeleteEgressOnlyInternetGateway": ec2Handler(func(input *ec2.DeleteEgressOnlyInternetGatewayInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_eigw.DeleteEgressOnlyInternetGateway(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeEgressOnlyInternetGateways": ec2Handler(func(input *ec2.DescribeEgressOnlyInternetGatewaysInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_eigw.DescribeEgressOnlyInternetGateways(input, gw.NATSConn)
|
||||
"DescribeEgressOnlyInternetGateways": ec2Handler(func(input *ec2.DescribeEgressOnlyInternetGatewaysInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_eigw.DescribeEgressOnlyInternetGateways(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"CreateVpc": ec2Handler(func(input *ec2.CreateVpcInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_vpc.CreateVpc(input, gw.NATSConn)
|
||||
"CreateVpc": ec2Handler(func(input *ec2.CreateVpcInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_vpc.CreateVpc(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DeleteVpc": ec2Handler(func(input *ec2.DeleteVpcInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_vpc.DeleteVpc(input, gw.NATSConn)
|
||||
"DeleteVpc": ec2Handler(func(input *ec2.DeleteVpcInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_vpc.DeleteVpc(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeVpcs": ec2Handler(func(input *ec2.DescribeVpcsInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_vpc.DescribeVpcs(input, gw.NATSConn)
|
||||
"DescribeVpcs": ec2Handler(func(input *ec2.DescribeVpcsInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_vpc.DescribeVpcs(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"CreateSubnet": ec2Handler(func(input *ec2.CreateSubnetInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_vpc.CreateSubnet(input, gw.NATSConn)
|
||||
"CreateSubnet": ec2Handler(func(input *ec2.CreateSubnetInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_vpc.CreateSubnet(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DeleteSubnet": ec2Handler(func(input *ec2.DeleteSubnetInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_vpc.DeleteSubnet(input, gw.NATSConn)
|
||||
"DeleteSubnet": ec2Handler(func(input *ec2.DeleteSubnetInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_vpc.DeleteSubnet(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeSubnets": ec2Handler(func(input *ec2.DescribeSubnetsInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_vpc.DescribeSubnets(input, gw.NATSConn)
|
||||
"DescribeSubnets": ec2Handler(func(input *ec2.DescribeSubnetsInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_vpc.DescribeSubnets(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"CreateNetworkInterface": ec2Handler(func(input *ec2.CreateNetworkInterfaceInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_vpc.CreateNetworkInterface(input, gw.NATSConn)
|
||||
"CreateNetworkInterface": ec2Handler(func(input *ec2.CreateNetworkInterfaceInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_vpc.CreateNetworkInterface(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DeleteNetworkInterface": ec2Handler(func(input *ec2.DeleteNetworkInterfaceInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_vpc.DeleteNetworkInterface(input, gw.NATSConn)
|
||||
"DeleteNetworkInterface": ec2Handler(func(input *ec2.DeleteNetworkInterfaceInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_vpc.DeleteNetworkInterface(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
"DescribeNetworkInterfaces": ec2Handler(func(input *ec2.DescribeNetworkInterfacesInput, gw *GatewayConfig) (any, error) {
|
||||
return gateway_ec2_vpc.DescribeNetworkInterfaces(input, gw.NATSConn)
|
||||
"DescribeNetworkInterfaces": ec2Handler(func(input *ec2.DescribeNetworkInterfacesInput, gw *GatewayConfig, accountID string) (any, error) {
|
||||
return gateway_ec2_vpc.DescribeNetworkInterfaces(input, gw.NATSConn, accountID)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -244,7 +246,14 @@ func (gw *GatewayConfig) EC2_Request(ctx *fiber.Ctx) error {
|
||||
return errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
xmlOutput, err := handler(action, queryArgs, gw)
|
||||
// Extract account ID from auth context
|
||||
accountID, _ := ctx.Locals("sigv4.accountId").(string)
|
||||
if accountID == "" {
|
||||
slog.Error("EC2_Request: no account ID in auth context")
|
||||
return errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
xmlOutput, err := handler(action, queryArgs, gw, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func ValidateDisableEbsEncryptionByDefaultInput(input *ec2.DisableEbsEncryptionB
|
||||
return nil
|
||||
}
|
||||
|
||||
func DisableEbsEncryptionByDefault(input *ec2.DisableEbsEncryptionByDefaultInput, natsConn *nats.Conn) (ec2.DisableEbsEncryptionByDefaultOutput, error) {
|
||||
func DisableEbsEncryptionByDefault(input *ec2.DisableEbsEncryptionByDefaultInput, natsConn *nats.Conn, accountID string) (ec2.DisableEbsEncryptionByDefaultOutput, error) {
|
||||
var output ec2.DisableEbsEncryptionByDefaultOutput
|
||||
|
||||
if err := ValidateDisableEbsEncryptionByDefaultInput(input); err != nil {
|
||||
@@ -24,7 +24,7 @@ func DisableEbsEncryptionByDefault(input *ec2.DisableEbsEncryptionByDefaultInput
|
||||
}
|
||||
|
||||
svc := handlers_ec2_account.NewNATSAccountSettingsService(natsConn)
|
||||
result, err := svc.DisableEbsEncryptionByDefault(input)
|
||||
result, err := svc.DisableEbsEncryptionByDefault(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func ValidateDisableSerialConsoleAccessInput(input *ec2.DisableSerialConsoleAcce
|
||||
return nil
|
||||
}
|
||||
|
||||
func DisableSerialConsoleAccess(input *ec2.DisableSerialConsoleAccessInput, natsConn *nats.Conn) (ec2.DisableSerialConsoleAccessOutput, error) {
|
||||
func DisableSerialConsoleAccess(input *ec2.DisableSerialConsoleAccessInput, natsConn *nats.Conn, accountID string) (ec2.DisableSerialConsoleAccessOutput, error) {
|
||||
var output ec2.DisableSerialConsoleAccessOutput
|
||||
|
||||
if err := ValidateDisableSerialConsoleAccessInput(input); err != nil {
|
||||
@@ -24,7 +24,7 @@ func DisableSerialConsoleAccess(input *ec2.DisableSerialConsoleAccessInput, nats
|
||||
}
|
||||
|
||||
svc := handlers_ec2_account.NewNATSAccountSettingsService(natsConn)
|
||||
result, err := svc.DisableSerialConsoleAccess(input)
|
||||
result, err := svc.DisableSerialConsoleAccess(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func ValidateEnableEbsEncryptionByDefaultInput(input *ec2.EnableEbsEncryptionByD
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnableEbsEncryptionByDefault(input *ec2.EnableEbsEncryptionByDefaultInput, natsConn *nats.Conn) (ec2.EnableEbsEncryptionByDefaultOutput, error) {
|
||||
func EnableEbsEncryptionByDefault(input *ec2.EnableEbsEncryptionByDefaultInput, natsConn *nats.Conn, accountID string) (ec2.EnableEbsEncryptionByDefaultOutput, error) {
|
||||
var output ec2.EnableEbsEncryptionByDefaultOutput
|
||||
|
||||
if err := ValidateEnableEbsEncryptionByDefaultInput(input); err != nil {
|
||||
@@ -24,7 +24,7 @@ func EnableEbsEncryptionByDefault(input *ec2.EnableEbsEncryptionByDefaultInput,
|
||||
}
|
||||
|
||||
svc := handlers_ec2_account.NewNATSAccountSettingsService(natsConn)
|
||||
result, err := svc.EnableEbsEncryptionByDefault(input)
|
||||
result, err := svc.EnableEbsEncryptionByDefault(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func ValidateEnableSerialConsoleAccessInput(input *ec2.EnableSerialConsoleAccess
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnableSerialConsoleAccess(input *ec2.EnableSerialConsoleAccessInput, natsConn *nats.Conn) (ec2.EnableSerialConsoleAccessOutput, error) {
|
||||
func EnableSerialConsoleAccess(input *ec2.EnableSerialConsoleAccessInput, natsConn *nats.Conn, accountID string) (ec2.EnableSerialConsoleAccessOutput, error) {
|
||||
var output ec2.EnableSerialConsoleAccessOutput
|
||||
|
||||
if err := ValidateEnableSerialConsoleAccessInput(input); err != nil {
|
||||
@@ -24,7 +24,7 @@ func EnableSerialConsoleAccess(input *ec2.EnableSerialConsoleAccessInput, natsCo
|
||||
}
|
||||
|
||||
svc := handlers_ec2_account.NewNATSAccountSettingsService(natsConn)
|
||||
result, err := svc.EnableSerialConsoleAccess(input)
|
||||
result, err := svc.EnableSerialConsoleAccess(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func ValidateGetEbsEncryptionByDefaultInput(input *ec2.GetEbsEncryptionByDefault
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetEbsEncryptionByDefault(input *ec2.GetEbsEncryptionByDefaultInput, natsConn *nats.Conn) (ec2.GetEbsEncryptionByDefaultOutput, error) {
|
||||
func GetEbsEncryptionByDefault(input *ec2.GetEbsEncryptionByDefaultInput, natsConn *nats.Conn, accountID string) (ec2.GetEbsEncryptionByDefaultOutput, error) {
|
||||
var output ec2.GetEbsEncryptionByDefaultOutput
|
||||
|
||||
if err := ValidateGetEbsEncryptionByDefaultInput(input); err != nil {
|
||||
@@ -24,7 +24,7 @@ func GetEbsEncryptionByDefault(input *ec2.GetEbsEncryptionByDefaultInput, natsCo
|
||||
}
|
||||
|
||||
svc := handlers_ec2_account.NewNATSAccountSettingsService(natsConn)
|
||||
result, err := svc.GetEbsEncryptionByDefault(input)
|
||||
result, err := svc.GetEbsEncryptionByDefault(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func ValidateGetSerialConsoleAccessStatusInput(input *ec2.GetSerialConsoleAccess
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetSerialConsoleAccessStatus(input *ec2.GetSerialConsoleAccessStatusInput, natsConn *nats.Conn) (ec2.GetSerialConsoleAccessStatusOutput, error) {
|
||||
func GetSerialConsoleAccessStatus(input *ec2.GetSerialConsoleAccessStatusInput, natsConn *nats.Conn, accountID string) (ec2.GetSerialConsoleAccessStatusOutput, error) {
|
||||
var output ec2.GetSerialConsoleAccessStatusOutput
|
||||
|
||||
if err := ValidateGetSerialConsoleAccessStatusInput(input); err != nil {
|
||||
@@ -24,7 +24,7 @@ func GetSerialConsoleAccessStatus(input *ec2.GetSerialConsoleAccessStatusInput,
|
||||
}
|
||||
|
||||
svc := handlers_ec2_account.NewNATSAccountSettingsService(natsConn)
|
||||
result, err := svc.GetSerialConsoleAccessStatus(input)
|
||||
result, err := svc.GetSerialConsoleAccessStatus(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func ValidateCreateEgressOnlyInternetGatewayInput(input *ec2.CreateEgressOnlyInt
|
||||
}
|
||||
|
||||
// CreateEgressOnlyInternetGateway handles the EC2 CreateEgressOnlyInternetGateway API call
|
||||
func CreateEgressOnlyInternetGateway(input *ec2.CreateEgressOnlyInternetGatewayInput, natsConn *nats.Conn) (ec2.CreateEgressOnlyInternetGatewayOutput, error) {
|
||||
func CreateEgressOnlyInternetGateway(input *ec2.CreateEgressOnlyInternetGatewayInput, natsConn *nats.Conn, accountID string) (ec2.CreateEgressOnlyInternetGatewayOutput, error) {
|
||||
var output ec2.CreateEgressOnlyInternetGatewayOutput
|
||||
|
||||
if err := ValidateCreateEgressOnlyInternetGatewayInput(input); err != nil {
|
||||
@@ -31,7 +31,7 @@ func CreateEgressOnlyInternetGateway(input *ec2.CreateEgressOnlyInternetGatewayI
|
||||
}
|
||||
|
||||
svc := handlers_ec2_eigw.NewNATSEgressOnlyIGWService(natsConn)
|
||||
result, err := svc.CreateEgressOnlyInternetGateway(input)
|
||||
result, err := svc.CreateEgressOnlyInternetGateway(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func ValidateDeleteEgressOnlyInternetGatewayInput(input *ec2.DeleteEgressOnlyInt
|
||||
}
|
||||
|
||||
// DeleteEgressOnlyInternetGateway handles the EC2 DeleteEgressOnlyInternetGateway API call
|
||||
func DeleteEgressOnlyInternetGateway(input *ec2.DeleteEgressOnlyInternetGatewayInput, natsConn *nats.Conn) (ec2.DeleteEgressOnlyInternetGatewayOutput, error) {
|
||||
func DeleteEgressOnlyInternetGateway(input *ec2.DeleteEgressOnlyInternetGatewayInput, natsConn *nats.Conn, accountID string) (ec2.DeleteEgressOnlyInternetGatewayOutput, error) {
|
||||
var output ec2.DeleteEgressOnlyInternetGatewayOutput
|
||||
|
||||
if err := ValidateDeleteEgressOnlyInternetGatewayInput(input); err != nil {
|
||||
@@ -31,7 +31,7 @@ func DeleteEgressOnlyInternetGateway(input *ec2.DeleteEgressOnlyInternetGatewayI
|
||||
}
|
||||
|
||||
svc := handlers_ec2_eigw.NewNATSEgressOnlyIGWService(natsConn)
|
||||
result, err := svc.DeleteEgressOnlyInternetGateway(input)
|
||||
result, err := svc.DeleteEgressOnlyInternetGateway(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func ValidateDescribeEgressOnlyInternetGatewaysInput(input *ec2.DescribeEgressOn
|
||||
}
|
||||
|
||||
// DescribeEgressOnlyInternetGateways handles the EC2 DescribeEgressOnlyInternetGateways API call
|
||||
func DescribeEgressOnlyInternetGateways(input *ec2.DescribeEgressOnlyInternetGatewaysInput, natsConn *nats.Conn) (ec2.DescribeEgressOnlyInternetGatewaysOutput, error) {
|
||||
func DescribeEgressOnlyInternetGateways(input *ec2.DescribeEgressOnlyInternetGatewaysInput, natsConn *nats.Conn, accountID string) (ec2.DescribeEgressOnlyInternetGatewaysOutput, error) {
|
||||
var output ec2.DescribeEgressOnlyInternetGatewaysOutput
|
||||
|
||||
if err := ValidateDescribeEgressOnlyInternetGatewaysInput(input); err != nil {
|
||||
@@ -27,7 +27,7 @@ func DescribeEgressOnlyInternetGateways(input *ec2.DescribeEgressOnlyInternetGat
|
||||
}
|
||||
|
||||
svc := handlers_ec2_eigw.NewNATSEgressOnlyIGWService(natsConn)
|
||||
result, err := svc.DescribeEgressOnlyInternetGateways(input)
|
||||
result, err := svc.DescribeEgressOnlyInternetGateways(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// AttachInternetGateway handles the EC2 AttachInternetGateway API call
|
||||
func AttachInternetGateway(input *ec2.AttachInternetGatewayInput, natsConn *nats.Conn) (ec2.AttachInternetGatewayOutput, error) {
|
||||
func AttachInternetGateway(input *ec2.AttachInternetGatewayInput, natsConn *nats.Conn, accountID string) (ec2.AttachInternetGatewayOutput, error) {
|
||||
var output ec2.AttachInternetGatewayOutput
|
||||
|
||||
if input == nil {
|
||||
@@ -24,7 +24,7 @@ func AttachInternetGateway(input *ec2.AttachInternetGatewayInput, natsConn *nats
|
||||
}
|
||||
|
||||
svc := handlers_ec2_igw.NewNATSIGWService(natsConn)
|
||||
result, err := svc.AttachInternetGateway(input)
|
||||
result, err := svc.AttachInternetGateway(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// CreateInternetGateway handles the EC2 CreateInternetGateway API call
|
||||
func CreateInternetGateway(input *ec2.CreateInternetGatewayInput, natsConn *nats.Conn) (ec2.CreateInternetGatewayOutput, error) {
|
||||
func CreateInternetGateway(input *ec2.CreateInternetGatewayInput, natsConn *nats.Conn, accountID string) (ec2.CreateInternetGatewayOutput, error) {
|
||||
var output ec2.CreateInternetGatewayOutput
|
||||
|
||||
if input == nil {
|
||||
@@ -18,7 +18,7 @@ func CreateInternetGateway(input *ec2.CreateInternetGatewayInput, natsConn *nats
|
||||
}
|
||||
|
||||
svc := handlers_ec2_igw.NewNATSIGWService(natsConn)
|
||||
result, err := svc.CreateInternetGateway(input)
|
||||
result, err := svc.CreateInternetGateway(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// DeleteInternetGateway handles the EC2 DeleteInternetGateway API call
|
||||
func DeleteInternetGateway(input *ec2.DeleteInternetGatewayInput, natsConn *nats.Conn) (ec2.DeleteInternetGatewayOutput, error) {
|
||||
func DeleteInternetGateway(input *ec2.DeleteInternetGatewayInput, natsConn *nats.Conn, accountID string) (ec2.DeleteInternetGatewayOutput, error) {
|
||||
var output ec2.DeleteInternetGatewayOutput
|
||||
|
||||
if input == nil {
|
||||
@@ -21,7 +21,7 @@ func DeleteInternetGateway(input *ec2.DeleteInternetGatewayInput, natsConn *nats
|
||||
}
|
||||
|
||||
svc := handlers_ec2_igw.NewNATSIGWService(natsConn)
|
||||
result, err := svc.DeleteInternetGateway(input)
|
||||
result, err := svc.DeleteInternetGateway(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// DescribeInternetGateways handles the EC2 DescribeInternetGateways API call
|
||||
func DescribeInternetGateways(input *ec2.DescribeInternetGatewaysInput, natsConn *nats.Conn) (ec2.DescribeInternetGatewaysOutput, error) {
|
||||
func DescribeInternetGateways(input *ec2.DescribeInternetGatewaysInput, natsConn *nats.Conn, accountID string) (ec2.DescribeInternetGatewaysOutput, error) {
|
||||
var output ec2.DescribeInternetGatewaysOutput
|
||||
|
||||
if input == nil {
|
||||
@@ -18,7 +18,7 @@ func DescribeInternetGateways(input *ec2.DescribeInternetGatewaysInput, natsConn
|
||||
}
|
||||
|
||||
svc := handlers_ec2_igw.NewNATSIGWService(natsConn)
|
||||
result, err := svc.DescribeInternetGateways(input)
|
||||
result, err := svc.DescribeInternetGateways(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// DetachInternetGateway handles the EC2 DetachInternetGateway API call
|
||||
func DetachInternetGateway(input *ec2.DetachInternetGatewayInput, natsConn *nats.Conn) (ec2.DetachInternetGatewayOutput, error) {
|
||||
func DetachInternetGateway(input *ec2.DetachInternetGatewayInput, natsConn *nats.Conn, accountID string) (ec2.DetachInternetGatewayOutput, error) {
|
||||
var output ec2.DetachInternetGatewayOutput
|
||||
|
||||
if input == nil {
|
||||
@@ -24,7 +24,7 @@ func DetachInternetGateway(input *ec2.DetachInternetGatewayInput, natsConn *nats
|
||||
}
|
||||
|
||||
svc := handlers_ec2_igw.NewNATSIGWService(natsConn)
|
||||
result, err := svc.DetachInternetGateway(input)
|
||||
result, err := svc.DetachInternetGateway(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -10,76 +10,76 @@ import (
|
||||
)
|
||||
|
||||
func TestCreateInternetGateway_NilInput(t *testing.T) {
|
||||
_, err := CreateInternetGateway(nil, nil)
|
||||
_, err := CreateInternetGateway(nil, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestDeleteInternetGateway_NilInput(t *testing.T) {
|
||||
_, err := DeleteInternetGateway(nil, nil)
|
||||
_, err := DeleteInternetGateway(nil, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestDeleteInternetGateway_NilIGWId(t *testing.T) {
|
||||
_, err := DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{}, nil)
|
||||
_, err := DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{}, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDeleteInternetGateway_EmptyIGWId(t *testing.T) {
|
||||
_, err := DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{InternetGatewayId: aws.String("")}, nil)
|
||||
_, err := DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{InternetGatewayId: aws.String("")}, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestAttachInternetGateway_NilInput(t *testing.T) {
|
||||
_, err := AttachInternetGateway(nil, nil)
|
||||
_, err := AttachInternetGateway(nil, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestAttachInternetGateway_NilIGWId(t *testing.T) {
|
||||
_, err := AttachInternetGateway(&ec2.AttachInternetGatewayInput{VpcId: aws.String("vpc-123")}, nil)
|
||||
_, err := AttachInternetGateway(&ec2.AttachInternetGatewayInput{VpcId: aws.String("vpc-123")}, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestAttachInternetGateway_EmptyIGWId(t *testing.T) {
|
||||
_, err := AttachInternetGateway(&ec2.AttachInternetGatewayInput{InternetGatewayId: aws.String(""), VpcId: aws.String("vpc-123")}, nil)
|
||||
_, err := AttachInternetGateway(&ec2.AttachInternetGatewayInput{InternetGatewayId: aws.String(""), VpcId: aws.String("vpc-123")}, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestAttachInternetGateway_NilVpcId(t *testing.T) {
|
||||
_, err := AttachInternetGateway(&ec2.AttachInternetGatewayInput{InternetGatewayId: aws.String("igw-123")}, nil)
|
||||
_, err := AttachInternetGateway(&ec2.AttachInternetGatewayInput{InternetGatewayId: aws.String("igw-123")}, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestAttachInternetGateway_EmptyVpcId(t *testing.T) {
|
||||
_, err := AttachInternetGateway(&ec2.AttachInternetGatewayInput{InternetGatewayId: aws.String("igw-123"), VpcId: aws.String("")}, nil)
|
||||
_, err := AttachInternetGateway(&ec2.AttachInternetGatewayInput{InternetGatewayId: aws.String("igw-123"), VpcId: aws.String("")}, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDetachInternetGateway_NilInput(t *testing.T) {
|
||||
_, err := DetachInternetGateway(nil, nil)
|
||||
_, err := DetachInternetGateway(nil, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestDetachInternetGateway_NilIGWId(t *testing.T) {
|
||||
_, err := DetachInternetGateway(&ec2.DetachInternetGatewayInput{VpcId: aws.String("vpc-123")}, nil)
|
||||
_, err := DetachInternetGateway(&ec2.DetachInternetGatewayInput{VpcId: aws.String("vpc-123")}, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDetachInternetGateway_EmptyIGWId(t *testing.T) {
|
||||
_, err := DetachInternetGateway(&ec2.DetachInternetGatewayInput{InternetGatewayId: aws.String(""), VpcId: aws.String("vpc-123")}, nil)
|
||||
_, err := DetachInternetGateway(&ec2.DetachInternetGatewayInput{InternetGatewayId: aws.String(""), VpcId: aws.String("vpc-123")}, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDetachInternetGateway_NilVpcId(t *testing.T) {
|
||||
_, err := DetachInternetGateway(&ec2.DetachInternetGatewayInput{InternetGatewayId: aws.String("igw-123")}, nil)
|
||||
_, err := DetachInternetGateway(&ec2.DetachInternetGatewayInput{InternetGatewayId: aws.String("igw-123")}, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDetachInternetGateway_EmptyVpcId(t *testing.T) {
|
||||
_, err := DetachInternetGateway(&ec2.DetachInternetGatewayInput{InternetGatewayId: aws.String("igw-123"), VpcId: aws.String("")}, nil)
|
||||
_, err := DetachInternetGateway(&ec2.DetachInternetGatewayInput{InternetGatewayId: aws.String("igw-123"), VpcId: aws.String("")}, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDescribeInternetGateways_NilInput(t *testing.T) {
|
||||
_, err := DescribeInternetGateways(nil, nil)
|
||||
_, err := DescribeInternetGateways(nil, nil, "")
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func ValidateCreateImageInput(input *ec2.CreateImageInput) error {
|
||||
}
|
||||
|
||||
// CreateImage handles the EC2 CreateImage API call
|
||||
func CreateImage(input *ec2.CreateImageInput, natsConn *nats.Conn) (ec2.CreateImageOutput, error) {
|
||||
func CreateImage(input *ec2.CreateImageInput, natsConn *nats.Conn, accountID string) (ec2.CreateImageOutput, error) {
|
||||
var output ec2.CreateImageOutput
|
||||
|
||||
if err := ValidateCreateImageInput(input); err != nil {
|
||||
@@ -40,7 +40,7 @@ func CreateImage(input *ec2.CreateImageInput, natsConn *nats.Conn) (ec2.CreateIm
|
||||
}
|
||||
|
||||
svc := handlers_ec2_image.NewNATSImageService(natsConn)
|
||||
result, err := svc.CreateImage(input)
|
||||
result, err := svc.CreateImage(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func ValidateDescribeImagesInput(input *ec2.DescribeImagesInput) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func DescribeImages(input *ec2.DescribeImagesInput, natsConn *nats.Conn) (output ec2.DescribeImagesOutput, err error) {
|
||||
func DescribeImages(input *ec2.DescribeImagesInput, natsConn *nats.Conn, accountID string) (output ec2.DescribeImagesOutput, err error) {
|
||||
|
||||
// Validate input
|
||||
err = ValidateDescribeImagesInput(input)
|
||||
@@ -38,7 +38,7 @@ func DescribeImages(input *ec2.DescribeImagesInput, natsConn *nats.Conn) (output
|
||||
|
||||
// Create NATS service and call handler
|
||||
imageService := handlers_ec2_image.NewNATSImageService(natsConn)
|
||||
result, err := imageService.DescribeImages(input)
|
||||
result, err := imageService.DescribeImages(input, accountID)
|
||||
|
||||
if err != nil {
|
||||
return output, err
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
// DescribeInstances queries all hive nodes for their instances via NATS
|
||||
// and aggregates the results into a single response
|
||||
func DescribeInstances(input *ec2.DescribeInstancesInput, natsConn *nats.Conn, expectedNodes int) (*ec2.DescribeInstancesOutput, error) {
|
||||
func DescribeInstances(input *ec2.DescribeInstancesInput, natsConn *nats.Conn, expectedNodes int, accountID string) (*ec2.DescribeInstancesOutput, error) {
|
||||
// Marshal input to JSON
|
||||
jsonData, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
@@ -30,8 +30,12 @@ func DescribeInstances(input *ec2.DescribeInstancesInput, natsConn *nats.Conn, e
|
||||
}
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
// Publish request to all nodes (no queue group, so all daemons receive it)
|
||||
err = natsConn.PublishRequest("ec2.DescribeInstances", inbox, jsonData)
|
||||
// Publish request to all nodes with account ID header
|
||||
pubMsg := nats.NewMsg("ec2.DescribeInstances")
|
||||
pubMsg.Reply = inbox
|
||||
pubMsg.Data = jsonData
|
||||
pubMsg.Header.Set(utils.AccountIDHeader, accountID)
|
||||
err = natsConn.PublishMsg(pubMsg)
|
||||
if err != nil {
|
||||
slog.Error("DescribeInstances: Failed to publish request", "err", err)
|
||||
return nil, fmt.Errorf("failed to publish request: %w", err)
|
||||
@@ -102,7 +106,10 @@ func DescribeInstances(input *ec2.DescribeInstancesInput, natsConn *nats.Conn, e
|
||||
}
|
||||
|
||||
// Query stopped instances from shared KV (via queue group — only one daemon handles it)
|
||||
stoppedMsg, err := natsConn.Request("ec2.DescribeStoppedInstances", jsonData, 3*time.Second)
|
||||
stoppedReqMsg := nats.NewMsg("ec2.DescribeStoppedInstances")
|
||||
stoppedReqMsg.Data = jsonData
|
||||
stoppedReqMsg.Header.Set(utils.AccountIDHeader, accountID)
|
||||
stoppedMsg, err := natsConn.RequestMsg(stoppedReqMsg, 3*time.Second)
|
||||
if err != nil {
|
||||
slog.Warn("DescribeInstances: Failed to query stopped instances (may not be available)", "err", err)
|
||||
} else {
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestDescribeInstances_SingleNode(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
input := &ec2.DescribeInstancesInput{}
|
||||
output, err := DescribeInstances(input, nc, 1)
|
||||
output, err := DescribeInstances(input, nc, 1, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, output)
|
||||
@@ -87,7 +87,7 @@ func TestDescribeInstances_MultipleNodes(t *testing.T) {
|
||||
nc2.Flush()
|
||||
|
||||
input := &ec2.DescribeInstancesInput{}
|
||||
output, err := DescribeInstances(input, nc, 2)
|
||||
output, err := DescribeInstances(input, nc, 2, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, output)
|
||||
@@ -98,7 +98,7 @@ func TestDescribeInstances_NoSubscribers(t *testing.T) {
|
||||
_, nc := startTestNATSServer(t)
|
||||
|
||||
input := &ec2.DescribeInstancesInput{}
|
||||
output, err := DescribeInstances(input, nc, 0)
|
||||
output, err := DescribeInstances(input, nc, 0, "123456789012")
|
||||
|
||||
// No subscribers means timeout — function returns empty reservations, no error
|
||||
require.NoError(t, err)
|
||||
@@ -115,7 +115,7 @@ func TestDescribeInstances_NodeReturnsError(t *testing.T) {
|
||||
})
|
||||
|
||||
input := &ec2.DescribeInstancesInput{}
|
||||
output, err := DescribeInstances(input, nc, 1)
|
||||
output, err := DescribeInstances(input, nc, 1, "123456789012")
|
||||
|
||||
// Error responses from nodes are logged but don't fail the overall call
|
||||
require.NoError(t, err)
|
||||
@@ -153,7 +153,7 @@ func TestDescribeInstances_MixedResponses(t *testing.T) {
|
||||
nc2.Flush()
|
||||
|
||||
input := &ec2.DescribeInstancesInput{}
|
||||
output, err := DescribeInstances(input, nc, 2)
|
||||
output, err := DescribeInstances(input, nc, 2, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, output)
|
||||
@@ -170,7 +170,7 @@ func TestDescribeInstances_MalformedJSON(t *testing.T) {
|
||||
})
|
||||
|
||||
input := &ec2.DescribeInstancesInput{}
|
||||
output, err := DescribeInstances(input, nc, 1)
|
||||
output, err := DescribeInstances(input, nc, 1, "123456789012")
|
||||
|
||||
// Malformed JSON from a node is skipped, not fatal
|
||||
require.NoError(t, err)
|
||||
@@ -189,7 +189,7 @@ func TestDescribeInstances_EmptyReservations(t *testing.T) {
|
||||
})
|
||||
|
||||
input := &ec2.DescribeInstancesInput{}
|
||||
output, err := DescribeInstances(input, nc, 1)
|
||||
output, err := DescribeInstances(input, nc, 1, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, output)
|
||||
@@ -211,7 +211,7 @@ func TestDescribeInstances_TimeoutCollection(t *testing.T) {
|
||||
})
|
||||
|
||||
input := &ec2.DescribeInstancesInput{}
|
||||
output, err := DescribeInstances(input, nc, 0) // 0 = timeout-based collection
|
||||
output, err := DescribeInstances(input, nc, 0, "123456789012") // 0 = timeout-based collection
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, output)
|
||||
@@ -232,7 +232,7 @@ func TestDescribeInstances_EarlyExitWithExpectedNodes(t *testing.T) {
|
||||
|
||||
input := &ec2.DescribeInstancesInput{}
|
||||
start := time.Now()
|
||||
output, err := DescribeInstances(input, nc, 1)
|
||||
output, err := DescribeInstances(input, nc, 1, "123456789012")
|
||||
duration := time.Since(start)
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -250,7 +250,7 @@ func TestDescribeInstances_ClosedConnection(t *testing.T) {
|
||||
closedNC.Close()
|
||||
|
||||
input := &ec2.DescribeInstancesInput{}
|
||||
_, err = DescribeInstances(input, closedNC, 1)
|
||||
_, err = DescribeInstances(input, closedNC, 1, "123456789012")
|
||||
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -8,14 +8,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/mulgadc/hive/hive/utils"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// GetConsoleOutput retrieves console output for a specific instance via NATS.
|
||||
// Routes directly to the node running the instance via ec2.{instanceID}.GetConsoleOutput.
|
||||
func GetConsoleOutput(input *ec2.GetConsoleOutputInput, natsConn *nats.Conn) (*ec2.GetConsoleOutputOutput, error) {
|
||||
func GetConsoleOutput(input *ec2.GetConsoleOutputInput, natsConn *nats.Conn, accountID string) (*ec2.GetConsoleOutputOutput, error) {
|
||||
if input.InstanceId == nil || *input.InstanceId == "" {
|
||||
return nil, fmt.Errorf("InstanceId is required")
|
||||
}
|
||||
@@ -26,7 +25,10 @@ func GetConsoleOutput(input *ec2.GetConsoleOutputInput, natsConn *nats.Conn) (*e
|
||||
}
|
||||
|
||||
topic := fmt.Sprintf("ec2.%s.GetConsoleOutput", *input.InstanceId)
|
||||
msg, err := natsConn.Request(topic, jsonData, 5*time.Second)
|
||||
reqMsg := nats.NewMsg(topic)
|
||||
reqMsg.Data = jsonData
|
||||
reqMsg.Header.Set(utils.AccountIDHeader, accountID)
|
||||
msg, err := natsConn.RequestMsg(reqMsg, 5*time.Second)
|
||||
if err != nil {
|
||||
if err == nats.ErrNoResponders || err == nats.ErrTimeout {
|
||||
return nil, fmt.Errorf("instance %s not found or not running", *input.InstanceId)
|
||||
|
||||
@@ -49,7 +49,7 @@ func ValidateModifyInstanceAttributeInput(input *ec2.ModifyInstanceAttributeInpu
|
||||
|
||||
// ModifyInstanceAttribute sends a modify request to the daemon via NATS.
|
||||
// The daemon updates the stopped instance in KV and returns an empty response on success.
|
||||
func ModifyInstanceAttribute(input *ec2.ModifyInstanceAttributeInput, natsConn *nats.Conn) (ec2.ModifyInstanceAttributeOutput, error) {
|
||||
func ModifyInstanceAttribute(input *ec2.ModifyInstanceAttributeInput, natsConn *nats.Conn, accountID string) (ec2.ModifyInstanceAttributeOutput, error) {
|
||||
if err := ValidateModifyInstanceAttributeInput(input); err != nil {
|
||||
return ec2.ModifyInstanceAttributeOutput{}, err
|
||||
}
|
||||
@@ -62,7 +62,10 @@ func ModifyInstanceAttribute(input *ec2.ModifyInstanceAttributeInput, natsConn *
|
||||
return ec2.ModifyInstanceAttributeOutput{}, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
msg, err := natsConn.Request("ec2.ModifyInstanceAttribute", jsonData, 30*time.Second)
|
||||
reqMsg := nats.NewMsg("ec2.ModifyInstanceAttribute")
|
||||
reqMsg.Data = jsonData
|
||||
reqMsg.Header.Set(utils.AccountIDHeader, accountID)
|
||||
msg, err := natsConn.RequestMsg(reqMsg, 30*time.Second)
|
||||
if err != nil {
|
||||
slog.Error("ModifyInstanceAttribute: Failed to send request", "instance_id", *input.InstanceId, "err", err)
|
||||
return ec2.ModifyInstanceAttributeOutput{}, fmt.Errorf("failed to send modify request: %w", err)
|
||||
|
||||
@@ -113,7 +113,7 @@ func TestModifyInstanceAttribute_Success(t *testing.T) {
|
||||
InstanceType: &ec2.AttributeValue{Value: aws.String("t3.medium")},
|
||||
}
|
||||
|
||||
_, err := ModifyInstanceAttribute(input, nc)
|
||||
_, err := ModifyInstanceAttribute(input, nc, "123456789012")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func TestModifyInstanceAttribute_DaemonError(t *testing.T) {
|
||||
InstanceType: &ec2.AttributeValue{Value: aws.String("t3.medium")},
|
||||
}
|
||||
|
||||
_, err := ModifyInstanceAttribute(input, nc)
|
||||
_, err := ModifyInstanceAttribute(input, nc, "123456789012")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidInstanceIDNotFound, err.Error())
|
||||
}
|
||||
@@ -137,7 +137,7 @@ func TestModifyInstanceAttribute_DaemonError(t *testing.T) {
|
||||
func TestModifyInstanceAttribute_ValidationFailure(t *testing.T) {
|
||||
_, nc := startTestNATSServer(t)
|
||||
|
||||
_, err := ModifyInstanceAttribute(nil, nc)
|
||||
_, err := ModifyInstanceAttribute(nil, nc, "123456789012")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidParameterValue, err.Error())
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func ValidateRunInstancesInput(input *ec2.RunInstancesInput) (err error) {
|
||||
|
||||
}
|
||||
|
||||
func RunInstances(input *ec2.RunInstancesInput, natsConn *nats.Conn) (reservation ec2.Reservation, err error) {
|
||||
func RunInstances(input *ec2.RunInstancesInput, natsConn *nats.Conn, accountID string) (reservation ec2.Reservation, err error) {
|
||||
|
||||
// Validate input
|
||||
err = ValidateRunInstancesInput(input)
|
||||
@@ -69,7 +69,7 @@ func RunInstances(input *ec2.RunInstancesInput, natsConn *nats.Conn) (reservatio
|
||||
service := handlers_ec2_instance.NewNATSInstanceService(natsConn)
|
||||
|
||||
// Call the service directly (no need for JSON marshaling/unmarshaling in same process)
|
||||
reservationPtr, err := service.RunInstances(input)
|
||||
reservationPtr, err := service.RunInstances(input, accountID)
|
||||
if err != nil {
|
||||
if errors.Is(err, nats.ErrNoResponders) {
|
||||
// ErrNoResponders means no daemon subscribes to ec2.RunInstances.{type}.
|
||||
@@ -90,7 +90,7 @@ func RunInstances(input *ec2.RunInstancesInput, natsConn *nats.Conn) (reservatio
|
||||
// isKnownInstanceType checks whether any daemon recognizes the given instance type.
|
||||
func isKnownInstanceType(natsConn *nats.Conn, instanceType string) bool {
|
||||
result, err := utils.NATSRequest[ec2.DescribeInstanceTypesOutput](
|
||||
natsConn, "ec2.DescribeInstanceTypes", &ec2.DescribeInstanceTypesInput{}, 3*time.Second)
|
||||
natsConn, "ec2.DescribeInstanceTypes", &ec2.DescribeInstanceTypesInput{}, 3*time.Second, "")
|
||||
if err != nil || result == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ func TestParseRunInstances(t *testing.T) {
|
||||
}
|
||||
|
||||
// For validation tests, we can pass nil conn since validation happens before NATS call
|
||||
response, err := RunInstances(test.input, nil)
|
||||
response, err := RunInstances(test.input, nil, "123456789012")
|
||||
|
||||
// Use assert to check if the error is as expected
|
||||
assert.Equal(t, test.want, err)
|
||||
|
||||
@@ -19,7 +19,7 @@ type startStoppedInstanceRequest struct {
|
||||
|
||||
// StartInstances sends start requests to the ec2.start queue group topic.
|
||||
// Any available daemon can pick up the request and launch the stopped instance.
|
||||
func StartInstances(input *ec2.StartInstancesInput, natsConn *nats.Conn) (*ec2.StartInstancesOutput, error) {
|
||||
func StartInstances(input *ec2.StartInstancesInput, natsConn *nats.Conn, accountID string) (*ec2.StartInstancesOutput, error) {
|
||||
if len(input.InstanceIds) == 0 {
|
||||
return nil, fmt.Errorf("no instance IDs provided")
|
||||
}
|
||||
@@ -43,7 +43,10 @@ func StartInstances(input *ec2.StartInstancesInput, natsConn *nats.Conn) (*ec2.S
|
||||
|
||||
slog.Info("StartInstances: Sending NATS request", "subject", "ec2.start", "instance_id", instanceID)
|
||||
|
||||
msg, err := natsConn.Request("ec2.start", jsonData, 30*time.Second)
|
||||
reqMsg := nats.NewMsg("ec2.start")
|
||||
reqMsg.Data = jsonData
|
||||
reqMsg.Header.Set(utils.AccountIDHeader, accountID)
|
||||
msg, err := natsConn.RequestMsg(reqMsg, 30*time.Second)
|
||||
if err != nil {
|
||||
slog.Error("StartInstances: Failed to send start request", "instance_id", instanceID, "err", err)
|
||||
stateChanges = append(stateChanges, newStateChange(instanceID, 80, "stopped", 80, "stopped"))
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestStartInstances_Success(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(instanceID)},
|
||||
}
|
||||
|
||||
output, err := StartInstances(input, nc)
|
||||
output, err := StartInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, output)
|
||||
@@ -61,7 +61,7 @@ func TestStartInstances_MultipleInstances(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(ids[0]), aws.String(ids[1]), aws.String(ids[2])},
|
||||
}
|
||||
|
||||
output, err := StartInstances(input, nc)
|
||||
output, err := StartInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, output)
|
||||
@@ -80,7 +80,7 @@ func TestStartInstances_EmptyInstanceIds(t *testing.T) {
|
||||
InstanceIds: []*string{},
|
||||
}
|
||||
|
||||
_, err := StartInstances(input, nc)
|
||||
_, err := StartInstances(input, nc, "123456789012")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no instance IDs provided")
|
||||
}
|
||||
@@ -97,7 +97,7 @@ func TestStartInstances_NilInstanceIdSkipped(t *testing.T) {
|
||||
InstanceIds: []*string{nil, aws.String(instanceID), nil},
|
||||
}
|
||||
|
||||
output, err := StartInstances(input, nc)
|
||||
output, err := StartInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, output)
|
||||
@@ -116,7 +116,7 @@ func TestStartInstances_NATSRequestFails(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(instanceID)},
|
||||
}
|
||||
|
||||
output, err := StartInstances(input, nc)
|
||||
output, err := StartInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err) // Function itself doesn't error, it records state change
|
||||
require.NotNil(t, output)
|
||||
@@ -153,7 +153,7 @@ func TestStartInstances_MixedSuccessAndFailure(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(goodID), aws.String(badID)},
|
||||
}
|
||||
|
||||
_, err := StartInstances(input, nc)
|
||||
_, err := StartInstances(input, nc, "123456789012")
|
||||
|
||||
// Daemon error for any instance should fail the whole call
|
||||
require.Error(t, err)
|
||||
|
||||
@@ -2,18 +2,20 @@ package gateway_ec2_instance
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mulgadc/hive/hive/qmp"
|
||||
"github.com/mulgadc/hive/hive/utils"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// StopInstances sends stop commands to specified instances via NATS
|
||||
// Uses system_powerdown with stop_instance attribute to prevent auto-restart on daemon boot
|
||||
func StopInstances(input *ec2.StopInstancesInput, natsConn *nats.Conn) (*ec2.StopInstancesOutput, error) {
|
||||
func StopInstances(input *ec2.StopInstancesInput, natsConn *nats.Conn, accountID string) (*ec2.StopInstancesOutput, error) {
|
||||
if len(input.InstanceIds) == 0 {
|
||||
return nil, fmt.Errorf("no instance IDs provided")
|
||||
}
|
||||
@@ -50,15 +52,24 @@ func StopInstances(input *ec2.StopInstancesInput, natsConn *nats.Conn) (*ec2.Sto
|
||||
continue
|
||||
}
|
||||
|
||||
// Send NATS request to the specific instance topic
|
||||
// Send NATS request to the specific instance topic with account ID header
|
||||
subject := fmt.Sprintf("ec2.cmd.%s", instanceID)
|
||||
msg, err := natsConn.Request(subject, jsonData, 5*time.Second)
|
||||
reqMsg := nats.NewMsg(subject)
|
||||
reqMsg.Data = jsonData
|
||||
reqMsg.Header.Set(utils.AccountIDHeader, accountID)
|
||||
msg, err := natsConn.RequestMsg(reqMsg, 5*time.Second)
|
||||
if err != nil {
|
||||
slog.Error("StopInstances: Failed to send command", "instance_id", instanceID, "err", err)
|
||||
stateChanges = append(stateChanges, newStateChange(instanceID, 16, "running", 16, "running"))
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the daemon returned an error response (e.g. ownership check failure)
|
||||
if responseError, parseErr := utils.ValidateErrorPayload(msg.Data); parseErr != nil {
|
||||
slog.Error("StopInstances: Daemon returned error", "instance_id", instanceID, "code", *responseError.Code)
|
||||
return nil, errors.New(*responseError.Code)
|
||||
}
|
||||
|
||||
slog.Info("StopInstances: Command sent successfully", "instance_id", instanceID, "response", string(msg.Data))
|
||||
|
||||
stateChanges = append(stateChanges, newStateChange(instanceID, 64, "stopping", 16, "running"))
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestStopInstances_Success(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(instanceID)},
|
||||
}
|
||||
|
||||
output, err := StopInstances(input, nc)
|
||||
output, err := StopInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, output)
|
||||
@@ -63,7 +63,7 @@ func TestStopInstances_MultipleInstances(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(ids[0]), aws.String(ids[1])},
|
||||
}
|
||||
|
||||
output, err := StopInstances(input, nc)
|
||||
output, err := StopInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, output.StoppingInstances, 2)
|
||||
@@ -82,7 +82,7 @@ func TestStopInstances_EmptyInstanceIds(t *testing.T) {
|
||||
InstanceIds: []*string{},
|
||||
}
|
||||
|
||||
_, err := StopInstances(input, nc)
|
||||
_, err := StopInstances(input, nc, "123456789012")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no instance IDs provided")
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func TestStopInstances_NilInstanceIdSkipped(t *testing.T) {
|
||||
InstanceIds: []*string{nil, aws.String(instanceID)},
|
||||
}
|
||||
|
||||
output, err := StopInstances(input, nc)
|
||||
output, err := StopInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, output.StoppingInstances, 1)
|
||||
@@ -115,7 +115,7 @@ func TestStopInstances_NATSRequestFails(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(instanceID)},
|
||||
}
|
||||
|
||||
output, err := StopInstances(input, nc)
|
||||
output, err := StopInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, output.StoppingInstances, 1)
|
||||
@@ -143,7 +143,7 @@ func TestStopInstances_MixedSuccessAndFailure(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(goodID), aws.String(badID)},
|
||||
}
|
||||
|
||||
output, err := StopInstances(input, nc)
|
||||
output, err := StopInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, output.StoppingInstances, 2)
|
||||
|
||||
@@ -20,7 +20,7 @@ type terminateStoppedInstanceRequest struct {
|
||||
|
||||
// TerminateInstances sends terminate commands to specified instances via NATS
|
||||
// Uses system_powerdown with stop_instance attribute to prevent restart
|
||||
func TerminateInstances(input *ec2.TerminateInstancesInput, natsConn *nats.Conn) (*ec2.TerminateInstancesOutput, error) {
|
||||
func TerminateInstances(input *ec2.TerminateInstancesInput, natsConn *nats.Conn, accountID string) (*ec2.TerminateInstancesOutput, error) {
|
||||
if len(input.InstanceIds) == 0 {
|
||||
return nil, fmt.Errorf("no instance IDs provided")
|
||||
}
|
||||
@@ -57,16 +57,22 @@ func TerminateInstances(input *ec2.TerminateInstancesInput, natsConn *nats.Conn)
|
||||
continue
|
||||
}
|
||||
|
||||
// Send NATS request to the specific instance topic
|
||||
// Send NATS request to the specific instance topic with account ID header
|
||||
subject := fmt.Sprintf("ec2.cmd.%s", instanceID)
|
||||
msg, err := natsConn.Request(subject, jsonData, 5*time.Second)
|
||||
reqMsg := nats.NewMsg(subject)
|
||||
reqMsg.Data = jsonData
|
||||
reqMsg.Header.Set(utils.AccountIDHeader, accountID)
|
||||
msg, err := natsConn.RequestMsg(reqMsg, 5*time.Second)
|
||||
if err != nil {
|
||||
// If no daemon owns this instance, try the ec2.terminate topic for stopped instances
|
||||
if errors.Is(err, nats.ErrNoResponders) {
|
||||
slog.Info("TerminateInstances: No responder on per-instance topic, trying ec2.terminate", "instance_id", instanceID)
|
||||
|
||||
terminateReq, _ := json.Marshal(terminateStoppedInstanceRequest{InstanceID: instanceID})
|
||||
terminateMsg, terminateErr := natsConn.Request("ec2.terminate", terminateReq, 30*time.Second)
|
||||
terminateReqMsg := nats.NewMsg("ec2.terminate")
|
||||
terminateReqMsg.Data = terminateReq
|
||||
terminateReqMsg.Header.Set(utils.AccountIDHeader, accountID)
|
||||
terminateMsg, terminateErr := natsConn.RequestMsg(terminateReqMsg, 30*time.Second)
|
||||
if terminateErr == nil {
|
||||
if _, parseErr := utils.ValidateErrorPayload(terminateMsg.Data); parseErr == nil {
|
||||
slog.Info("TerminateInstances: Stopped instance terminated via ec2.terminate", "instance_id", instanceID)
|
||||
@@ -81,6 +87,12 @@ func TerminateInstances(input *ec2.TerminateInstancesInput, natsConn *nats.Conn)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the daemon returned an error response (e.g. ownership check failure)
|
||||
if responseError, parseErr := utils.ValidateErrorPayload(msg.Data); parseErr != nil {
|
||||
slog.Error("TerminateInstances: Daemon returned error", "instance_id", instanceID, "code", *responseError.Code)
|
||||
return nil, errors.New(*responseError.Code)
|
||||
}
|
||||
|
||||
slog.Info("TerminateInstances: Command sent successfully", "instance_id", instanceID, "response", string(msg.Data))
|
||||
|
||||
stateChanges = append(stateChanges, newStateChange(instanceID, 32, "shutting-down", 16, "running"))
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestTerminateInstances_Success(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(instanceID)},
|
||||
}
|
||||
|
||||
output, err := TerminateInstances(input, nc)
|
||||
output, err := TerminateInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, output)
|
||||
@@ -63,7 +63,7 @@ func TestTerminateInstances_MultipleInstances(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(ids[0]), aws.String(ids[1]), aws.String(ids[2])},
|
||||
}
|
||||
|
||||
output, err := TerminateInstances(input, nc)
|
||||
output, err := TerminateInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, output.TerminatingInstances, 3)
|
||||
@@ -82,7 +82,7 @@ func TestTerminateInstances_EmptyInstanceIds(t *testing.T) {
|
||||
InstanceIds: []*string{},
|
||||
}
|
||||
|
||||
_, err := TerminateInstances(input, nc)
|
||||
_, err := TerminateInstances(input, nc, "123456789012")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no instance IDs provided")
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func TestTerminateInstances_NilInstanceIdSkipped(t *testing.T) {
|
||||
InstanceIds: []*string{nil, aws.String(instanceID), nil},
|
||||
}
|
||||
|
||||
output, err := TerminateInstances(input, nc)
|
||||
output, err := TerminateInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, output.TerminatingInstances, 1)
|
||||
@@ -115,7 +115,7 @@ func TestTerminateInstances_NATSRequestFails(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(instanceID)},
|
||||
}
|
||||
|
||||
output, err := TerminateInstances(input, nc)
|
||||
output, err := TerminateInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, output.TerminatingInstances, 1)
|
||||
@@ -143,7 +143,7 @@ func TestTerminateInstances_MixedSuccessAndFailure(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(goodID), aws.String(badID)},
|
||||
}
|
||||
|
||||
output, err := TerminateInstances(input, nc)
|
||||
output, err := TerminateInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, output.TerminatingInstances, 2)
|
||||
@@ -172,7 +172,7 @@ func TestTerminateInstances_VerifiesQMPAttributes(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(instanceID)},
|
||||
}
|
||||
|
||||
_, err := TerminateInstances(input, nc)
|
||||
_, err := TerminateInstances(input, nc, "123456789012")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Terminate should set both stop and terminate flags
|
||||
@@ -199,7 +199,7 @@ func TestTerminateInstances_StoppedInstanceFallback(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(instanceID)},
|
||||
}
|
||||
|
||||
output, err := TerminateInstances(input, nc)
|
||||
output, err := TerminateInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, output.TerminatingInstances, 1)
|
||||
@@ -234,7 +234,7 @@ func TestTerminateInstances_MixedRunningAndStopped(t *testing.T) {
|
||||
InstanceIds: []*string{aws.String(runningID), aws.String(stoppedID)},
|
||||
}
|
||||
|
||||
output, err := TerminateInstances(input, nc)
|
||||
output, err := TerminateInstances(input, nc, "123456789012")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, output.TerminatingInstances, 2)
|
||||
|
||||
@@ -22,7 +22,7 @@ func ValidateCreateKeyPairInput(input *ec2.CreateKeyPairInput) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func CreateKeyPair(input *ec2.CreateKeyPairInput, natsConn *nats.Conn) (output ec2.CreateKeyPairOutput, err error) {
|
||||
func CreateKeyPair(input *ec2.CreateKeyPairInput, natsConn *nats.Conn, accountID string) (output ec2.CreateKeyPairOutput, err error) {
|
||||
|
||||
// Validate input
|
||||
err = ValidateCreateKeyPairInput(input)
|
||||
@@ -33,7 +33,7 @@ func CreateKeyPair(input *ec2.CreateKeyPairInput, natsConn *nats.Conn) (output e
|
||||
|
||||
// Create NATS key service and call CreateKeyPair
|
||||
keyService := handlers_ec2_key.NewNATSKeyService(natsConn)
|
||||
result, err := keyService.CreateKeyPair(input)
|
||||
result, err := keyService.CreateKeyPair(input, accountID)
|
||||
|
||||
if err != nil {
|
||||
slog.Error("CreateKeyPair failed", "err", err)
|
||||
|
||||
@@ -23,7 +23,7 @@ func ValidateDeleteKeyPairInput(input *ec2.DeleteKeyPairInput) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func DeleteKeyPair(input *ec2.DeleteKeyPairInput, natsConn *nats.Conn) (output ec2.DeleteKeyPairOutput, err error) {
|
||||
func DeleteKeyPair(input *ec2.DeleteKeyPairInput, natsConn *nats.Conn, accountID string) (output ec2.DeleteKeyPairOutput, err error) {
|
||||
|
||||
// Validate input
|
||||
err = ValidateDeleteKeyPairInput(input)
|
||||
@@ -34,7 +34,7 @@ func DeleteKeyPair(input *ec2.DeleteKeyPairInput, natsConn *nats.Conn) (output e
|
||||
|
||||
// Create NATS key service and call DeleteKeyPair
|
||||
keyService := handlers_ec2_key.NewNATSKeyService(natsConn)
|
||||
result, err := keyService.DeleteKeyPair(input)
|
||||
result, err := keyService.DeleteKeyPair(input, accountID)
|
||||
|
||||
if err != nil {
|
||||
slog.Error("DeleteKeyPair failed", "err", err)
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
)
|
||||
|
||||
// DescribeKeyPairs lists key pairs via NATS
|
||||
func DescribeKeyPairs(input *ec2.DescribeKeyPairsInput, natsConn *nats.Conn) (output ec2.DescribeKeyPairsOutput, err error) {
|
||||
func DescribeKeyPairs(input *ec2.DescribeKeyPairsInput, natsConn *nats.Conn, accountID string) (output ec2.DescribeKeyPairsOutput, err error) {
|
||||
// all input fields are optional filters
|
||||
|
||||
// Create NATS key service and call DescribeKeyPairs
|
||||
keyService := handlers_ec2_key.NewNATSKeyService(natsConn)
|
||||
result, err := keyService.DescribeKeyPairs(input)
|
||||
result, err := keyService.DescribeKeyPairs(input, accountID)
|
||||
|
||||
if err != nil {
|
||||
return output, err
|
||||
|
||||
@@ -25,7 +25,7 @@ func ValidateImportKeyPairInput(input *ec2.ImportKeyPairInput) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func ImportKeyPair(input *ec2.ImportKeyPairInput, natsConn *nats.Conn) (output ec2.ImportKeyPairOutput, err error) {
|
||||
func ImportKeyPair(input *ec2.ImportKeyPairInput, natsConn *nats.Conn, accountID string) (output ec2.ImportKeyPairOutput, err error) {
|
||||
// Validate input
|
||||
err = ValidateImportKeyPairInput(input)
|
||||
|
||||
@@ -35,7 +35,7 @@ func ImportKeyPair(input *ec2.ImportKeyPairInput, natsConn *nats.Conn) (output e
|
||||
|
||||
// Create NATS key service and call ImportKeyPair
|
||||
keyService := handlers_ec2_key.NewNATSKeyService(natsConn)
|
||||
result, err := keyService.ImportKeyPair(input)
|
||||
result, err := keyService.ImportKeyPair(input, accountID)
|
||||
|
||||
if err != nil {
|
||||
return output, err
|
||||
|
||||
@@ -32,7 +32,7 @@ func ValidateCopySnapshotInput(input *ec2.CopySnapshotInput) error {
|
||||
}
|
||||
|
||||
// CopySnapshot handles the EC2 CopySnapshot API call
|
||||
func CopySnapshot(input *ec2.CopySnapshotInput, natsConn *nats.Conn) (ec2.CopySnapshotOutput, error) {
|
||||
func CopySnapshot(input *ec2.CopySnapshotInput, natsConn *nats.Conn, accountID string) (ec2.CopySnapshotOutput, error) {
|
||||
var output ec2.CopySnapshotOutput
|
||||
|
||||
if err := ValidateCopySnapshotInput(input); err != nil {
|
||||
@@ -40,7 +40,7 @@ func CopySnapshot(input *ec2.CopySnapshotInput, natsConn *nats.Conn) (ec2.CopySn
|
||||
}
|
||||
|
||||
svc := handlers_ec2_snapshot.NewNATSSnapshotService(natsConn)
|
||||
result, err := svc.CopySnapshot(input)
|
||||
result, err := svc.CopySnapshot(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func ValidateCreateSnapshotInput(input *ec2.CreateSnapshotInput) error {
|
||||
}
|
||||
|
||||
// CreateSnapshot handles the EC2 CreateSnapshot API call
|
||||
func CreateSnapshot(input *ec2.CreateSnapshotInput, natsConn *nats.Conn) (ec2.Snapshot, error) {
|
||||
func CreateSnapshot(input *ec2.CreateSnapshotInput, natsConn *nats.Conn, accountID string) (ec2.Snapshot, error) {
|
||||
var output ec2.Snapshot
|
||||
|
||||
if err := ValidateCreateSnapshotInput(input); err != nil {
|
||||
@@ -36,7 +36,7 @@ func CreateSnapshot(input *ec2.CreateSnapshotInput, natsConn *nats.Conn) (ec2.Sn
|
||||
}
|
||||
|
||||
svc := handlers_ec2_snapshot.NewNATSSnapshotService(natsConn)
|
||||
result, err := svc.CreateSnapshot(input)
|
||||
result, err := svc.CreateSnapshot(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func ValidateDeleteSnapshotInput(input *ec2.DeleteSnapshotInput) error {
|
||||
}
|
||||
|
||||
// DeleteSnapshot handles the EC2 DeleteSnapshot API call
|
||||
func DeleteSnapshot(input *ec2.DeleteSnapshotInput, natsConn *nats.Conn) (ec2.DeleteSnapshotOutput, error) {
|
||||
func DeleteSnapshot(input *ec2.DeleteSnapshotInput, natsConn *nats.Conn, accountID string) (ec2.DeleteSnapshotOutput, error) {
|
||||
var output ec2.DeleteSnapshotOutput
|
||||
|
||||
if err := ValidateDeleteSnapshotInput(input); err != nil {
|
||||
@@ -36,7 +36,7 @@ func DeleteSnapshot(input *ec2.DeleteSnapshotInput, natsConn *nats.Conn) (ec2.De
|
||||
}
|
||||
|
||||
svc := handlers_ec2_snapshot.NewNATSSnapshotService(natsConn)
|
||||
result, err := svc.DeleteSnapshot(input)
|
||||
result, err := svc.DeleteSnapshot(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func ValidateDescribeSnapshotsInput(input *ec2.DescribeSnapshotsInput) error {
|
||||
}
|
||||
|
||||
// DescribeSnapshots handles the EC2 DescribeSnapshots API call
|
||||
func DescribeSnapshots(input *ec2.DescribeSnapshotsInput, natsConn *nats.Conn) (ec2.DescribeSnapshotsOutput, error) {
|
||||
func DescribeSnapshots(input *ec2.DescribeSnapshotsInput, natsConn *nats.Conn, accountID string) (ec2.DescribeSnapshotsOutput, error) {
|
||||
var output ec2.DescribeSnapshotsOutput
|
||||
|
||||
if err := ValidateDescribeSnapshotsInput(input); err != nil {
|
||||
@@ -35,7 +35,7 @@ func DescribeSnapshots(input *ec2.DescribeSnapshotsInput, natsConn *nats.Conn) (
|
||||
}
|
||||
|
||||
svc := handlers_ec2_snapshot.NewNATSSnapshotService(natsConn)
|
||||
result, err := svc.DescribeSnapshots(input)
|
||||
result, err := svc.DescribeSnapshots(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func ValidateCreateTagsInput(input *ec2.CreateTagsInput) error {
|
||||
}
|
||||
|
||||
// CreateTags handles the EC2 CreateTags API call
|
||||
func CreateTags(input *ec2.CreateTagsInput, natsConn *nats.Conn) (ec2.CreateTagsOutput, error) {
|
||||
func CreateTags(input *ec2.CreateTagsInput, natsConn *nats.Conn, accountID string) (ec2.CreateTagsOutput, error) {
|
||||
var output ec2.CreateTagsOutput
|
||||
|
||||
if err := ValidateCreateTagsInput(input); err != nil {
|
||||
@@ -42,7 +42,7 @@ func CreateTags(input *ec2.CreateTagsInput, natsConn *nats.Conn) (ec2.CreateTags
|
||||
}
|
||||
|
||||
svc := handlers_ec2_tags.NewNATSTagsService(natsConn)
|
||||
result, err := svc.CreateTags(input)
|
||||
result, err := svc.CreateTags(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func ValidateDeleteTagsInput(input *ec2.DeleteTagsInput) error {
|
||||
}
|
||||
|
||||
// DeleteTags handles the EC2 DeleteTags API call
|
||||
func DeleteTags(input *ec2.DeleteTagsInput, natsConn *nats.Conn) (ec2.DeleteTagsOutput, error) {
|
||||
func DeleteTags(input *ec2.DeleteTagsInput, natsConn *nats.Conn, accountID string) (ec2.DeleteTagsOutput, error) {
|
||||
var output ec2.DeleteTagsOutput
|
||||
|
||||
if err := ValidateDeleteTagsInput(input); err != nil {
|
||||
@@ -31,7 +31,7 @@ func DeleteTags(input *ec2.DeleteTagsInput, natsConn *nats.Conn) (ec2.DeleteTags
|
||||
}
|
||||
|
||||
svc := handlers_ec2_tags.NewNATSTagsService(natsConn)
|
||||
result, err := svc.DeleteTags(input)
|
||||
result, err := svc.DeleteTags(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
)
|
||||
|
||||
// DescribeTags handles the EC2 DescribeTags API call
|
||||
func DescribeTags(input *ec2.DescribeTagsInput, natsConn *nats.Conn) (ec2.DescribeTagsOutput, error) {
|
||||
func DescribeTags(input *ec2.DescribeTagsInput, natsConn *nats.Conn, accountID string) (ec2.DescribeTagsOutput, error) {
|
||||
// all input fields are optional filters
|
||||
var output ec2.DescribeTagsOutput
|
||||
|
||||
svc := handlers_ec2_tags.NewNATSTagsService(natsConn)
|
||||
result, err := svc.DescribeTags(input)
|
||||
result, err := svc.DescribeTags(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func ValidateAttachVolumeInput(input *ec2.AttachVolumeInput) error {
|
||||
}
|
||||
|
||||
// AttachVolume sends an attach-volume command to the daemon owning the instance
|
||||
func AttachVolume(input *ec2.AttachVolumeInput, natsConn *nats.Conn) (ec2.VolumeAttachment, error) {
|
||||
func AttachVolume(input *ec2.AttachVolumeInput, natsConn *nats.Conn, accountID string) (ec2.VolumeAttachment, error) {
|
||||
var output ec2.VolumeAttachment
|
||||
|
||||
if err := ValidateAttachVolumeInput(input); err != nil {
|
||||
@@ -66,7 +66,10 @@ func AttachVolume(input *ec2.AttachVolumeInput, natsConn *nats.Conn) (ec2.Volume
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("ec2.cmd.%s", instanceID)
|
||||
msg, err := natsConn.Request(subject, jsonData, 30*time.Second)
|
||||
reqMsg := nats.NewMsg(subject)
|
||||
reqMsg.Data = jsonData
|
||||
reqMsg.Header.Set(utils.AccountIDHeader, accountID)
|
||||
msg, err := natsConn.RequestMsg(reqMsg, 30*time.Second)
|
||||
if err != nil {
|
||||
slog.Error("AttachVolume: NATS request failed", "instanceId", instanceID, "volumeId", volumeID, "err", err)
|
||||
if errors.Is(err, nats.ErrNoResponders) {
|
||||
|
||||
@@ -32,7 +32,7 @@ func ValidateCreateVolumeInput(input *ec2.CreateVolumeInput) error {
|
||||
}
|
||||
|
||||
// CreateVolume handles the CreateVolume API call
|
||||
func CreateVolume(input *ec2.CreateVolumeInput, natsConn *nats.Conn) (ec2.Volume, error) {
|
||||
func CreateVolume(input *ec2.CreateVolumeInput, natsConn *nats.Conn, accountID string) (ec2.Volume, error) {
|
||||
var output ec2.Volume
|
||||
|
||||
err := ValidateCreateVolumeInput(input)
|
||||
@@ -41,7 +41,7 @@ func CreateVolume(input *ec2.CreateVolumeInput, natsConn *nats.Conn) (ec2.Volume
|
||||
}
|
||||
|
||||
volumeService := handlers_ec2_volume.NewNATSVolumeService(natsConn)
|
||||
result, err := volumeService.CreateVolume(input)
|
||||
result, err := volumeService.CreateVolume(input, accountID)
|
||||
|
||||
if err != nil {
|
||||
return output, err
|
||||
|
||||
@@ -24,7 +24,7 @@ func ValidateDeleteVolumeInput(input *ec2.DeleteVolumeInput) error {
|
||||
}
|
||||
|
||||
// DeleteVolume handles the DeleteVolume API call
|
||||
func DeleteVolume(input *ec2.DeleteVolumeInput, natsConn *nats.Conn) (ec2.DeleteVolumeOutput, error) {
|
||||
func DeleteVolume(input *ec2.DeleteVolumeInput, natsConn *nats.Conn, accountID string) (ec2.DeleteVolumeOutput, error) {
|
||||
var output ec2.DeleteVolumeOutput
|
||||
|
||||
err := ValidateDeleteVolumeInput(input)
|
||||
@@ -33,7 +33,7 @@ func DeleteVolume(input *ec2.DeleteVolumeInput, natsConn *nats.Conn) (ec2.Delete
|
||||
}
|
||||
|
||||
volumeService := handlers_ec2_volume.NewNATSVolumeService(natsConn)
|
||||
result, err := volumeService.DeleteVolume(input)
|
||||
result, err := volumeService.DeleteVolume(input, accountID)
|
||||
|
||||
if err != nil {
|
||||
return output, err
|
||||
|
||||
@@ -27,7 +27,7 @@ func ValidateDescribeVolumeStatusInput(input *ec2.DescribeVolumeStatusInput) err
|
||||
}
|
||||
|
||||
// DescribeVolumeStatus handles the DescribeVolumeStatus API call
|
||||
func DescribeVolumeStatus(input *ec2.DescribeVolumeStatusInput, natsConn *nats.Conn) (ec2.DescribeVolumeStatusOutput, error) {
|
||||
func DescribeVolumeStatus(input *ec2.DescribeVolumeStatusInput, natsConn *nats.Conn, accountID string) (ec2.DescribeVolumeStatusOutput, error) {
|
||||
var output ec2.DescribeVolumeStatusOutput
|
||||
|
||||
err := ValidateDescribeVolumeStatusInput(input)
|
||||
@@ -36,7 +36,7 @@ func DescribeVolumeStatus(input *ec2.DescribeVolumeStatusInput, natsConn *nats.C
|
||||
}
|
||||
|
||||
volumeService := handlers_ec2_volume.NewNATSVolumeService(natsConn)
|
||||
result, err := volumeService.DescribeVolumeStatus(input)
|
||||
result, err := volumeService.DescribeVolumeStatus(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestDescribeVolumeStatus_InputValidation(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := DescribeVolumeStatus(tt.input, nil)
|
||||
_, err := DescribeVolumeStatus(tt.input, nil, "")
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
|
||||
@@ -29,7 +29,7 @@ func ValidateDescribeVolumesInput(input *ec2.DescribeVolumesInput) error {
|
||||
}
|
||||
|
||||
// DescribeVolumes handles the DescribeVolumes API call
|
||||
func DescribeVolumes(input *ec2.DescribeVolumesInput, natsConn *nats.Conn) (ec2.DescribeVolumesOutput, error) {
|
||||
func DescribeVolumes(input *ec2.DescribeVolumesInput, natsConn *nats.Conn, accountID string) (ec2.DescribeVolumesOutput, error) {
|
||||
var output ec2.DescribeVolumesOutput
|
||||
|
||||
// Validate input
|
||||
@@ -40,7 +40,7 @@ func DescribeVolumes(input *ec2.DescribeVolumesInput, natsConn *nats.Conn) (ec2.
|
||||
|
||||
// Create NATS service and call handler
|
||||
volumeService := handlers_ec2_volume.NewNATSVolumeService(natsConn)
|
||||
result, err := volumeService.DescribeVolumes(input)
|
||||
result, err := volumeService.DescribeVolumes(input, accountID)
|
||||
|
||||
if err != nil {
|
||||
return output, err
|
||||
|
||||
@@ -29,7 +29,7 @@ func ValidateDetachVolumeInput(input *ec2.DetachVolumeInput) error {
|
||||
}
|
||||
|
||||
// DetachVolume sends a detach-volume command to the daemon owning the instance
|
||||
func DetachVolume(input *ec2.DetachVolumeInput, natsConn *nats.Conn) (ec2.VolumeAttachment, error) {
|
||||
func DetachVolume(input *ec2.DetachVolumeInput, natsConn *nats.Conn, accountID string) (ec2.VolumeAttachment, error) {
|
||||
var output ec2.VolumeAttachment
|
||||
|
||||
if err := ValidateDetachVolumeInput(input); err != nil {
|
||||
@@ -46,7 +46,7 @@ func DetachVolume(input *ec2.DetachVolumeInput, natsConn *nats.Conn) (ec2.Volume
|
||||
volSvc := handlers_ec2_volume.NewNATSVolumeService(natsConn)
|
||||
descOutput, err := volSvc.DescribeVolumes(&ec2.DescribeVolumesInput{
|
||||
VolumeIds: []*string{&volumeID},
|
||||
})
|
||||
}, accountID)
|
||||
if err != nil {
|
||||
slog.Error("DetachVolume: failed to describe volume", "volumeId", volumeID, "err", err)
|
||||
return output, errors.New(awserrors.ErrorInvalidVolumeNotFound)
|
||||
@@ -85,7 +85,10 @@ func DetachVolume(input *ec2.DetachVolumeInput, natsConn *nats.Conn) (ec2.Volume
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("ec2.cmd.%s", instanceID)
|
||||
msg, err := natsConn.Request(subject, jsonData, 30*time.Second)
|
||||
reqMsg := nats.NewMsg(subject)
|
||||
reqMsg.Data = jsonData
|
||||
reqMsg.Header.Set(utils.AccountIDHeader, accountID)
|
||||
msg, err := natsConn.RequestMsg(reqMsg, 30*time.Second)
|
||||
if err != nil {
|
||||
slog.Error("DetachVolume: NATS request failed", "instanceId", instanceID, "volumeId", volumeID, "err", err)
|
||||
if errors.Is(err, nats.ErrNoResponders) {
|
||||
|
||||
@@ -28,7 +28,7 @@ func ValidateModifyVolumeInput(input *ec2.ModifyVolumeInput) error {
|
||||
}
|
||||
|
||||
// ModifyVolume handles the ModifyVolume API call
|
||||
func ModifyVolume(input *ec2.ModifyVolumeInput, natsConn *nats.Conn) (ec2.ModifyVolumeOutput, error) {
|
||||
func ModifyVolume(input *ec2.ModifyVolumeInput, natsConn *nats.Conn, accountID string) (ec2.ModifyVolumeOutput, error) {
|
||||
var output ec2.ModifyVolumeOutput
|
||||
|
||||
err := ValidateModifyVolumeInput(input)
|
||||
@@ -37,7 +37,7 @@ func ModifyVolume(input *ec2.ModifyVolumeInput, natsConn *nats.Conn) (ec2.Modify
|
||||
}
|
||||
|
||||
volumeService := handlers_ec2_volume.NewNATSVolumeService(natsConn)
|
||||
result, err := volumeService.ModifyVolume(input)
|
||||
result, err := volumeService.ModifyVolume(input, accountID)
|
||||
|
||||
if err != nil {
|
||||
return output, err
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// CreateNetworkInterface handles the EC2 CreateNetworkInterface API call
|
||||
func CreateNetworkInterface(input *ec2.CreateNetworkInterfaceInput, natsConn *nats.Conn) (ec2.CreateNetworkInterfaceOutput, error) {
|
||||
func CreateNetworkInterface(input *ec2.CreateNetworkInterfaceInput, natsConn *nats.Conn, accountID string) (ec2.CreateNetworkInterfaceOutput, error) {
|
||||
var output ec2.CreateNetworkInterfaceOutput
|
||||
|
||||
if input == nil {
|
||||
@@ -21,7 +21,7 @@ func CreateNetworkInterface(input *ec2.CreateNetworkInterfaceInput, natsConn *na
|
||||
}
|
||||
|
||||
svc := handlers_ec2_vpc.NewNATSVPCService(natsConn)
|
||||
result, err := svc.CreateNetworkInterface(input)
|
||||
result, err := svc.CreateNetworkInterface(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// CreateSubnet handles the EC2 CreateSubnet API call
|
||||
func CreateSubnet(input *ec2.CreateSubnetInput, natsConn *nats.Conn) (ec2.CreateSubnetOutput, error) {
|
||||
func CreateSubnet(input *ec2.CreateSubnetInput, natsConn *nats.Conn, accountID string) (ec2.CreateSubnetOutput, error) {
|
||||
var output ec2.CreateSubnetOutput
|
||||
|
||||
if input == nil {
|
||||
@@ -24,7 +24,7 @@ func CreateSubnet(input *ec2.CreateSubnetInput, natsConn *nats.Conn) (ec2.Create
|
||||
}
|
||||
|
||||
svc := handlers_ec2_vpc.NewNATSVPCService(natsConn)
|
||||
result, err := svc.CreateSubnet(input)
|
||||
result, err := svc.CreateSubnet(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// CreateVpc handles the EC2 CreateVpc API call
|
||||
func CreateVpc(input *ec2.CreateVpcInput, natsConn *nats.Conn) (ec2.CreateVpcOutput, error) {
|
||||
func CreateVpc(input *ec2.CreateVpcInput, natsConn *nats.Conn, accountID string) (ec2.CreateVpcOutput, error) {
|
||||
var output ec2.CreateVpcOutput
|
||||
|
||||
if input == nil {
|
||||
@@ -21,7 +21,7 @@ func CreateVpc(input *ec2.CreateVpcInput, natsConn *nats.Conn) (ec2.CreateVpcOut
|
||||
}
|
||||
|
||||
svc := handlers_ec2_vpc.NewNATSVPCService(natsConn)
|
||||
result, err := svc.CreateVpc(input)
|
||||
result, err := svc.CreateVpc(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// DeleteNetworkInterface handles the EC2 DeleteNetworkInterface API call
|
||||
func DeleteNetworkInterface(input *ec2.DeleteNetworkInterfaceInput, natsConn *nats.Conn) (ec2.DeleteNetworkInterfaceOutput, error) {
|
||||
func DeleteNetworkInterface(input *ec2.DeleteNetworkInterfaceInput, natsConn *nats.Conn, accountID string) (ec2.DeleteNetworkInterfaceOutput, error) {
|
||||
var output ec2.DeleteNetworkInterfaceOutput
|
||||
|
||||
if input == nil {
|
||||
@@ -21,7 +21,7 @@ func DeleteNetworkInterface(input *ec2.DeleteNetworkInterfaceInput, natsConn *na
|
||||
}
|
||||
|
||||
svc := handlers_ec2_vpc.NewNATSVPCService(natsConn)
|
||||
result, err := svc.DeleteNetworkInterface(input)
|
||||
result, err := svc.DeleteNetworkInterface(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// DeleteSubnet handles the EC2 DeleteSubnet API call
|
||||
func DeleteSubnet(input *ec2.DeleteSubnetInput, natsConn *nats.Conn) (ec2.DeleteSubnetOutput, error) {
|
||||
func DeleteSubnet(input *ec2.DeleteSubnetInput, natsConn *nats.Conn, accountID string) (ec2.DeleteSubnetOutput, error) {
|
||||
var output ec2.DeleteSubnetOutput
|
||||
|
||||
if input == nil {
|
||||
@@ -21,7 +21,7 @@ func DeleteSubnet(input *ec2.DeleteSubnetInput, natsConn *nats.Conn) (ec2.Delete
|
||||
}
|
||||
|
||||
svc := handlers_ec2_vpc.NewNATSVPCService(natsConn)
|
||||
result, err := svc.DeleteSubnet(input)
|
||||
result, err := svc.DeleteSubnet(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// DeleteVpc handles the EC2 DeleteVpc API call
|
||||
func DeleteVpc(input *ec2.DeleteVpcInput, natsConn *nats.Conn) (ec2.DeleteVpcOutput, error) {
|
||||
func DeleteVpc(input *ec2.DeleteVpcInput, natsConn *nats.Conn, accountID string) (ec2.DeleteVpcOutput, error) {
|
||||
var output ec2.DeleteVpcOutput
|
||||
|
||||
if input == nil {
|
||||
@@ -21,7 +21,7 @@ func DeleteVpc(input *ec2.DeleteVpcInput, natsConn *nats.Conn) (ec2.DeleteVpcOut
|
||||
}
|
||||
|
||||
svc := handlers_ec2_vpc.NewNATSVPCService(natsConn)
|
||||
result, err := svc.DeleteVpc(input)
|
||||
result, err := svc.DeleteVpc(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
)
|
||||
|
||||
// DescribeNetworkInterfaces handles the EC2 DescribeNetworkInterfaces API call
|
||||
func DescribeNetworkInterfaces(input *ec2.DescribeNetworkInterfacesInput, natsConn *nats.Conn) (ec2.DescribeNetworkInterfacesOutput, error) {
|
||||
func DescribeNetworkInterfaces(input *ec2.DescribeNetworkInterfacesInput, natsConn *nats.Conn, accountID string) (ec2.DescribeNetworkInterfacesOutput, error) {
|
||||
svc := handlers_ec2_vpc.NewNATSVPCService(natsConn)
|
||||
result, err := svc.DescribeNetworkInterfaces(input)
|
||||
result, err := svc.DescribeNetworkInterfaces(input, accountID)
|
||||
if err != nil {
|
||||
return ec2.DescribeNetworkInterfacesOutput{}, err
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
)
|
||||
|
||||
// DescribeSubnets handles the EC2 DescribeSubnets API call
|
||||
func DescribeSubnets(input *ec2.DescribeSubnetsInput, natsConn *nats.Conn) (ec2.DescribeSubnetsOutput, error) {
|
||||
func DescribeSubnets(input *ec2.DescribeSubnetsInput, natsConn *nats.Conn, accountID string) (ec2.DescribeSubnetsOutput, error) {
|
||||
var output ec2.DescribeSubnetsOutput
|
||||
|
||||
svc := handlers_ec2_vpc.NewNATSVPCService(natsConn)
|
||||
result, err := svc.DescribeSubnets(input)
|
||||
result, err := svc.DescribeSubnets(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
)
|
||||
|
||||
// DescribeVpcs handles the EC2 DescribeVpcs API call
|
||||
func DescribeVpcs(input *ec2.DescribeVpcsInput, natsConn *nats.Conn) (ec2.DescribeVpcsOutput, error) {
|
||||
func DescribeVpcs(input *ec2.DescribeVpcsInput, natsConn *nats.Conn, accountID string) (ec2.DescribeVpcsOutput, error) {
|
||||
var output ec2.DescribeVpcsOutput
|
||||
|
||||
svc := handlers_ec2_vpc.NewNATSVPCService(natsConn)
|
||||
result, err := svc.DescribeVpcs(input)
|
||||
result, err := svc.DescribeVpcs(input, accountID)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
@@ -9,102 +9,104 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const testAccountID = "123456789012"
|
||||
|
||||
func TestCreateVpc_NilInput(t *testing.T) {
|
||||
_, err := CreateVpc(nil, nil)
|
||||
_, err := CreateVpc(nil, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestCreateVpc_NilCidrBlock(t *testing.T) {
|
||||
_, err := CreateVpc(&ec2.CreateVpcInput{}, nil)
|
||||
_, err := CreateVpc(&ec2.CreateVpcInput{}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestCreateVpc_EmptyCidrBlock(t *testing.T) {
|
||||
_, err := CreateVpc(&ec2.CreateVpcInput{CidrBlock: aws.String("")}, nil)
|
||||
_, err := CreateVpc(&ec2.CreateVpcInput{CidrBlock: aws.String("")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDeleteVpc_NilInput(t *testing.T) {
|
||||
_, err := DeleteVpc(nil, nil)
|
||||
_, err := DeleteVpc(nil, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestDeleteVpc_NilVpcId(t *testing.T) {
|
||||
_, err := DeleteVpc(&ec2.DeleteVpcInput{}, nil)
|
||||
_, err := DeleteVpc(&ec2.DeleteVpcInput{}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDeleteVpc_EmptyVpcId(t *testing.T) {
|
||||
_, err := DeleteVpc(&ec2.DeleteVpcInput{VpcId: aws.String("")}, nil)
|
||||
_, err := DeleteVpc(&ec2.DeleteVpcInput{VpcId: aws.String("")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestCreateSubnet_NilInput(t *testing.T) {
|
||||
_, err := CreateSubnet(nil, nil)
|
||||
_, err := CreateSubnet(nil, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestCreateSubnet_NilVpcId(t *testing.T) {
|
||||
_, err := CreateSubnet(&ec2.CreateSubnetInput{CidrBlock: aws.String("10.0.0.0/24")}, nil)
|
||||
_, err := CreateSubnet(&ec2.CreateSubnetInput{CidrBlock: aws.String("10.0.0.0/24")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestCreateSubnet_EmptyVpcId(t *testing.T) {
|
||||
_, err := CreateSubnet(&ec2.CreateSubnetInput{VpcId: aws.String(""), CidrBlock: aws.String("10.0.0.0/24")}, nil)
|
||||
_, err := CreateSubnet(&ec2.CreateSubnetInput{VpcId: aws.String(""), CidrBlock: aws.String("10.0.0.0/24")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestCreateSubnet_NilCidrBlock(t *testing.T) {
|
||||
_, err := CreateSubnet(&ec2.CreateSubnetInput{VpcId: aws.String("vpc-123")}, nil)
|
||||
_, err := CreateSubnet(&ec2.CreateSubnetInput{VpcId: aws.String("vpc-123")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestCreateSubnet_EmptyCidrBlock(t *testing.T) {
|
||||
_, err := CreateSubnet(&ec2.CreateSubnetInput{VpcId: aws.String("vpc-123"), CidrBlock: aws.String("")}, nil)
|
||||
_, err := CreateSubnet(&ec2.CreateSubnetInput{VpcId: aws.String("vpc-123"), CidrBlock: aws.String("")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDeleteSubnet_NilInput(t *testing.T) {
|
||||
_, err := DeleteSubnet(nil, nil)
|
||||
_, err := DeleteSubnet(nil, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestDeleteSubnet_NilSubnetId(t *testing.T) {
|
||||
_, err := DeleteSubnet(&ec2.DeleteSubnetInput{}, nil)
|
||||
_, err := DeleteSubnet(&ec2.DeleteSubnetInput{}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDeleteSubnet_EmptySubnetId(t *testing.T) {
|
||||
_, err := DeleteSubnet(&ec2.DeleteSubnetInput{SubnetId: aws.String("")}, nil)
|
||||
_, err := DeleteSubnet(&ec2.DeleteSubnetInput{SubnetId: aws.String("")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestCreateNetworkInterface_NilInput(t *testing.T) {
|
||||
_, err := CreateNetworkInterface(nil, nil)
|
||||
_, err := CreateNetworkInterface(nil, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestCreateNetworkInterface_NilSubnetId(t *testing.T) {
|
||||
_, err := CreateNetworkInterface(&ec2.CreateNetworkInterfaceInput{}, nil)
|
||||
_, err := CreateNetworkInterface(&ec2.CreateNetworkInterfaceInput{}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestCreateNetworkInterface_EmptySubnetId(t *testing.T) {
|
||||
_, err := CreateNetworkInterface(&ec2.CreateNetworkInterfaceInput{SubnetId: aws.String("")}, nil)
|
||||
_, err := CreateNetworkInterface(&ec2.CreateNetworkInterfaceInput{SubnetId: aws.String("")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDeleteNetworkInterface_NilInput(t *testing.T) {
|
||||
_, err := DeleteNetworkInterface(nil, nil)
|
||||
_, err := DeleteNetworkInterface(nil, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
func TestDeleteNetworkInterface_NilNetworkInterfaceId(t *testing.T) {
|
||||
_, err := DeleteNetworkInterface(&ec2.DeleteNetworkInterfaceInput{}, nil)
|
||||
_, err := DeleteNetworkInterface(&ec2.DeleteNetworkInterfaceInput{}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
func TestDeleteNetworkInterface_EmptyNetworkInterfaceId(t *testing.T) {
|
||||
_, err := DeleteNetworkInterface(&ec2.DeleteNetworkInterfaceInput{NetworkInterfaceId: aws.String("")}, nil)
|
||||
_, err := DeleteNetworkInterface(&ec2.DeleteNetworkInterfaceInput{NetworkInterfaceId: aws.String("")}, nil, testAccountID)
|
||||
assert.EqualError(t, err, awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import "github.com/aws/aws-sdk-go/service/ec2"
|
||||
|
||||
// AccountSettingsService defines the interface for EC2 account-level settings
|
||||
type AccountSettingsService interface {
|
||||
EnableEbsEncryptionByDefault(input *ec2.EnableEbsEncryptionByDefaultInput) (*ec2.EnableEbsEncryptionByDefaultOutput, error)
|
||||
DisableEbsEncryptionByDefault(input *ec2.DisableEbsEncryptionByDefaultInput) (*ec2.DisableEbsEncryptionByDefaultOutput, error)
|
||||
GetEbsEncryptionByDefault(input *ec2.GetEbsEncryptionByDefaultInput) (*ec2.GetEbsEncryptionByDefaultOutput, error)
|
||||
GetSerialConsoleAccessStatus(input *ec2.GetSerialConsoleAccessStatusInput) (*ec2.GetSerialConsoleAccessStatusOutput, error)
|
||||
EnableSerialConsoleAccess(input *ec2.EnableSerialConsoleAccessInput) (*ec2.EnableSerialConsoleAccessOutput, error)
|
||||
DisableSerialConsoleAccess(input *ec2.DisableSerialConsoleAccessInput) (*ec2.DisableSerialConsoleAccessOutput, error)
|
||||
EnableEbsEncryptionByDefault(input *ec2.EnableEbsEncryptionByDefaultInput, accountID string) (*ec2.EnableEbsEncryptionByDefaultOutput, error)
|
||||
DisableEbsEncryptionByDefault(input *ec2.DisableEbsEncryptionByDefaultInput, accountID string) (*ec2.DisableEbsEncryptionByDefaultOutput, error)
|
||||
GetEbsEncryptionByDefault(input *ec2.GetEbsEncryptionByDefaultInput, accountID string) (*ec2.GetEbsEncryptionByDefaultOutput, error)
|
||||
GetSerialConsoleAccessStatus(input *ec2.GetSerialConsoleAccessStatusInput, accountID string) (*ec2.GetSerialConsoleAccessStatusOutput, error)
|
||||
EnableSerialConsoleAccess(input *ec2.EnableSerialConsoleAccessInput, accountID string) (*ec2.EnableSerialConsoleAccessOutput, error)
|
||||
DisableSerialConsoleAccess(input *ec2.DisableSerialConsoleAccessInput, accountID string) (*ec2.DisableSerialConsoleAccessOutput, error)
|
||||
}
|
||||
|
||||
@@ -67,9 +67,18 @@ func getOrCreateKVBucket(js nats.JetStreamContext, bucketName string, history in
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
// settingsKey returns the per-account KV key for storing settings.
|
||||
// Falls back to "default" for pre-Phase-4 resources with no accountID.
|
||||
func settingsKey(accountID string) string {
|
||||
if accountID == "" {
|
||||
return "default"
|
||||
}
|
||||
return accountID
|
||||
}
|
||||
|
||||
// getSettings retrieves current account settings
|
||||
func (s *AccountSettingsServiceImpl) getSettings() (*AccountSettingsRecord, error) {
|
||||
entry, err := s.settingsKV.Get("default")
|
||||
func (s *AccountSettingsServiceImpl) getSettings(accountID string) (*AccountSettingsRecord, error) {
|
||||
entry, err := s.settingsKV.Get(settingsKey(accountID))
|
||||
if err != nil {
|
||||
if errors.Is(err, nats.ErrKeyNotFound) {
|
||||
return &AccountSettingsRecord{
|
||||
@@ -89,28 +98,28 @@ func (s *AccountSettingsServiceImpl) getSettings() (*AccountSettingsRecord, erro
|
||||
}
|
||||
|
||||
// saveSettings saves current account settings
|
||||
func (s *AccountSettingsServiceImpl) saveSettings(settings *AccountSettingsRecord) error {
|
||||
func (s *AccountSettingsServiceImpl) saveSettings(settings *AccountSettingsRecord, accountID string) error {
|
||||
data, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = s.settingsKV.Put("default", data)
|
||||
_, err = s.settingsKV.Put(settingsKey(accountID), data)
|
||||
return err
|
||||
}
|
||||
|
||||
// EnableEbsEncryptionByDefault enables EBS encryption by default for the account
|
||||
func (s *AccountSettingsServiceImpl) EnableEbsEncryptionByDefault(input *ec2.EnableEbsEncryptionByDefaultInput) (*ec2.EnableEbsEncryptionByDefaultOutput, error) {
|
||||
slog.Info("EnableEbsEncryptionByDefault called")
|
||||
func (s *AccountSettingsServiceImpl) EnableEbsEncryptionByDefault(input *ec2.EnableEbsEncryptionByDefaultInput, accountID string) (*ec2.EnableEbsEncryptionByDefaultOutput, error) {
|
||||
slog.Info("EnableEbsEncryptionByDefault called", "accountID", accountID)
|
||||
|
||||
settings, err := s.getSettings()
|
||||
settings, err := s.getSettings(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settings.EbsEncryptionByDefault = true
|
||||
|
||||
if err := s.saveSettings(settings); err != nil {
|
||||
if err := s.saveSettings(settings, accountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -120,17 +129,17 @@ func (s *AccountSettingsServiceImpl) EnableEbsEncryptionByDefault(input *ec2.Ena
|
||||
}
|
||||
|
||||
// DisableEbsEncryptionByDefault disables EBS encryption by default for the account
|
||||
func (s *AccountSettingsServiceImpl) DisableEbsEncryptionByDefault(input *ec2.DisableEbsEncryptionByDefaultInput) (*ec2.DisableEbsEncryptionByDefaultOutput, error) {
|
||||
slog.Info("DisableEbsEncryptionByDefault called")
|
||||
func (s *AccountSettingsServiceImpl) DisableEbsEncryptionByDefault(input *ec2.DisableEbsEncryptionByDefaultInput, accountID string) (*ec2.DisableEbsEncryptionByDefaultOutput, error) {
|
||||
slog.Info("DisableEbsEncryptionByDefault called", "accountID", accountID)
|
||||
|
||||
settings, err := s.getSettings()
|
||||
settings, err := s.getSettings(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settings.EbsEncryptionByDefault = false
|
||||
|
||||
if err := s.saveSettings(settings); err != nil {
|
||||
if err := s.saveSettings(settings, accountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -140,10 +149,10 @@ func (s *AccountSettingsServiceImpl) DisableEbsEncryptionByDefault(input *ec2.Di
|
||||
}
|
||||
|
||||
// GetEbsEncryptionByDefault gets the current EBS encryption by default setting
|
||||
func (s *AccountSettingsServiceImpl) GetEbsEncryptionByDefault(input *ec2.GetEbsEncryptionByDefaultInput) (*ec2.GetEbsEncryptionByDefaultOutput, error) {
|
||||
slog.Info("GetEbsEncryptionByDefault called")
|
||||
func (s *AccountSettingsServiceImpl) GetEbsEncryptionByDefault(input *ec2.GetEbsEncryptionByDefaultInput, accountID string) (*ec2.GetEbsEncryptionByDefaultOutput, error) {
|
||||
slog.Info("GetEbsEncryptionByDefault called", "accountID", accountID)
|
||||
|
||||
settings, err := s.getSettings()
|
||||
settings, err := s.getSettings(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -154,10 +163,10 @@ func (s *AccountSettingsServiceImpl) GetEbsEncryptionByDefault(input *ec2.GetEbs
|
||||
}
|
||||
|
||||
// GetSerialConsoleAccessStatus gets the current serial console access status
|
||||
func (s *AccountSettingsServiceImpl) GetSerialConsoleAccessStatus(input *ec2.GetSerialConsoleAccessStatusInput) (*ec2.GetSerialConsoleAccessStatusOutput, error) {
|
||||
slog.Info("GetSerialConsoleAccessStatus called")
|
||||
func (s *AccountSettingsServiceImpl) GetSerialConsoleAccessStatus(input *ec2.GetSerialConsoleAccessStatusInput, accountID string) (*ec2.GetSerialConsoleAccessStatusOutput, error) {
|
||||
slog.Info("GetSerialConsoleAccessStatus called", "accountID", accountID)
|
||||
|
||||
settings, err := s.getSettings()
|
||||
settings, err := s.getSettings(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -168,17 +177,17 @@ func (s *AccountSettingsServiceImpl) GetSerialConsoleAccessStatus(input *ec2.Get
|
||||
}
|
||||
|
||||
// EnableSerialConsoleAccess enables serial console access for the account
|
||||
func (s *AccountSettingsServiceImpl) EnableSerialConsoleAccess(input *ec2.EnableSerialConsoleAccessInput) (*ec2.EnableSerialConsoleAccessOutput, error) {
|
||||
slog.Info("EnableSerialConsoleAccess called")
|
||||
func (s *AccountSettingsServiceImpl) EnableSerialConsoleAccess(input *ec2.EnableSerialConsoleAccessInput, accountID string) (*ec2.EnableSerialConsoleAccessOutput, error) {
|
||||
slog.Info("EnableSerialConsoleAccess called", "accountID", accountID)
|
||||
|
||||
settings, err := s.getSettings()
|
||||
settings, err := s.getSettings(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settings.SerialConsoleAccess = true
|
||||
|
||||
if err := s.saveSettings(settings); err != nil {
|
||||
if err := s.saveSettings(settings, accountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -188,17 +197,17 @@ func (s *AccountSettingsServiceImpl) EnableSerialConsoleAccess(input *ec2.Enable
|
||||
}
|
||||
|
||||
// DisableSerialConsoleAccess disables serial console access for the account
|
||||
func (s *AccountSettingsServiceImpl) DisableSerialConsoleAccess(input *ec2.DisableSerialConsoleAccessInput) (*ec2.DisableSerialConsoleAccessOutput, error) {
|
||||
slog.Info("DisableSerialConsoleAccess called")
|
||||
func (s *AccountSettingsServiceImpl) DisableSerialConsoleAccess(input *ec2.DisableSerialConsoleAccessInput, accountID string) (*ec2.DisableSerialConsoleAccessOutput, error) {
|
||||
slog.Info("DisableSerialConsoleAccess called", "accountID", accountID)
|
||||
|
||||
settings, err := s.getSettings()
|
||||
settings, err := s.getSettings(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settings.SerialConsoleAccess = false
|
||||
|
||||
if err := s.saveSettings(settings); err != nil {
|
||||
if err := s.saveSettings(settings, accountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -62,11 +62,13 @@ func setupTestAccountService(t *testing.T) *AccountSettingsServiceImpl {
|
||||
return svc
|
||||
}
|
||||
|
||||
const testAccountID = "111111111111"
|
||||
|
||||
// EBS Encryption tests
|
||||
|
||||
func TestEbsEncryption_DefaultOff(t *testing.T) {
|
||||
svc := setupTestAccountService(t)
|
||||
out, err := svc.GetEbsEncryptionByDefault(&ec2.GetEbsEncryptionByDefaultInput{})
|
||||
out, err := svc.GetEbsEncryptionByDefault(&ec2.GetEbsEncryptionByDefaultInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, *out.EbsEncryptionByDefault)
|
||||
}
|
||||
@@ -75,22 +77,22 @@ func TestEbsEncryption_EnableDisable(t *testing.T) {
|
||||
svc := setupTestAccountService(t)
|
||||
|
||||
// Enable
|
||||
enableOut, err := svc.EnableEbsEncryptionByDefault(&ec2.EnableEbsEncryptionByDefaultInput{})
|
||||
enableOut, err := svc.EnableEbsEncryptionByDefault(&ec2.EnableEbsEncryptionByDefaultInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, *enableOut.EbsEncryptionByDefault)
|
||||
|
||||
// Verify
|
||||
getOut, err := svc.GetEbsEncryptionByDefault(&ec2.GetEbsEncryptionByDefaultInput{})
|
||||
getOut, err := svc.GetEbsEncryptionByDefault(&ec2.GetEbsEncryptionByDefaultInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, *getOut.EbsEncryptionByDefault)
|
||||
|
||||
// Disable
|
||||
disableOut, err := svc.DisableEbsEncryptionByDefault(&ec2.DisableEbsEncryptionByDefaultInput{})
|
||||
disableOut, err := svc.DisableEbsEncryptionByDefault(&ec2.DisableEbsEncryptionByDefaultInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, *disableOut.EbsEncryptionByDefault)
|
||||
|
||||
// Verify
|
||||
getOut, err = svc.GetEbsEncryptionByDefault(&ec2.GetEbsEncryptionByDefaultInput{})
|
||||
getOut, err = svc.GetEbsEncryptionByDefault(&ec2.GetEbsEncryptionByDefaultInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, *getOut.EbsEncryptionByDefault)
|
||||
}
|
||||
@@ -99,7 +101,7 @@ func TestEbsEncryption_EnableDisable(t *testing.T) {
|
||||
|
||||
func TestSerialConsole_DefaultOff(t *testing.T) {
|
||||
svc := setupTestAccountService(t)
|
||||
out, err := svc.GetSerialConsoleAccessStatus(&ec2.GetSerialConsoleAccessStatusInput{})
|
||||
out, err := svc.GetSerialConsoleAccessStatus(&ec2.GetSerialConsoleAccessStatusInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, *out.SerialConsoleAccessEnabled)
|
||||
}
|
||||
@@ -107,19 +109,66 @@ func TestSerialConsole_DefaultOff(t *testing.T) {
|
||||
func TestSerialConsole_EnableDisable(t *testing.T) {
|
||||
svc := setupTestAccountService(t)
|
||||
|
||||
enableOut, err := svc.EnableSerialConsoleAccess(&ec2.EnableSerialConsoleAccessInput{})
|
||||
enableOut, err := svc.EnableSerialConsoleAccess(&ec2.EnableSerialConsoleAccessInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, *enableOut.SerialConsoleAccessEnabled)
|
||||
|
||||
getOut, err := svc.GetSerialConsoleAccessStatus(&ec2.GetSerialConsoleAccessStatusInput{})
|
||||
getOut, err := svc.GetSerialConsoleAccessStatus(&ec2.GetSerialConsoleAccessStatusInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, *getOut.SerialConsoleAccessEnabled)
|
||||
|
||||
disableOut, err := svc.DisableSerialConsoleAccess(&ec2.DisableSerialConsoleAccessInput{})
|
||||
disableOut, err := svc.DisableSerialConsoleAccess(&ec2.DisableSerialConsoleAccessInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, *disableOut.SerialConsoleAccessEnabled)
|
||||
|
||||
getOut, err = svc.GetSerialConsoleAccessStatus(&ec2.GetSerialConsoleAccessStatusInput{})
|
||||
getOut, err = svc.GetSerialConsoleAccessStatus(&ec2.GetSerialConsoleAccessStatusInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, *getOut.SerialConsoleAccessEnabled)
|
||||
}
|
||||
|
||||
// Multi-account isolation tests
|
||||
|
||||
func TestEbsEncryption_MultiAccountIsolation(t *testing.T) {
|
||||
svc := setupTestAccountService(t)
|
||||
accountA := "111111111111"
|
||||
accountB := "222222222222"
|
||||
|
||||
// Enable EBS encryption for account A only
|
||||
_, err := svc.EnableEbsEncryptionByDefault(&ec2.EnableEbsEncryptionByDefaultInput{}, accountA)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Account A should have encryption enabled
|
||||
outA, err := svc.GetEbsEncryptionByDefault(&ec2.GetEbsEncryptionByDefaultInput{}, accountA)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, *outA.EbsEncryptionByDefault)
|
||||
|
||||
// Account B should still have encryption disabled (default)
|
||||
outB, err := svc.GetEbsEncryptionByDefault(&ec2.GetEbsEncryptionByDefaultInput{}, accountB)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, *outB.EbsEncryptionByDefault)
|
||||
}
|
||||
|
||||
func TestSerialConsole_MultiAccountIsolation(t *testing.T) {
|
||||
svc := setupTestAccountService(t)
|
||||
accountA := "111111111111"
|
||||
accountB := "222222222222"
|
||||
|
||||
// Enable serial console for account A only
|
||||
_, err := svc.EnableSerialConsoleAccess(&ec2.EnableSerialConsoleAccessInput{}, accountA)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Account A should have serial console enabled
|
||||
outA, err := svc.GetSerialConsoleAccessStatus(&ec2.GetSerialConsoleAccessStatusInput{}, accountA)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, *outA.SerialConsoleAccessEnabled)
|
||||
|
||||
// Account B should still have serial console disabled (default)
|
||||
outB, err := svc.GetSerialConsoleAccessStatus(&ec2.GetSerialConsoleAccessStatusInput{}, accountB)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, *outB.SerialConsoleAccessEnabled)
|
||||
}
|
||||
|
||||
func TestSettingsKey_EmptyAccountIDFallback(t *testing.T) {
|
||||
assert.Equal(t, "default", settingsKey(""))
|
||||
assert.Equal(t, "123456789012", settingsKey("123456789012"))
|
||||
}
|
||||
|
||||
@@ -18,26 +18,26 @@ func NewNATSAccountSettingsService(natsConn *nats.Conn) AccountSettingsService {
|
||||
return &NATSAccountSettingsService{natsConn: natsConn}
|
||||
}
|
||||
|
||||
func (s *NATSAccountSettingsService) EnableEbsEncryptionByDefault(input *ec2.EnableEbsEncryptionByDefaultInput) (*ec2.EnableEbsEncryptionByDefaultOutput, error) {
|
||||
return utils.NATSRequest[ec2.EnableEbsEncryptionByDefaultOutput](s.natsConn, "ec2.EnableEbsEncryptionByDefault", input, 30*time.Second)
|
||||
func (s *NATSAccountSettingsService) EnableEbsEncryptionByDefault(input *ec2.EnableEbsEncryptionByDefaultInput, accountID string) (*ec2.EnableEbsEncryptionByDefaultOutput, error) {
|
||||
return utils.NATSRequest[ec2.EnableEbsEncryptionByDefaultOutput](s.natsConn, "ec2.EnableEbsEncryptionByDefault", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSAccountSettingsService) DisableEbsEncryptionByDefault(input *ec2.DisableEbsEncryptionByDefaultInput) (*ec2.DisableEbsEncryptionByDefaultOutput, error) {
|
||||
return utils.NATSRequest[ec2.DisableEbsEncryptionByDefaultOutput](s.natsConn, "ec2.DisableEbsEncryptionByDefault", input, 30*time.Second)
|
||||
func (s *NATSAccountSettingsService) DisableEbsEncryptionByDefault(input *ec2.DisableEbsEncryptionByDefaultInput, accountID string) (*ec2.DisableEbsEncryptionByDefaultOutput, error) {
|
||||
return utils.NATSRequest[ec2.DisableEbsEncryptionByDefaultOutput](s.natsConn, "ec2.DisableEbsEncryptionByDefault", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSAccountSettingsService) GetEbsEncryptionByDefault(input *ec2.GetEbsEncryptionByDefaultInput) (*ec2.GetEbsEncryptionByDefaultOutput, error) {
|
||||
return utils.NATSRequest[ec2.GetEbsEncryptionByDefaultOutput](s.natsConn, "ec2.GetEbsEncryptionByDefault", input, 30*time.Second)
|
||||
func (s *NATSAccountSettingsService) GetEbsEncryptionByDefault(input *ec2.GetEbsEncryptionByDefaultInput, accountID string) (*ec2.GetEbsEncryptionByDefaultOutput, error) {
|
||||
return utils.NATSRequest[ec2.GetEbsEncryptionByDefaultOutput](s.natsConn, "ec2.GetEbsEncryptionByDefault", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSAccountSettingsService) GetSerialConsoleAccessStatus(input *ec2.GetSerialConsoleAccessStatusInput) (*ec2.GetSerialConsoleAccessStatusOutput, error) {
|
||||
return utils.NATSRequest[ec2.GetSerialConsoleAccessStatusOutput](s.natsConn, "ec2.GetSerialConsoleAccessStatus", input, 30*time.Second)
|
||||
func (s *NATSAccountSettingsService) GetSerialConsoleAccessStatus(input *ec2.GetSerialConsoleAccessStatusInput, accountID string) (*ec2.GetSerialConsoleAccessStatusOutput, error) {
|
||||
return utils.NATSRequest[ec2.GetSerialConsoleAccessStatusOutput](s.natsConn, "ec2.GetSerialConsoleAccessStatus", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSAccountSettingsService) EnableSerialConsoleAccess(input *ec2.EnableSerialConsoleAccessInput) (*ec2.EnableSerialConsoleAccessOutput, error) {
|
||||
return utils.NATSRequest[ec2.EnableSerialConsoleAccessOutput](s.natsConn, "ec2.EnableSerialConsoleAccess", input, 30*time.Second)
|
||||
func (s *NATSAccountSettingsService) EnableSerialConsoleAccess(input *ec2.EnableSerialConsoleAccessInput, accountID string) (*ec2.EnableSerialConsoleAccessOutput, error) {
|
||||
return utils.NATSRequest[ec2.EnableSerialConsoleAccessOutput](s.natsConn, "ec2.EnableSerialConsoleAccess", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSAccountSettingsService) DisableSerialConsoleAccess(input *ec2.DisableSerialConsoleAccessInput) (*ec2.DisableSerialConsoleAccessOutput, error) {
|
||||
return utils.NATSRequest[ec2.DisableSerialConsoleAccessOutput](s.natsConn, "ec2.DisableSerialConsoleAccess", input, 30*time.Second)
|
||||
func (s *NATSAccountSettingsService) DisableSerialConsoleAccess(input *ec2.DisableSerialConsoleAccessInput, accountID string) (*ec2.DisableSerialConsoleAccessOutput, error) {
|
||||
return utils.NATSRequest[ec2.DisableSerialConsoleAccessOutput](s.natsConn, "ec2.DisableSerialConsoleAccess", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import "github.com/aws/aws-sdk-go/service/ec2"
|
||||
|
||||
// EgressOnlyIGWService defines the interface for Egress-only Internet Gateway operations
|
||||
type EgressOnlyIGWService interface {
|
||||
CreateEgressOnlyInternetGateway(input *ec2.CreateEgressOnlyInternetGatewayInput) (*ec2.CreateEgressOnlyInternetGatewayOutput, error)
|
||||
DeleteEgressOnlyInternetGateway(input *ec2.DeleteEgressOnlyInternetGatewayInput) (*ec2.DeleteEgressOnlyInternetGatewayOutput, error)
|
||||
DescribeEgressOnlyInternetGateways(input *ec2.DescribeEgressOnlyInternetGatewaysInput) (*ec2.DescribeEgressOnlyInternetGatewaysOutput, error)
|
||||
CreateEgressOnlyInternetGateway(input *ec2.CreateEgressOnlyInternetGatewayInput, accountID string) (*ec2.CreateEgressOnlyInternetGatewayOutput, error)
|
||||
DeleteEgressOnlyInternetGateway(input *ec2.DeleteEgressOnlyInternetGatewayInput, accountID string) (*ec2.DeleteEgressOnlyInternetGatewayOutput, error)
|
||||
DescribeEgressOnlyInternetGateways(input *ec2.DescribeEgressOnlyInternetGatewaysInput, accountID string) (*ec2.DescribeEgressOnlyInternetGatewaysOutput, error)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
var _ EgressOnlyIGWService = (*EgressOnlyIGWServiceImpl)(nil)
|
||||
|
||||
const KVBucketEgressOnlyIGW = "hive-eigw"
|
||||
const kvBucketVPCs = "hive-vpc-vpcs"
|
||||
|
||||
// EgressOnlyIGWRecord represents a stored Egress-only Internet Gateway
|
||||
type EgressOnlyIGWRecord struct {
|
||||
@@ -33,6 +35,7 @@ type EgressOnlyIGWRecord struct {
|
||||
type EgressOnlyIGWServiceImpl struct {
|
||||
config *config.Config
|
||||
eigwKV nats.KeyValue
|
||||
vpcKV nats.KeyValue
|
||||
}
|
||||
|
||||
// NewEgressOnlyIGWServiceImpl creates a new Egress-only Internet Gateway service implementation
|
||||
@@ -54,11 +57,18 @@ func NewEgressOnlyIGWServiceImplWithNATS(cfg *config.Config, natsConn *nats.Conn
|
||||
return nil, fmt.Errorf("failed to create KV bucket %s: %w", KVBucketEgressOnlyIGW, err)
|
||||
}
|
||||
|
||||
// Get VPC KV bucket for cross-resource ownership validation
|
||||
vpcKV, err := js.KeyValue(kvBucketVPCs)
|
||||
if err != nil {
|
||||
slog.Warn("EIGW service: VPC KV bucket not available, VPC ownership checks disabled", "error", err)
|
||||
}
|
||||
|
||||
slog.Info("Egress-only IGW service initialized with JetStream KV", "bucket", KVBucketEgressOnlyIGW)
|
||||
|
||||
return &EgressOnlyIGWServiceImpl{
|
||||
config: cfg,
|
||||
eigwKV: eigwKV,
|
||||
vpcKV: vpcKV,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -76,12 +86,24 @@ func getOrCreateKVBucket(js nats.JetStreamContext, bucketName string, history in
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
func accountKey(accountID, resourceID string) string {
|
||||
return accountID + "." + resourceID
|
||||
}
|
||||
|
||||
// CreateEgressOnlyInternetGateway creates a new Egress-only Internet Gateway
|
||||
func (s *EgressOnlyIGWServiceImpl) CreateEgressOnlyInternetGateway(input *ec2.CreateEgressOnlyInternetGatewayInput) (*ec2.CreateEgressOnlyInternetGatewayOutput, error) {
|
||||
func (s *EgressOnlyIGWServiceImpl) CreateEgressOnlyInternetGateway(input *ec2.CreateEgressOnlyInternetGatewayInput, accountID string) (*ec2.CreateEgressOnlyInternetGatewayOutput, error) {
|
||||
if input.VpcId == nil || *input.VpcId == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
// Verify the caller owns the target VPC
|
||||
if s.vpcKV != nil {
|
||||
if _, err := s.vpcKV.Get(accountKey(accountID, *input.VpcId)); err != nil {
|
||||
slog.Warn("CreateEgressOnlyInternetGateway: VPC not found for account", "vpcId", *input.VpcId, "accountID", accountID)
|
||||
return nil, errors.New(awserrors.ErrorInvalidVpcIDNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
eigwID := utils.GenerateResourceID("eigw")
|
||||
|
||||
record := EgressOnlyIGWRecord{
|
||||
@@ -106,11 +128,11 @@ func (s *EgressOnlyIGWServiceImpl) CreateEgressOnlyInternetGateway(input *ec2.Cr
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal Egress-only IGW record: %w", err)
|
||||
}
|
||||
if _, err := s.eigwKV.Put(eigwID, data); err != nil {
|
||||
if _, err := s.eigwKV.Put(accountKey(accountID, eigwID), data); err != nil {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
slog.Info("CreateEgressOnlyInternetGateway completed", "egressOnlyInternetGatewayId", eigwID, "vpcId", record.VpcId)
|
||||
slog.Info("CreateEgressOnlyInternetGateway completed", "egressOnlyInternetGatewayId", eigwID, "vpcId", record.VpcId, "accountID", accountID)
|
||||
|
||||
return &ec2.CreateEgressOnlyInternetGatewayOutput{
|
||||
EgressOnlyInternetGateway: s.recordToEC2(&record),
|
||||
@@ -118,22 +140,23 @@ func (s *EgressOnlyIGWServiceImpl) CreateEgressOnlyInternetGateway(input *ec2.Cr
|
||||
}
|
||||
|
||||
// DeleteEgressOnlyInternetGateway deletes an Egress-only Internet Gateway
|
||||
func (s *EgressOnlyIGWServiceImpl) DeleteEgressOnlyInternetGateway(input *ec2.DeleteEgressOnlyInternetGatewayInput) (*ec2.DeleteEgressOnlyInternetGatewayOutput, error) {
|
||||
func (s *EgressOnlyIGWServiceImpl) DeleteEgressOnlyInternetGateway(input *ec2.DeleteEgressOnlyInternetGatewayInput, accountID string) (*ec2.DeleteEgressOnlyInternetGatewayOutput, error) {
|
||||
if input.EgressOnlyInternetGatewayId == nil || *input.EgressOnlyInternetGatewayId == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
eigwID := *input.EgressOnlyInternetGatewayId
|
||||
key := accountKey(accountID, eigwID)
|
||||
|
||||
if _, err := s.eigwKV.Get(eigwID); err != nil {
|
||||
if _, err := s.eigwKV.Get(key); err != nil {
|
||||
return nil, errors.New(awserrors.ErrorInvalidEgressOnlyInternetGatewayIdNotFound)
|
||||
}
|
||||
|
||||
if err := s.eigwKV.Delete(eigwID); err != nil {
|
||||
if err := s.eigwKV.Delete(key); err != nil {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
slog.Info("DeleteEgressOnlyInternetGateway completed", "egressOnlyInternetGatewayId", eigwID)
|
||||
slog.Info("DeleteEgressOnlyInternetGateway completed", "egressOnlyInternetGatewayId", eigwID, "accountID", accountID)
|
||||
|
||||
return &ec2.DeleteEgressOnlyInternetGatewayOutput{
|
||||
ReturnCode: aws.Bool(true),
|
||||
@@ -141,7 +164,7 @@ func (s *EgressOnlyIGWServiceImpl) DeleteEgressOnlyInternetGateway(input *ec2.De
|
||||
}
|
||||
|
||||
// DescribeEgressOnlyInternetGateways describes Egress-only Internet Gateways
|
||||
func (s *EgressOnlyIGWServiceImpl) DescribeEgressOnlyInternetGateways(input *ec2.DescribeEgressOnlyInternetGatewaysInput) (*ec2.DescribeEgressOnlyInternetGatewaysOutput, error) {
|
||||
func (s *EgressOnlyIGWServiceImpl) DescribeEgressOnlyInternetGateways(input *ec2.DescribeEgressOnlyInternetGatewaysInput, accountID string) (*ec2.DescribeEgressOnlyInternetGatewaysOutput, error) {
|
||||
var egressOnlyIGWs []*ec2.EgressOnlyInternetGateway
|
||||
|
||||
eigwIDs := make(map[string]bool)
|
||||
@@ -151,13 +174,14 @@ func (s *EgressOnlyIGWServiceImpl) DescribeEgressOnlyInternetGateways(input *ec2
|
||||
}
|
||||
}
|
||||
|
||||
prefix := accountID + "."
|
||||
keys, err := s.eigwKV.Keys()
|
||||
if err != nil && !errors.Is(err, nats.ErrNoKeysFound) {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
if len(eigwIDs) > 0 && !eigwIDs[key] {
|
||||
if !strings.HasPrefix(key, prefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -173,10 +197,14 @@ func (s *EgressOnlyIGWServiceImpl) DescribeEgressOnlyInternetGateways(input *ec2
|
||||
continue
|
||||
}
|
||||
|
||||
if len(eigwIDs) > 0 && !eigwIDs[record.EgressOnlyInternetGatewayId] {
|
||||
continue
|
||||
}
|
||||
|
||||
egressOnlyIGWs = append(egressOnlyIGWs, s.recordToEC2(&record))
|
||||
}
|
||||
|
||||
slog.Info("DescribeEgressOnlyInternetGateways completed", "count", len(egressOnlyIGWs))
|
||||
slog.Info("DescribeEgressOnlyInternetGateways completed", "count", len(egressOnlyIGWs), "accountID", accountID)
|
||||
|
||||
return &ec2.DescribeEgressOnlyInternetGatewaysOutput{
|
||||
EgressOnlyInternetGateways: egressOnlyIGWs,
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testAccountID = "123456789012"
|
||||
|
||||
func setupTestEIGWService(t *testing.T) *EgressOnlyIGWServiceImpl {
|
||||
t.Helper()
|
||||
opts := &server.Options{
|
||||
@@ -41,7 +43,7 @@ func createTestEIGW(t *testing.T, svc *EgressOnlyIGWServiceImpl) string {
|
||||
t.Helper()
|
||||
out, err := svc.CreateEgressOnlyInternetGateway(&ec2.CreateEgressOnlyInternetGatewayInput{
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
return *out.EgressOnlyInternetGateway.EgressOnlyInternetGatewayId
|
||||
}
|
||||
@@ -50,7 +52,7 @@ func TestCreateEgressOnlyInternetGateway(t *testing.T) {
|
||||
svc := setupTestEIGWService(t)
|
||||
out, err := svc.CreateEgressOnlyInternetGateway(&ec2.CreateEgressOnlyInternetGatewayInput{
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out.EgressOnlyInternetGateway)
|
||||
assert.Equal(t, "eigw-", (*out.EgressOnlyInternetGateway.EgressOnlyInternetGatewayId)[:5])
|
||||
@@ -61,7 +63,7 @@ func TestCreateEgressOnlyInternetGateway(t *testing.T) {
|
||||
|
||||
func TestCreateEgressOnlyInternetGateway_MissingVpcId(t *testing.T) {
|
||||
svc := setupTestEIGWService(t)
|
||||
_, err := svc.CreateEgressOnlyInternetGateway(&ec2.CreateEgressOnlyInternetGatewayInput{})
|
||||
_, err := svc.CreateEgressOnlyInternetGateway(&ec2.CreateEgressOnlyInternetGatewayInput{}, testAccountID)
|
||||
assert.ErrorContains(t, err, "MissingParameter")
|
||||
}
|
||||
|
||||
@@ -69,7 +71,7 @@ func TestCreateEgressOnlyInternetGateway_EmptyVpcId(t *testing.T) {
|
||||
svc := setupTestEIGWService(t)
|
||||
_, err := svc.CreateEgressOnlyInternetGateway(&ec2.CreateEgressOnlyInternetGatewayInput{
|
||||
VpcId: aws.String(""),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "MissingParameter")
|
||||
}
|
||||
|
||||
@@ -86,7 +88,7 @@ func TestCreateEgressOnlyInternetGateway_WithTags(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out.EgressOnlyInternetGateway)
|
||||
assert.Len(t, out.EgressOnlyInternetGateway.Tags, 2)
|
||||
@@ -94,7 +96,7 @@ func TestCreateEgressOnlyInternetGateway_WithTags(t *testing.T) {
|
||||
// Verify tags persist through describe
|
||||
desc, err := svc.DescribeEgressOnlyInternetGateways(&ec2.DescribeEgressOnlyInternetGatewaysInput{
|
||||
EgressOnlyInternetGatewayIds: []*string{out.EgressOnlyInternetGateway.EgressOnlyInternetGatewayId},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, desc.EgressOnlyInternetGateways, 1)
|
||||
assert.Len(t, desc.EgressOnlyInternetGateways[0].Tags, 2)
|
||||
@@ -112,7 +114,7 @@ func TestCreateEgressOnlyInternetGateway_TagsWrongResourceType(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, out.EgressOnlyInternetGateway.Tags)
|
||||
}
|
||||
@@ -123,13 +125,13 @@ func TestDeleteEgressOnlyInternetGateway(t *testing.T) {
|
||||
|
||||
out, err := svc.DeleteEgressOnlyInternetGateway(&ec2.DeleteEgressOnlyInternetGatewayInput{
|
||||
EgressOnlyInternetGatewayId: aws.String(eigwID),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, *out.ReturnCode)
|
||||
|
||||
desc, err := svc.DescribeEgressOnlyInternetGateways(&ec2.DescribeEgressOnlyInternetGatewaysInput{
|
||||
EgressOnlyInternetGatewayIds: []*string{aws.String(eigwID)},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, desc.EgressOnlyInternetGateways)
|
||||
}
|
||||
@@ -138,13 +140,13 @@ func TestDeleteEgressOnlyInternetGateway_NotFound(t *testing.T) {
|
||||
svc := setupTestEIGWService(t)
|
||||
_, err := svc.DeleteEgressOnlyInternetGateway(&ec2.DeleteEgressOnlyInternetGatewayInput{
|
||||
EgressOnlyInternetGatewayId: aws.String("eigw-nonexistent"),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "InvalidEgressOnlyInternetGatewayId.NotFound")
|
||||
}
|
||||
|
||||
func TestDeleteEgressOnlyInternetGateway_MissingID(t *testing.T) {
|
||||
svc := setupTestEIGWService(t)
|
||||
_, err := svc.DeleteEgressOnlyInternetGateway(&ec2.DeleteEgressOnlyInternetGatewayInput{})
|
||||
_, err := svc.DeleteEgressOnlyInternetGateway(&ec2.DeleteEgressOnlyInternetGatewayInput{}, testAccountID)
|
||||
assert.ErrorContains(t, err, "MissingParameter")
|
||||
}
|
||||
|
||||
@@ -152,7 +154,7 @@ func TestDeleteEgressOnlyInternetGateway_EmptyID(t *testing.T) {
|
||||
svc := setupTestEIGWService(t)
|
||||
_, err := svc.DeleteEgressOnlyInternetGateway(&ec2.DeleteEgressOnlyInternetGatewayInput{
|
||||
EgressOnlyInternetGatewayId: aws.String(""),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "MissingParameter")
|
||||
}
|
||||
|
||||
@@ -161,7 +163,7 @@ func TestDescribeEgressOnlyInternetGateways_All(t *testing.T) {
|
||||
createTestEIGW(t, svc)
|
||||
createTestEIGW(t, svc)
|
||||
|
||||
desc, err := svc.DescribeEgressOnlyInternetGateways(&ec2.DescribeEgressOnlyInternetGatewaysInput{})
|
||||
desc, err := svc.DescribeEgressOnlyInternetGateways(&ec2.DescribeEgressOnlyInternetGatewaysInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, desc.EgressOnlyInternetGateways, 2)
|
||||
}
|
||||
@@ -172,7 +174,7 @@ func TestDescribeEgressOnlyInternetGateways_ByID(t *testing.T) {
|
||||
|
||||
desc, err := svc.DescribeEgressOnlyInternetGateways(&ec2.DescribeEgressOnlyInternetGatewaysInput{
|
||||
EgressOnlyInternetGatewayIds: []*string{aws.String(eigwID)},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, desc.EgressOnlyInternetGateways, 1)
|
||||
assert.Equal(t, eigwID, *desc.EgressOnlyInternetGateways[0].EgressOnlyInternetGatewayId)
|
||||
@@ -180,7 +182,58 @@ func TestDescribeEgressOnlyInternetGateways_ByID(t *testing.T) {
|
||||
|
||||
func TestDescribeEgressOnlyInternetGateways_Empty(t *testing.T) {
|
||||
svc := setupTestEIGWService(t)
|
||||
desc, err := svc.DescribeEgressOnlyInternetGateways(&ec2.DescribeEgressOnlyInternetGatewaysInput{})
|
||||
desc, err := svc.DescribeEgressOnlyInternetGateways(&ec2.DescribeEgressOnlyInternetGatewaysInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, desc.EgressOnlyInternetGateways)
|
||||
}
|
||||
|
||||
// TestCreateEgressOnlyInternetGateway_CrossAccountVPCRejected tests that creating an EIGW in another account's VPC is rejected.
|
||||
func TestCreateEgressOnlyInternetGateway_CrossAccountVPCRejected(t *testing.T) {
|
||||
// Set up with manual NATS to get VPC KV access
|
||||
opts := &server.Options{
|
||||
Host: "127.0.0.1",
|
||||
Port: -1,
|
||||
JetStream: true,
|
||||
StoreDir: t.TempDir(),
|
||||
NoLog: true,
|
||||
NoSigs: true,
|
||||
}
|
||||
ns, err := server.NewServer(opts)
|
||||
require.NoError(t, err)
|
||||
go ns.Start()
|
||||
require.True(t, ns.ReadyForConnections(5*time.Second))
|
||||
t.Cleanup(func() { ns.Shutdown() })
|
||||
|
||||
nc, err := nats.Connect(ns.ClientURL())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { nc.Close() })
|
||||
|
||||
// Create VPC KV bucket with a VPC owned by testAccountID
|
||||
js, err := nc.JetStream()
|
||||
require.NoError(t, err)
|
||||
vpcKV, err := js.CreateKeyValue(&nats.KeyValueConfig{Bucket: kvBucketVPCs, History: 1})
|
||||
require.NoError(t, err)
|
||||
|
||||
vpcID := "vpc-alpha456"
|
||||
_, err = vpcKV.Put(accountKey(testAccountID, vpcID), []byte(`{"vpc_id":"vpc-alpha456","state":"available"}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create EIGW service (VPC KV will be populated since we created it before EIGW init)
|
||||
svc, err := NewEgressOnlyIGWServiceImplWithNATS(nil, nc)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Other account tries to create EIGW in testAccountID's VPC — should fail
|
||||
otherAccount := "999999999999"
|
||||
_, err = svc.CreateEgressOnlyInternetGateway(&ec2.CreateEgressOnlyInternetGatewayInput{
|
||||
VpcId: aws.String(vpcID),
|
||||
}, otherAccount)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "InvalidVpcID.NotFound")
|
||||
|
||||
// Owner creates EIGW in their own VPC — should succeed
|
||||
out, err := svc.CreateEgressOnlyInternetGateway(&ec2.CreateEgressOnlyInternetGatewayInput{
|
||||
VpcId: aws.String(vpcID),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, out.EgressOnlyInternetGateway)
|
||||
}
|
||||
|
||||
@@ -18,14 +18,14 @@ func NewNATSEgressOnlyIGWService(conn *nats.Conn) EgressOnlyIGWService {
|
||||
return &NATSEgressOnlyIGWService{natsConn: conn}
|
||||
}
|
||||
|
||||
func (s *NATSEgressOnlyIGWService) CreateEgressOnlyInternetGateway(input *ec2.CreateEgressOnlyInternetGatewayInput) (*ec2.CreateEgressOnlyInternetGatewayOutput, error) {
|
||||
return utils.NATSRequest[ec2.CreateEgressOnlyInternetGatewayOutput](s.natsConn, "ec2.CreateEgressOnlyInternetGateway", input, 30*time.Second)
|
||||
func (s *NATSEgressOnlyIGWService) CreateEgressOnlyInternetGateway(input *ec2.CreateEgressOnlyInternetGatewayInput, accountID string) (*ec2.CreateEgressOnlyInternetGatewayOutput, error) {
|
||||
return utils.NATSRequest[ec2.CreateEgressOnlyInternetGatewayOutput](s.natsConn, "ec2.CreateEgressOnlyInternetGateway", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEgressOnlyIGWService) DeleteEgressOnlyInternetGateway(input *ec2.DeleteEgressOnlyInternetGatewayInput) (*ec2.DeleteEgressOnlyInternetGatewayOutput, error) {
|
||||
return utils.NATSRequest[ec2.DeleteEgressOnlyInternetGatewayOutput](s.natsConn, "ec2.DeleteEgressOnlyInternetGateway", input, 30*time.Second)
|
||||
func (s *NATSEgressOnlyIGWService) DeleteEgressOnlyInternetGateway(input *ec2.DeleteEgressOnlyInternetGatewayInput, accountID string) (*ec2.DeleteEgressOnlyInternetGatewayOutput, error) {
|
||||
return utils.NATSRequest[ec2.DeleteEgressOnlyInternetGatewayOutput](s.natsConn, "ec2.DeleteEgressOnlyInternetGateway", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSEgressOnlyIGWService) DescribeEgressOnlyInternetGateways(input *ec2.DescribeEgressOnlyInternetGatewaysInput) (*ec2.DescribeEgressOnlyInternetGatewaysOutput, error) {
|
||||
return utils.NATSRequest[ec2.DescribeEgressOnlyInternetGatewaysOutput](s.natsConn, "ec2.DescribeEgressOnlyInternetGateways", input, 30*time.Second)
|
||||
func (s *NATSEgressOnlyIGWService) DescribeEgressOnlyInternetGateways(input *ec2.DescribeEgressOnlyInternetGatewaysInput, accountID string) (*ec2.DescribeEgressOnlyInternetGatewaysOutput, error) {
|
||||
return utils.NATSRequest[ec2.DescribeEgressOnlyInternetGatewaysOutput](s.natsConn, "ec2.DescribeEgressOnlyInternetGateways", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import "github.com/aws/aws-sdk-go/service/ec2"
|
||||
|
||||
// IGWService defines the interface for Internet Gateway operations
|
||||
type IGWService interface {
|
||||
CreateInternetGateway(input *ec2.CreateInternetGatewayInput) (*ec2.CreateInternetGatewayOutput, error)
|
||||
DeleteInternetGateway(input *ec2.DeleteInternetGatewayInput) (*ec2.DeleteInternetGatewayOutput, error)
|
||||
DescribeInternetGateways(input *ec2.DescribeInternetGatewaysInput) (*ec2.DescribeInternetGatewaysOutput, error)
|
||||
AttachInternetGateway(input *ec2.AttachInternetGatewayInput) (*ec2.AttachInternetGatewayOutput, error)
|
||||
DetachInternetGateway(input *ec2.DetachInternetGatewayInput) (*ec2.DetachInternetGatewayOutput, error)
|
||||
CreateInternetGateway(input *ec2.CreateInternetGatewayInput, accountID string) (*ec2.CreateInternetGatewayOutput, error)
|
||||
DeleteInternetGateway(input *ec2.DeleteInternetGatewayInput, accountID string) (*ec2.DeleteInternetGatewayOutput, error)
|
||||
DescribeInternetGateways(input *ec2.DescribeInternetGatewaysInput, accountID string) (*ec2.DescribeInternetGatewaysOutput, error)
|
||||
AttachInternetGateway(input *ec2.AttachInternetGatewayInput, accountID string) (*ec2.AttachInternetGatewayOutput, error)
|
||||
DetachInternetGateway(input *ec2.DetachInternetGatewayInput, accountID string) (*ec2.DetachInternetGatewayOutput, error)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
var _ IGWService = (*IGWServiceImpl)(nil)
|
||||
|
||||
const KVBucketIGW = "hive-igw"
|
||||
const kvBucketVPCs = "hive-vpc-vpcs"
|
||||
|
||||
// IGWRecord represents a stored Internet Gateway
|
||||
type IGWRecord struct {
|
||||
@@ -33,6 +35,7 @@ type IGWRecord struct {
|
||||
type IGWServiceImpl struct {
|
||||
config *config.Config
|
||||
igwKV nats.KeyValue
|
||||
vpcKV nats.KeyValue
|
||||
natsConn *nats.Conn
|
||||
}
|
||||
|
||||
@@ -55,11 +58,18 @@ func NewIGWServiceImplWithNATS(cfg *config.Config, natsConn *nats.Conn) (*IGWSer
|
||||
return nil, fmt.Errorf("failed to create KV bucket %s: %w", KVBucketIGW, err)
|
||||
}
|
||||
|
||||
// Get VPC KV bucket for cross-resource ownership validation
|
||||
vpcKV, err := js.KeyValue(kvBucketVPCs)
|
||||
if err != nil {
|
||||
slog.Warn("IGW service: VPC KV bucket not available, VPC ownership checks disabled", "error", err)
|
||||
}
|
||||
|
||||
slog.Info("IGW service initialized with JetStream KV", "bucket", KVBucketIGW)
|
||||
|
||||
return &IGWServiceImpl{
|
||||
config: cfg,
|
||||
igwKV: igwKV,
|
||||
vpcKV: vpcKV,
|
||||
natsConn: natsConn,
|
||||
}, nil
|
||||
}
|
||||
@@ -78,8 +88,12 @@ func getOrCreateKVBucket(js nats.JetStreamContext, bucketName string, history in
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
func accountKey(accountID, resourceID string) string {
|
||||
return accountID + "." + resourceID
|
||||
}
|
||||
|
||||
// CreateInternetGateway creates a new Internet Gateway (initially detached)
|
||||
func (s *IGWServiceImpl) CreateInternetGateway(input *ec2.CreateInternetGatewayInput) (*ec2.CreateInternetGatewayOutput, error) {
|
||||
func (s *IGWServiceImpl) CreateInternetGateway(input *ec2.CreateInternetGatewayInput, accountID string) (*ec2.CreateInternetGatewayOutput, error) {
|
||||
igwID := utils.GenerateResourceID("igw")
|
||||
|
||||
record := IGWRecord{
|
||||
@@ -103,11 +117,11 @@ func (s *IGWServiceImpl) CreateInternetGateway(input *ec2.CreateInternetGatewayI
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal IGW record: %w", err)
|
||||
}
|
||||
if _, err := s.igwKV.Put(igwID, data); err != nil {
|
||||
if _, err := s.igwKV.Put(accountKey(accountID, igwID), data); err != nil {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
slog.Info("CreateInternetGateway completed", "internetGatewayId", igwID)
|
||||
slog.Info("CreateInternetGateway completed", "internetGatewayId", igwID, "accountID", accountID)
|
||||
|
||||
return &ec2.CreateInternetGatewayOutput{
|
||||
InternetGateway: s.recordToEC2(&record),
|
||||
@@ -115,14 +129,15 @@ func (s *IGWServiceImpl) CreateInternetGateway(input *ec2.CreateInternetGatewayI
|
||||
}
|
||||
|
||||
// DeleteInternetGateway deletes an Internet Gateway (must be detached first)
|
||||
func (s *IGWServiceImpl) DeleteInternetGateway(input *ec2.DeleteInternetGatewayInput) (*ec2.DeleteInternetGatewayOutput, error) {
|
||||
func (s *IGWServiceImpl) DeleteInternetGateway(input *ec2.DeleteInternetGatewayInput, accountID string) (*ec2.DeleteInternetGatewayOutput, error) {
|
||||
if input.InternetGatewayId == nil || *input.InternetGatewayId == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
|
||||
igwID := *input.InternetGatewayId
|
||||
key := accountKey(accountID, igwID)
|
||||
|
||||
entry, err := s.igwKV.Get(igwID)
|
||||
entry, err := s.igwKV.Get(key)
|
||||
if err != nil {
|
||||
return nil, errors.New(awserrors.ErrorInvalidInternetGatewayIDNotFound)
|
||||
}
|
||||
@@ -137,17 +152,17 @@ func (s *IGWServiceImpl) DeleteInternetGateway(input *ec2.DeleteInternetGatewayI
|
||||
return nil, errors.New(awserrors.ErrorDependencyViolation)
|
||||
}
|
||||
|
||||
if err := s.igwKV.Delete(igwID); err != nil {
|
||||
if err := s.igwKV.Delete(key); err != nil {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
slog.Info("DeleteInternetGateway completed", "internetGatewayId", igwID)
|
||||
slog.Info("DeleteInternetGateway completed", "internetGatewayId", igwID, "accountID", accountID)
|
||||
|
||||
return &ec2.DeleteInternetGatewayOutput{}, nil
|
||||
}
|
||||
|
||||
// DescribeInternetGateways lists Internet Gateways, optionally filtered by ID
|
||||
func (s *IGWServiceImpl) DescribeInternetGateways(input *ec2.DescribeInternetGatewaysInput) (*ec2.DescribeInternetGatewaysOutput, error) {
|
||||
func (s *IGWServiceImpl) DescribeInternetGateways(input *ec2.DescribeInternetGatewaysInput, accountID string) (*ec2.DescribeInternetGatewaysOutput, error) {
|
||||
var igws []*ec2.InternetGateway
|
||||
|
||||
igwIDs := make(map[string]bool)
|
||||
@@ -157,13 +172,16 @@ func (s *IGWServiceImpl) DescribeInternetGateways(input *ec2.DescribeInternetGat
|
||||
}
|
||||
}
|
||||
|
||||
prefix := accountID + "."
|
||||
keys, err := s.igwKV.Keys()
|
||||
if err != nil && !errors.Is(err, nats.ErrNoKeysFound) {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
foundIDs := make(map[string]bool)
|
||||
|
||||
for _, key := range keys {
|
||||
if len(igwIDs) > 0 && !igwIDs[key] {
|
||||
if !strings.HasPrefix(key, prefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -179,10 +197,22 @@ func (s *IGWServiceImpl) DescribeInternetGateways(input *ec2.DescribeInternetGat
|
||||
continue
|
||||
}
|
||||
|
||||
if len(igwIDs) > 0 && !igwIDs[record.InternetGatewayId] {
|
||||
continue
|
||||
}
|
||||
|
||||
igws = append(igws, s.recordToEC2(&record))
|
||||
foundIDs[record.InternetGatewayId] = true
|
||||
}
|
||||
|
||||
slog.Info("DescribeInternetGateways completed", "count", len(igws))
|
||||
// Return error if specific IDs were requested but not found
|
||||
for id := range igwIDs {
|
||||
if !foundIDs[id] {
|
||||
return nil, errors.New(awserrors.ErrorInvalidInternetGatewayIDNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("DescribeInternetGateways completed", "count", len(igws), "accountID", accountID)
|
||||
|
||||
return &ec2.DescribeInternetGatewaysOutput{
|
||||
InternetGateways: igws,
|
||||
@@ -191,7 +221,7 @@ func (s *IGWServiceImpl) DescribeInternetGateways(input *ec2.DescribeInternetGat
|
||||
|
||||
// AttachInternetGateway attaches an IGW to a VPC and publishes a NATS event
|
||||
// for vpcd to create the OVN external switch, gateway port, and SNAT rules.
|
||||
func (s *IGWServiceImpl) AttachInternetGateway(input *ec2.AttachInternetGatewayInput) (*ec2.AttachInternetGatewayOutput, error) {
|
||||
func (s *IGWServiceImpl) AttachInternetGateway(input *ec2.AttachInternetGatewayInput, accountID string) (*ec2.AttachInternetGatewayOutput, error) {
|
||||
if input.InternetGatewayId == nil || *input.InternetGatewayId == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
@@ -201,8 +231,9 @@ func (s *IGWServiceImpl) AttachInternetGateway(input *ec2.AttachInternetGatewayI
|
||||
|
||||
igwID := *input.InternetGatewayId
|
||||
vpcID := *input.VpcId
|
||||
key := accountKey(accountID, igwID)
|
||||
|
||||
entry, err := s.igwKV.Get(igwID)
|
||||
entry, err := s.igwKV.Get(key)
|
||||
if err != nil {
|
||||
return nil, errors.New(awserrors.ErrorInvalidInternetGatewayIDNotFound)
|
||||
}
|
||||
@@ -216,6 +247,14 @@ func (s *IGWServiceImpl) AttachInternetGateway(input *ec2.AttachInternetGatewayI
|
||||
return nil, errors.New(awserrors.ErrorResourceAlreadyAssociated)
|
||||
}
|
||||
|
||||
// Verify the caller owns the target VPC
|
||||
if s.vpcKV != nil {
|
||||
if _, err := s.vpcKV.Get(accountKey(accountID, vpcID)); err != nil {
|
||||
slog.Warn("AttachInternetGateway: VPC not found for account", "vpcId", vpcID, "accountID", accountID)
|
||||
return nil, errors.New(awserrors.ErrorInvalidVpcIDNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
record.VpcId = vpcID
|
||||
record.State = "attached"
|
||||
|
||||
@@ -223,7 +262,7 @@ func (s *IGWServiceImpl) AttachInternetGateway(input *ec2.AttachInternetGatewayI
|
||||
if err != nil {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
if _, err := s.igwKV.Put(igwID, data); err != nil {
|
||||
if _, err := s.igwKV.Put(key, data); err != nil {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
@@ -239,14 +278,14 @@ func (s *IGWServiceImpl) AttachInternetGateway(input *ec2.AttachInternetGatewayI
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("AttachInternetGateway completed", "internetGatewayId", igwID, "vpcId", vpcID)
|
||||
slog.Info("AttachInternetGateway completed", "internetGatewayId", igwID, "vpcId", vpcID, "accountID", accountID)
|
||||
|
||||
return &ec2.AttachInternetGatewayOutput{}, nil
|
||||
}
|
||||
|
||||
// DetachInternetGateway detaches an IGW from a VPC and publishes a NATS event
|
||||
// for vpcd to clean up the OVN external switch, gateway port, and NAT rules.
|
||||
func (s *IGWServiceImpl) DetachInternetGateway(input *ec2.DetachInternetGatewayInput) (*ec2.DetachInternetGatewayOutput, error) {
|
||||
func (s *IGWServiceImpl) DetachInternetGateway(input *ec2.DetachInternetGatewayInput, accountID string) (*ec2.DetachInternetGatewayOutput, error) {
|
||||
if input.InternetGatewayId == nil || *input.InternetGatewayId == "" {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
@@ -256,8 +295,9 @@ func (s *IGWServiceImpl) DetachInternetGateway(input *ec2.DetachInternetGatewayI
|
||||
|
||||
igwID := *input.InternetGatewayId
|
||||
vpcID := *input.VpcId
|
||||
key := accountKey(accountID, igwID)
|
||||
|
||||
entry, err := s.igwKV.Get(igwID)
|
||||
entry, err := s.igwKV.Get(key)
|
||||
if err != nil {
|
||||
return nil, errors.New(awserrors.ErrorInvalidInternetGatewayIDNotFound)
|
||||
}
|
||||
@@ -278,7 +318,7 @@ func (s *IGWServiceImpl) DetachInternetGateway(input *ec2.DetachInternetGatewayI
|
||||
if err != nil {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
if _, err := s.igwKV.Put(igwID, data); err != nil {
|
||||
if _, err := s.igwKV.Put(key, data); err != nil {
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
|
||||
@@ -294,7 +334,7 @@ func (s *IGWServiceImpl) DetachInternetGateway(input *ec2.DetachInternetGatewayI
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("DetachInternetGateway completed", "internetGatewayId", igwID, "vpcId", vpcID)
|
||||
slog.Info("DetachInternetGateway completed", "internetGatewayId", igwID, "vpcId", vpcID, "accountID", accountID)
|
||||
|
||||
return &ec2.DetachInternetGatewayOutput{}, nil
|
||||
}
|
||||
|
||||
@@ -39,14 +39,14 @@ func setupTestIGWService(t *testing.T) (*IGWServiceImpl, *nats.Conn) {
|
||||
|
||||
func createTestIGW(t *testing.T, svc *IGWServiceImpl) string {
|
||||
t.Helper()
|
||||
out, err := svc.CreateInternetGateway(&ec2.CreateInternetGatewayInput{})
|
||||
out, err := svc.CreateInternetGateway(&ec2.CreateInternetGatewayInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
return *out.InternetGateway.InternetGatewayId
|
||||
}
|
||||
|
||||
func TestCreateInternetGateway(t *testing.T) {
|
||||
svc, _ := setupTestIGWService(t)
|
||||
out, err := svc.CreateInternetGateway(&ec2.CreateInternetGatewayInput{})
|
||||
out, err := svc.CreateInternetGateway(&ec2.CreateInternetGatewayInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out.InternetGateway)
|
||||
assert.Equal(t, "igw-", (*out.InternetGateway.InternetGatewayId)[:4])
|
||||
@@ -66,14 +66,14 @@ func TestCreateInternetGateway_WithTags(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, out.InternetGateway.Tags, 2)
|
||||
|
||||
// Verify tags persist through describe
|
||||
desc, err := svc.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{
|
||||
InternetGatewayIds: []*string{out.InternetGateway.InternetGatewayId},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, desc.InternetGateways, 1)
|
||||
assert.Len(t, desc.InternetGateways[0].Tags, 2)
|
||||
@@ -90,7 +90,7 @@ func TestCreateInternetGateway_TagsWrongResourceType(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, out.InternetGateway.Tags)
|
||||
}
|
||||
@@ -101,27 +101,26 @@ func TestDeleteInternetGateway(t *testing.T) {
|
||||
|
||||
_, err := svc.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
desc, err := svc.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{
|
||||
_, err = svc.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{
|
||||
InternetGatewayIds: []*string{aws.String(igwID)},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, desc.InternetGateways)
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "InvalidInternetGatewayID.NotFound")
|
||||
}
|
||||
|
||||
func TestDeleteInternetGateway_NotFound(t *testing.T) {
|
||||
svc, _ := setupTestIGWService(t)
|
||||
_, err := svc.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{
|
||||
InternetGatewayId: aws.String("igw-nonexistent"),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "InvalidInternetGatewayID.NotFound")
|
||||
}
|
||||
|
||||
func TestDeleteInternetGateway_MissingID(t *testing.T) {
|
||||
svc, _ := setupTestIGWService(t)
|
||||
_, err := svc.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{})
|
||||
_, err := svc.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{}, testAccountID)
|
||||
assert.ErrorContains(t, err, "MissingParameter")
|
||||
}
|
||||
|
||||
@@ -129,7 +128,7 @@ func TestDeleteInternetGateway_EmptyID(t *testing.T) {
|
||||
svc, _ := setupTestIGWService(t)
|
||||
_, err := svc.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(""),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "MissingParameter")
|
||||
}
|
||||
|
||||
@@ -141,13 +140,13 @@ func TestDeleteInternetGateway_WhileAttached(t *testing.T) {
|
||||
_, err := svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to delete — should fail with DependencyViolation
|
||||
_, err = svc.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "DependencyViolation")
|
||||
}
|
||||
|
||||
@@ -156,7 +155,7 @@ func TestDescribeInternetGateways_All(t *testing.T) {
|
||||
createTestIGW(t, svc)
|
||||
createTestIGW(t, svc)
|
||||
|
||||
desc, err := svc.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{})
|
||||
desc, err := svc.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, desc.InternetGateways, 2)
|
||||
}
|
||||
@@ -168,7 +167,7 @@ func TestDescribeInternetGateways_ByID(t *testing.T) {
|
||||
|
||||
desc, err := svc.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{
|
||||
InternetGatewayIds: []*string{aws.String(igwID)},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, desc.InternetGateways, 1)
|
||||
assert.Equal(t, igwID, *desc.InternetGateways[0].InternetGatewayId)
|
||||
@@ -176,7 +175,7 @@ func TestDescribeInternetGateways_ByID(t *testing.T) {
|
||||
|
||||
func TestDescribeInternetGateways_Empty(t *testing.T) {
|
||||
svc, _ := setupTestIGWService(t)
|
||||
desc, err := svc.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{})
|
||||
desc, err := svc.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, desc.InternetGateways)
|
||||
}
|
||||
@@ -188,13 +187,13 @@ func TestAttachInternetGateway(t *testing.T) {
|
||||
_, err := svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify attachment via describe
|
||||
desc, err := svc.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{
|
||||
InternetGatewayIds: []*string{aws.String(igwID)},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, desc.InternetGateways, 1)
|
||||
require.Len(t, desc.InternetGateways[0].Attachments, 1)
|
||||
@@ -207,7 +206,7 @@ func TestAttachInternetGateway_NotFound(t *testing.T) {
|
||||
_, err := svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String("igw-nonexistent"),
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "InvalidInternetGatewayID.NotFound")
|
||||
}
|
||||
|
||||
@@ -218,14 +217,14 @@ func TestAttachInternetGateway_AlreadyAttached(t *testing.T) {
|
||||
_, err := svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try attaching again — should fail
|
||||
_, err = svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String("vpc-other"),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "Resource.AlreadyAssociated")
|
||||
}
|
||||
|
||||
@@ -233,12 +232,12 @@ func TestAttachInternetGateway_MissingParams(t *testing.T) {
|
||||
svc, _ := setupTestIGWService(t)
|
||||
_, err := svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "MissingParameter")
|
||||
|
||||
_, err = svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String("igw-test"),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "MissingParameter")
|
||||
}
|
||||
|
||||
@@ -250,20 +249,20 @@ func TestDetachInternetGateway(t *testing.T) {
|
||||
_, err := svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Detach
|
||||
_, err = svc.DetachInternetGateway(&ec2.DetachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify detached
|
||||
desc, err := svc.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{
|
||||
InternetGatewayIds: []*string{aws.String(igwID)},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, desc.InternetGateways, 1)
|
||||
assert.Empty(t, desc.InternetGateways[0].Attachments)
|
||||
@@ -276,7 +275,7 @@ func TestDetachInternetGateway_NotAttached(t *testing.T) {
|
||||
_, err := svc.DetachInternetGateway(&ec2.DetachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "Gateway.NotAttached")
|
||||
}
|
||||
|
||||
@@ -287,14 +286,14 @@ func TestDetachInternetGateway_WrongVPC(t *testing.T) {
|
||||
_, err := svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try detaching from wrong VPC
|
||||
_, err = svc.DetachInternetGateway(&ec2.DetachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String("vpc-wrong"),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "Gateway.NotAttached")
|
||||
}
|
||||
|
||||
@@ -303,7 +302,7 @@ func TestDetachInternetGateway_NotFound(t *testing.T) {
|
||||
_, err := svc.DetachInternetGateway(&ec2.DetachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String("igw-nonexistent"),
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "InvalidInternetGatewayID.NotFound")
|
||||
}
|
||||
|
||||
@@ -311,12 +310,12 @@ func TestDetachInternetGateway_MissingParams(t *testing.T) {
|
||||
svc, _ := setupTestIGWService(t)
|
||||
_, err := svc.DetachInternetGateway(&ec2.DetachInternetGatewayInput{
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "MissingParameter")
|
||||
|
||||
_, err = svc.DetachInternetGateway(&ec2.DetachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String("igw-test"),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "MissingParameter")
|
||||
}
|
||||
|
||||
@@ -330,30 +329,30 @@ func TestIGWLifecycle_CreateAttachDetachDelete(t *testing.T) {
|
||||
_, err := svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String("vpc-lifecycle"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cannot delete while attached
|
||||
_, err = svc.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
})
|
||||
}, testAccountID)
|
||||
assert.ErrorContains(t, err, "DependencyViolation")
|
||||
|
||||
// Detach
|
||||
_, err = svc.DetachInternetGateway(&ec2.DetachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String("vpc-lifecycle"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now delete succeeds
|
||||
_, err = svc.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify gone
|
||||
desc, err := svc.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{})
|
||||
desc, err := svc.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, desc.InternetGateways)
|
||||
}
|
||||
@@ -373,7 +372,7 @@ func TestAttachInternetGateway_PublishesEvent(t *testing.T) {
|
||||
_, err = svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String("vpc-event-test"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify event was published
|
||||
@@ -394,7 +393,7 @@ func TestDetachInternetGateway_PublishesEvent(t *testing.T) {
|
||||
_, err := svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String("vpc-event-test"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Subscribe to IGW detach events
|
||||
@@ -408,7 +407,7 @@ func TestDetachInternetGateway_PublishesEvent(t *testing.T) {
|
||||
_, err = svc.DetachInternetGateway(&ec2.DetachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String("vpc-event-test"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify event was published
|
||||
@@ -432,7 +431,7 @@ func TestCreateInternetGateway_PublishesNoEvent(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = sub.Unsubscribe() }()
|
||||
|
||||
_, err = svc.CreateInternetGateway(&ec2.CreateInternetGatewayInput{})
|
||||
_, err = svc.CreateInternetGateway(&ec2.CreateInternetGatewayInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify no event was published
|
||||
@@ -452,6 +451,46 @@ func TestNewIGWServiceImpl(t *testing.T) {
|
||||
assert.Nil(t, svc.natsConn)
|
||||
}
|
||||
|
||||
// TestAttachInternetGateway_CrossAccountVPCRejected tests that attaching an IGW to another account's VPC is rejected.
|
||||
func TestAttachInternetGateway_CrossAccountVPCRejected(t *testing.T) {
|
||||
svc, nc := setupTestIGWService(t)
|
||||
|
||||
// Create VPC KV bucket and add a VPC owned by testAccountID
|
||||
js, err := nc.JetStream()
|
||||
require.NoError(t, err)
|
||||
vpcKV, err := js.CreateKeyValue(&nats.KeyValueConfig{Bucket: kvBucketVPCs, History: 1})
|
||||
require.NoError(t, err)
|
||||
|
||||
vpcID := "vpc-alpha123"
|
||||
_, err = vpcKV.Put(accountKey(testAccountID, vpcID), []byte(`{"vpc_id":"vpc-alpha123","state":"available"}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Refresh service to pick up the VPC KV bucket
|
||||
svc.vpcKV = vpcKV
|
||||
|
||||
// Create IGW owned by otherAccountID
|
||||
otherAccount := "999999999999"
|
||||
out, err := svc.CreateInternetGateway(&ec2.CreateInternetGatewayInput{}, otherAccount)
|
||||
require.NoError(t, err)
|
||||
igwID := *out.InternetGateway.InternetGatewayId
|
||||
|
||||
// Other account tries to attach their IGW to testAccountID's VPC — should fail
|
||||
_, err = svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
VpcId: aws.String(vpcID),
|
||||
}, otherAccount)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "InvalidVpcID.NotFound")
|
||||
|
||||
// Owner attaches their own IGW to their own VPC — should succeed
|
||||
ownIGW := createTestIGW(t, svc)
|
||||
_, err = svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(ownIGW),
|
||||
VpcId: aws.String(vpcID),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteInternetGateway_PublishesNoEvent(t *testing.T) {
|
||||
svc, nc := setupTestIGWService(t)
|
||||
igwID := createTestIGW(t, svc)
|
||||
@@ -466,7 +505,7 @@ func TestDeleteInternetGateway_PublishesNoEvent(t *testing.T) {
|
||||
|
||||
_, err = svc.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{
|
||||
InternetGatewayId: aws.String(igwID),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify no event was published
|
||||
|
||||
@@ -18,22 +18,22 @@ func NewNATSIGWService(conn *nats.Conn) IGWService {
|
||||
return &NATSIGWService{natsConn: conn}
|
||||
}
|
||||
|
||||
func (s *NATSIGWService) CreateInternetGateway(input *ec2.CreateInternetGatewayInput) (*ec2.CreateInternetGatewayOutput, error) {
|
||||
return utils.NATSRequest[ec2.CreateInternetGatewayOutput](s.natsConn, "ec2.CreateInternetGateway", input, 30*time.Second)
|
||||
func (s *NATSIGWService) CreateInternetGateway(input *ec2.CreateInternetGatewayInput, accountID string) (*ec2.CreateInternetGatewayOutput, error) {
|
||||
return utils.NATSRequest[ec2.CreateInternetGatewayOutput](s.natsConn, "ec2.CreateInternetGateway", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSIGWService) DeleteInternetGateway(input *ec2.DeleteInternetGatewayInput) (*ec2.DeleteInternetGatewayOutput, error) {
|
||||
return utils.NATSRequest[ec2.DeleteInternetGatewayOutput](s.natsConn, "ec2.DeleteInternetGateway", input, 30*time.Second)
|
||||
func (s *NATSIGWService) DeleteInternetGateway(input *ec2.DeleteInternetGatewayInput, accountID string) (*ec2.DeleteInternetGatewayOutput, error) {
|
||||
return utils.NATSRequest[ec2.DeleteInternetGatewayOutput](s.natsConn, "ec2.DeleteInternetGateway", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSIGWService) DescribeInternetGateways(input *ec2.DescribeInternetGatewaysInput) (*ec2.DescribeInternetGatewaysOutput, error) {
|
||||
return utils.NATSRequest[ec2.DescribeInternetGatewaysOutput](s.natsConn, "ec2.DescribeInternetGateways", input, 30*time.Second)
|
||||
func (s *NATSIGWService) DescribeInternetGateways(input *ec2.DescribeInternetGatewaysInput, accountID string) (*ec2.DescribeInternetGatewaysOutput, error) {
|
||||
return utils.NATSRequest[ec2.DescribeInternetGatewaysOutput](s.natsConn, "ec2.DescribeInternetGateways", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSIGWService) AttachInternetGateway(input *ec2.AttachInternetGatewayInput) (*ec2.AttachInternetGatewayOutput, error) {
|
||||
return utils.NATSRequest[ec2.AttachInternetGatewayOutput](s.natsConn, "ec2.AttachInternetGateway", input, 30*time.Second)
|
||||
func (s *NATSIGWService) AttachInternetGateway(input *ec2.AttachInternetGatewayInput, accountID string) (*ec2.AttachInternetGatewayOutput, error) {
|
||||
return utils.NATSRequest[ec2.AttachInternetGatewayOutput](s.natsConn, "ec2.AttachInternetGateway", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSIGWService) DetachInternetGateway(input *ec2.DetachInternetGatewayInput) (*ec2.DetachInternetGatewayOutput, error) {
|
||||
return utils.NATSRequest[ec2.DetachInternetGatewayOutput](s.natsConn, "ec2.DetachInternetGateway", input, 30*time.Second)
|
||||
func (s *NATSIGWService) DetachInternetGateway(input *ec2.DetachInternetGatewayInput, accountID string) (*ec2.DetachInternetGatewayOutput, error) {
|
||||
return utils.NATSRequest[ec2.DetachInternetGatewayOutput](s.natsConn, "ec2.DetachInternetGateway", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testAccountID = "123456789012"
|
||||
|
||||
// setupNATSIGWServiceTest creates a NATSIGWService client connected to an
|
||||
// IGWServiceImpl backend via NATS.
|
||||
func setupNATSIGWServiceTest(t *testing.T) (IGWService, *IGWServiceImpl) {
|
||||
@@ -36,13 +38,14 @@ func setupNATSIGWServiceTest(t *testing.T) (IGWService, *IGWServiceImpl) {
|
||||
return client, backend
|
||||
}
|
||||
|
||||
func handleNATSMsg[In any, Out any](msg *nats.Msg, fn func(*In) (*Out, error)) {
|
||||
func handleNATSMsg[In any, Out any](msg *nats.Msg, fn func(*In, string) (*Out, error)) {
|
||||
var input In
|
||||
if err := json.Unmarshal(msg.Data, &input); err != nil {
|
||||
_ = msg.Respond([]byte(`{"error":"unmarshal"}`))
|
||||
return
|
||||
}
|
||||
result, err := fn(&input)
|
||||
accountID := msg.Header.Get("X-Account-ID")
|
||||
result, err := fn(&input, accountID)
|
||||
if err != nil {
|
||||
errResp, _ := json.Marshal(map[string]string{"error": err.Error()})
|
||||
_ = msg.Respond(errResp)
|
||||
@@ -55,7 +58,7 @@ func handleNATSMsg[In any, Out any](msg *nats.Msg, fn func(*In) (*Out, error)) {
|
||||
func TestNATSIGWService_CreateInternetGateway(t *testing.T) {
|
||||
client, _ := setupNATSIGWServiceTest(t)
|
||||
|
||||
out, err := client.CreateInternetGateway(&ec2.CreateInternetGatewayInput{})
|
||||
out, err := client.CreateInternetGateway(&ec2.CreateInternetGatewayInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out.InternetGateway)
|
||||
assert.NotEmpty(t, *out.InternetGateway.InternetGatewayId)
|
||||
@@ -64,10 +67,10 @@ func TestNATSIGWService_CreateInternetGateway(t *testing.T) {
|
||||
func TestNATSIGWService_DescribeInternetGateways(t *testing.T) {
|
||||
client, _ := setupNATSIGWServiceTest(t)
|
||||
|
||||
_, err := client.CreateInternetGateway(&ec2.CreateInternetGatewayInput{})
|
||||
_, err := client.CreateInternetGateway(&ec2.CreateInternetGatewayInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := client.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{})
|
||||
out, err := client.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(out.InternetGateways), 1)
|
||||
}
|
||||
@@ -75,31 +78,31 @@ func TestNATSIGWService_DescribeInternetGateways(t *testing.T) {
|
||||
func TestNATSIGWService_DeleteInternetGateway(t *testing.T) {
|
||||
client, _ := setupNATSIGWServiceTest(t)
|
||||
|
||||
createOut, err := client.CreateInternetGateway(&ec2.CreateInternetGatewayInput{})
|
||||
createOut, err := client.CreateInternetGateway(&ec2.CreateInternetGatewayInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{
|
||||
InternetGatewayId: createOut.InternetGateway.InternetGatewayId,
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNATSIGWService_AttachAndDetach(t *testing.T) {
|
||||
client, _ := setupNATSIGWServiceTest(t)
|
||||
|
||||
createOut, err := client.CreateInternetGateway(&ec2.CreateInternetGatewayInput{})
|
||||
createOut, err := client.CreateInternetGateway(&ec2.CreateInternetGatewayInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
igwID := createOut.InternetGateway.InternetGatewayId
|
||||
|
||||
_, err = client.AttachInternetGateway(&ec2.AttachInternetGatewayInput{
|
||||
InternetGatewayId: igwID,
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.DetachInternetGateway(&ec2.DetachInternetGatewayInput{
|
||||
InternetGatewayId: igwID,
|
||||
VpcId: aws.String("vpc-test123"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import "github.com/aws/aws-sdk-go/service/ec2"
|
||||
|
||||
// ImageService defines the interface for EC2 image operations business logic
|
||||
type ImageService interface {
|
||||
CreateImage(input *ec2.CreateImageInput) (*ec2.CreateImageOutput, error)
|
||||
CopyImage(input *ec2.CopyImageInput) (*ec2.CopyImageOutput, error)
|
||||
DescribeImages(input *ec2.DescribeImagesInput) (*ec2.DescribeImagesOutput, error)
|
||||
DescribeImageAttribute(input *ec2.DescribeImageAttributeInput) (*ec2.DescribeImageAttributeOutput, error)
|
||||
RegisterImage(input *ec2.RegisterImageInput) (*ec2.RegisterImageOutput, error)
|
||||
DeregisterImage(input *ec2.DeregisterImageInput) (*ec2.DeregisterImageOutput, error)
|
||||
ModifyImageAttribute(input *ec2.ModifyImageAttributeInput) (*ec2.ModifyImageAttributeOutput, error)
|
||||
ResetImageAttribute(input *ec2.ResetImageAttributeInput) (*ec2.ResetImageAttributeOutput, error)
|
||||
CreateImage(input *ec2.CreateImageInput, accountID string) (*ec2.CreateImageOutput, error)
|
||||
CopyImage(input *ec2.CopyImageInput, accountID string) (*ec2.CopyImageOutput, error)
|
||||
DescribeImages(input *ec2.DescribeImagesInput, accountID string) (*ec2.DescribeImagesOutput, error)
|
||||
DescribeImageAttribute(input *ec2.DescribeImageAttributeInput, accountID string) (*ec2.DescribeImageAttributeOutput, error)
|
||||
RegisterImage(input *ec2.RegisterImageInput, accountID string) (*ec2.RegisterImageOutput, error)
|
||||
DeregisterImage(input *ec2.DeregisterImageInput, accountID string) (*ec2.DeregisterImageOutput, error)
|
||||
ModifyImageAttribute(input *ec2.ModifyImageAttributeInput, accountID string) (*ec2.ModifyImageAttributeOutput, error)
|
||||
ResetImageAttribute(input *ec2.ResetImageAttributeInput, accountID string) (*ec2.ResetImageAttributeOutput, error)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ type ImageServiceImpl struct {
|
||||
store objectstore.ObjectStore
|
||||
natsConn *nats.Conn
|
||||
bucketName string
|
||||
accountID string // AWS account ID for S3 key storage path
|
||||
}
|
||||
|
||||
// NewImageServiceImpl creates a new daemon-side image service
|
||||
@@ -58,21 +57,19 @@ func NewImageServiceImpl(cfg *config.Config, natsConn *nats.Conn) *ImageServiceI
|
||||
store: store,
|
||||
natsConn: natsConn,
|
||||
bucketName: cfg.Predastore.Bucket,
|
||||
accountID: "123456789", // TODO: Implement proper account ID management
|
||||
}
|
||||
}
|
||||
|
||||
// NewImageServiceImplWithStore creates an image service with a custom object store (for testing)
|
||||
func NewImageServiceImplWithStore(store objectstore.ObjectStore, bucketName, accountID string) *ImageServiceImpl {
|
||||
func NewImageServiceImplWithStore(store objectstore.ObjectStore, bucketName string) *ImageServiceImpl {
|
||||
return &ImageServiceImpl{
|
||||
store: store,
|
||||
bucketName: bucketName,
|
||||
accountID: accountID,
|
||||
}
|
||||
}
|
||||
|
||||
// DescribeImages lists available AMI images by reading config.json files from S3
|
||||
func (s *ImageServiceImpl) DescribeImages(input *ec2.DescribeImagesInput) (*ec2.DescribeImagesOutput, error) {
|
||||
func (s *ImageServiceImpl) DescribeImages(input *ec2.DescribeImagesInput, accountID string) (*ec2.DescribeImagesOutput, error) {
|
||||
if input == nil {
|
||||
input = &ec2.DescribeImagesInput{}
|
||||
}
|
||||
@@ -162,12 +159,38 @@ func (s *ImageServiceImpl) DescribeImages(input *ec2.DescribeImagesInput) (*ec2.
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by Owner if specified (only support "self" and account IDs for now)
|
||||
// Determine AMI ownership. Phase4+ AMIs store the creator's account ID
|
||||
// in ImageOwnerAlias. Pre-phase4 AMIs have non-account values like "self"
|
||||
// or "hive" and are treated as system/public images visible to all.
|
||||
amiOwner := amiMeta.ImageOwnerAlias
|
||||
isSystemAMI := !isAccountID(amiOwner)
|
||||
|
||||
// Visibility check: callers can only see their own AMIs and system AMIs.
|
||||
// This runs regardless of whether an owner filter is specified.
|
||||
if !isSystemAMI && amiOwner != accountID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter by Owner if specified
|
||||
if len(input.Owners) > 0 {
|
||||
found := false
|
||||
for _, owner := range input.Owners {
|
||||
if owner != nil && (*owner == "self" || *owner == s.accountID || *owner == amiMeta.ImageOwnerAlias) {
|
||||
found = true
|
||||
if owner == nil {
|
||||
continue
|
||||
}
|
||||
switch *owner {
|
||||
case "self":
|
||||
// "self" matches only AMIs owned by the caller
|
||||
if amiOwner == accountID {
|
||||
found = true
|
||||
}
|
||||
default:
|
||||
// Match by explicit account ID
|
||||
if amiOwner == *owner {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -176,6 +199,12 @@ func (s *ImageServiceImpl) DescribeImages(input *ec2.DescribeImagesInput) (*ec2.
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the OwnerId for the response. System AMIs use global account.
|
||||
ownerID := amiOwner
|
||||
if isSystemAMI {
|
||||
ownerID = "000000000000"
|
||||
}
|
||||
|
||||
// Build EC2 Image from AMIMetadata
|
||||
image := &ec2.Image{
|
||||
ImageId: aws.String(amiMeta.ImageID),
|
||||
@@ -187,7 +216,7 @@ func (s *ImageServiceImpl) DescribeImages(input *ec2.DescribeImagesInput) (*ec2.
|
||||
RootDeviceType: aws.String(amiMeta.RootDeviceType),
|
||||
VirtualizationType: aws.String(amiMeta.Virtualization),
|
||||
ImageOwnerAlias: aws.String(amiMeta.ImageOwnerAlias),
|
||||
OwnerId: aws.String(s.accountID),
|
||||
OwnerId: aws.String(ownerID),
|
||||
Public: aws.Bool(false),
|
||||
State: aws.String("available"),
|
||||
ImageType: aws.String("machine"),
|
||||
@@ -249,13 +278,13 @@ func (s *ImageServiceImpl) DescribeImages(input *ec2.DescribeImagesInput) (*ec2.
|
||||
|
||||
// CreateImage is the generic interface method — on the daemon side, the handler
|
||||
// calls CreateImageFromInstance directly with the extra instance context.
|
||||
func (s *ImageServiceImpl) CreateImage(input *ec2.CreateImageInput) (*ec2.CreateImageOutput, error) {
|
||||
func (s *ImageServiceImpl) CreateImage(input *ec2.CreateImageInput, accountID string) (*ec2.CreateImageOutput, error) {
|
||||
return nil, errors.New("CreateImage requires instance context; use CreateImageFromInstance on daemon side")
|
||||
}
|
||||
|
||||
// CreateImageFromInstance creates an AMI from an instance by snapshotting the root
|
||||
// volume and storing a new AMI config in S3.
|
||||
func (s *ImageServiceImpl) CreateImageFromInstance(params CreateImageParams) (*ec2.CreateImageOutput, error) {
|
||||
func (s *ImageServiceImpl) CreateImageFromInstance(params CreateImageParams, accountID string) (*ec2.CreateImageOutput, error) {
|
||||
input := params.Input
|
||||
if input == nil || input.InstanceId == nil {
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
@@ -312,7 +341,7 @@ func (s *ImageServiceImpl) CreateImageFromInstance(params CreateImageParams) (*e
|
||||
}
|
||||
|
||||
// Step 4: Store snapshot metadata
|
||||
if err := s.putSnapshotMetadata(snapshotID, params.RootVolumeID, volumeSizeGiB); err != nil {
|
||||
if err := s.putSnapshotMetadata(snapshotID, params.RootVolumeID, volumeSizeGiB, accountID); err != nil {
|
||||
slog.Error("CreateImageFromInstance: failed to write snapshot metadata", "snapshotId", snapshotID, "err", err)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
@@ -333,7 +362,7 @@ func (s *ImageServiceImpl) CreateImageFromInstance(params CreateImageParams) (*e
|
||||
VolumeSizeGiB: volumeSizeGiB,
|
||||
CreationDate: time.Now(),
|
||||
RootDeviceType: "ebs",
|
||||
ImageOwnerAlias: "self",
|
||||
ImageOwnerAlias: accountID,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -538,7 +567,7 @@ func (s *ImageServiceImpl) GetAMIConfig(imageID string) (viperblock.AMIMetadata,
|
||||
}
|
||||
|
||||
// putSnapshotMetadata stores snapshot metadata in S3 using the canonical SnapshotConfig type
|
||||
func (s *ImageServiceImpl) putSnapshotMetadata(snapshotID, volumeID string, volumeSizeGiB uint64) error {
|
||||
func (s *ImageServiceImpl) putSnapshotMetadata(snapshotID, volumeID string, volumeSizeGiB uint64, accountID string) error {
|
||||
cfg := handlers_ec2_snapshot.SnapshotConfig{
|
||||
SnapshotID: snapshotID,
|
||||
VolumeID: volumeID,
|
||||
@@ -546,7 +575,7 @@ func (s *ImageServiceImpl) putSnapshotMetadata(snapshotID, volumeID string, volu
|
||||
State: "completed",
|
||||
Progress: "100%",
|
||||
StartTime: time.Now(),
|
||||
OwnerID: s.accountID,
|
||||
OwnerID: accountID,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(cfg)
|
||||
@@ -564,26 +593,40 @@ func (s *ImageServiceImpl) putSnapshotMetadata(snapshotID, volumeID string, volu
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ImageServiceImpl) CopyImage(input *ec2.CopyImageInput) (*ec2.CopyImageOutput, error) {
|
||||
// isAccountID returns true if the string looks like a valid Hive account ID
|
||||
// (12-digit zero-padded number). Pre-phase4 values like "self" or "hive" return false.
|
||||
func isAccountID(s string) bool {
|
||||
if len(s) != 12 {
|
||||
return false
|
||||
}
|
||||
for _, c := range s {
|
||||
if c < '0' || c > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *ImageServiceImpl) CopyImage(input *ec2.CopyImageInput, accountID string) (*ec2.CopyImageOutput, error) {
|
||||
return nil, errors.New("CopyImage not yet implemented")
|
||||
}
|
||||
|
||||
func (s *ImageServiceImpl) DescribeImageAttribute(input *ec2.DescribeImageAttributeInput) (*ec2.DescribeImageAttributeOutput, error) {
|
||||
func (s *ImageServiceImpl) DescribeImageAttribute(input *ec2.DescribeImageAttributeInput, accountID string) (*ec2.DescribeImageAttributeOutput, error) {
|
||||
return nil, errors.New("DescribeImageAttribute not yet implemented")
|
||||
}
|
||||
|
||||
func (s *ImageServiceImpl) RegisterImage(input *ec2.RegisterImageInput) (*ec2.RegisterImageOutput, error) {
|
||||
func (s *ImageServiceImpl) RegisterImage(input *ec2.RegisterImageInput, accountID string) (*ec2.RegisterImageOutput, error) {
|
||||
return nil, errors.New("RegisterImage not yet implemented")
|
||||
}
|
||||
|
||||
func (s *ImageServiceImpl) DeregisterImage(input *ec2.DeregisterImageInput) (*ec2.DeregisterImageOutput, error) {
|
||||
func (s *ImageServiceImpl) DeregisterImage(input *ec2.DeregisterImageInput, accountID string) (*ec2.DeregisterImageOutput, error) {
|
||||
return nil, errors.New("DeregisterImage not yet implemented")
|
||||
}
|
||||
|
||||
func (s *ImageServiceImpl) ModifyImageAttribute(input *ec2.ModifyImageAttributeInput) (*ec2.ModifyImageAttributeOutput, error) {
|
||||
func (s *ImageServiceImpl) ModifyImageAttribute(input *ec2.ModifyImageAttributeInput, accountID string) (*ec2.ModifyImageAttributeOutput, error) {
|
||||
return nil, errors.New("ModifyImageAttribute not yet implemented")
|
||||
}
|
||||
|
||||
func (s *ImageServiceImpl) ResetImageAttribute(input *ec2.ResetImageAttributeInput) (*ec2.ResetImageAttributeOutput, error) {
|
||||
func (s *ImageServiceImpl) ResetImageAttribute(input *ec2.ResetImageAttributeInput, accountID string) (*ec2.ResetImageAttributeOutput, error) {
|
||||
return nil, errors.New("ResetImageAttribute not yet implemented")
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@ import (
|
||||
)
|
||||
|
||||
const testBucket = "test-bucket"
|
||||
const testAccountID = "123456789"
|
||||
const testAccountID = "000000000001"
|
||||
|
||||
// setupTestImageService creates an image service with in-memory storage for testing
|
||||
func setupTestImageService(t *testing.T) (*ImageServiceImpl, *objectstore.MemoryObjectStore) {
|
||||
store := objectstore.NewMemoryObjectStore()
|
||||
svc := NewImageServiceImplWithStore(store, testBucket, testAccountID)
|
||||
svc := NewImageServiceImplWithStore(store, testBucket)
|
||||
return svc, store
|
||||
}
|
||||
|
||||
@@ -101,10 +101,38 @@ func createTestAMIConfigWithName(t *testing.T, store *objectstore.MemoryObjectSt
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// createTestAMIConfigWithOwner creates a test AMI config with a specified name and owner
|
||||
func createTestAMIConfigWithOwner(t *testing.T, store *objectstore.MemoryObjectStore, imageID, name, owner string) {
|
||||
amiState := viperblock.VBState{
|
||||
VolumeConfig: viperblock.VolumeConfig{
|
||||
AMIMetadata: viperblock.AMIMetadata{
|
||||
ImageID: imageID,
|
||||
Name: name,
|
||||
Architecture: "x86_64",
|
||||
PlatformDetails: "Linux/UNIX",
|
||||
Virtualization: "hvm",
|
||||
RootDeviceType: "ebs",
|
||||
VolumeSizeGiB: 8,
|
||||
ImageOwnerAlias: owner,
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := json.Marshal(amiState)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = store.PutObject(&awss3.PutObjectInput{
|
||||
Bucket: aws.String(testBucket),
|
||||
Key: aws.String(imageID + "/config.json"),
|
||||
Body: strings.NewReader(string(data)),
|
||||
ContentType: aws.String("application/json"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCreateImageFromInstance_NilInput(t *testing.T) {
|
||||
svc, _ := setupTestImageService(t)
|
||||
|
||||
_, err := svc.CreateImageFromInstance(CreateImageParams{})
|
||||
_, err := svc.CreateImageFromInstance(CreateImageParams{}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidParameterValue, err.Error())
|
||||
}
|
||||
@@ -125,7 +153,7 @@ func TestCreateImageFromInstance_RunningInstance_NoNATS(t *testing.T) {
|
||||
RootVolumeID: "vol-root123",
|
||||
SourceImageID: "ami-source123",
|
||||
IsRunning: true,
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
|
||||
}
|
||||
@@ -142,7 +170,7 @@ func TestCreateImageFromInstance_StoppedInstance_NoConfig(t *testing.T) {
|
||||
RootVolumeID: "vol-nonexistent",
|
||||
SourceImageID: "ami-source123",
|
||||
IsRunning: false,
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
|
||||
}
|
||||
@@ -150,20 +178,21 @@ func TestCreateImageFromInstance_StoppedInstance_NoConfig(t *testing.T) {
|
||||
func TestDescribeImages_AfterCreate(t *testing.T) {
|
||||
svc, store := setupTestImageService(t)
|
||||
|
||||
// Manually create an AMI config in the store (simulates what CreateImageFromInstance does)
|
||||
// Manually create an AMI config with the caller as owner
|
||||
amiID := "ami-testimage123"
|
||||
createTestAMIConfig(t, store, amiID)
|
||||
createTestAMIConfigWithOwner(t, store, amiID, "test-ami", testAccountID)
|
||||
|
||||
// Describe images should find it
|
||||
result, err := svc.DescribeImages(&ec2.DescribeImagesInput{
|
||||
ImageIds: []*string{aws.String(amiID)},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Len(t, result.Images, 1)
|
||||
assert.Equal(t, amiID, *result.Images[0].ImageId)
|
||||
assert.Equal(t, "test-ami", *result.Images[0].Name)
|
||||
assert.Equal(t, "x86_64", *result.Images[0].Architecture)
|
||||
assert.Equal(t, testAccountID, *result.Images[0].OwnerId)
|
||||
}
|
||||
|
||||
func TestGetVolumeConfig(t *testing.T) {
|
||||
@@ -208,7 +237,7 @@ func TestGetAMIConfig_NotFound(t *testing.T) {
|
||||
func TestPutSnapshotMetadata(t *testing.T) {
|
||||
svc, store := setupTestImageService(t)
|
||||
|
||||
err := svc.putSnapshotMetadata("snap-abc123", "vol-xyz789", 10)
|
||||
err := svc.putSnapshotMetadata("snap-abc123", "vol-xyz789", 10, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the metadata was written correctly
|
||||
@@ -245,7 +274,7 @@ func TestCreateImageFromInstance_SourceAMIReadFailure(t *testing.T) {
|
||||
RootVolumeID: "vol-root123",
|
||||
SourceImageID: "ami-nonexistent",
|
||||
IsRunning: true, // will fail at snapshot step first (no NATS)
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
|
||||
}
|
||||
@@ -259,7 +288,7 @@ func TestDescribeImages_NotFound(t *testing.T) {
|
||||
// Request a non-existent AMI ID — should return InvalidAMIID.NotFound
|
||||
_, err := svc.DescribeImages(&ec2.DescribeImagesInput{
|
||||
ImageIds: []*string{aws.String("ami-nonexistent")},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidAMIIDNotFound, err.Error())
|
||||
}
|
||||
@@ -276,7 +305,7 @@ func TestDescribeImages_MixedExistingAndMissing(t *testing.T) {
|
||||
aws.String("ami-exists123"),
|
||||
aws.String("ami-missing456"),
|
||||
},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidAMIIDNotFound, err.Error())
|
||||
}
|
||||
@@ -284,38 +313,20 @@ func TestDescribeImages_MixedExistingAndMissing(t *testing.T) {
|
||||
func TestDescribeImages_FilterByOwnerSelf(t *testing.T) {
|
||||
svc, store := setupTestImageService(t)
|
||||
|
||||
// Create AMI with "self" owner alias
|
||||
amiID := "ami-selfowned"
|
||||
amiState := viperblock.VBState{
|
||||
VolumeConfig: viperblock.VolumeConfig{
|
||||
AMIMetadata: viperblock.AMIMetadata{
|
||||
ImageID: amiID,
|
||||
Name: "self-owned-ami",
|
||||
ImageOwnerAlias: "self",
|
||||
Architecture: "x86_64",
|
||||
PlatformDetails: "Linux/UNIX",
|
||||
RootDeviceType: "ebs",
|
||||
VolumeSizeGiB: 8,
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := json.Marshal(amiState)
|
||||
require.NoError(t, err)
|
||||
_, err = store.PutObject(&awss3.PutObjectInput{
|
||||
Bucket: aws.String(testBucket),
|
||||
Key: aws.String(amiID + "/config.json"),
|
||||
Body: strings.NewReader(string(data)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// Create an AMI owned by the caller's account
|
||||
createTestAMIConfigWithOwner(t, store, "ami-selfowned", "self-owned-ami", testAccountID)
|
||||
|
||||
// Filter by "self"
|
||||
// Create a system AMI (should not appear with --owners self)
|
||||
createTestAMIConfigWithOwner(t, store, "ami-system", "system-ami", "hive")
|
||||
|
||||
// Filter by "self" should return only the caller's AMI
|
||||
result, err := svc.DescribeImages(&ec2.DescribeImagesInput{
|
||||
Owners: []*string{aws.String("self")},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Len(t, result.Images, 1)
|
||||
assert.Equal(t, amiID, *result.Images[0].ImageId)
|
||||
assert.Equal(t, "ami-selfowned", *result.Images[0].ImageId)
|
||||
}
|
||||
|
||||
func TestCreateImageFromInstance_DuplicateName(t *testing.T) {
|
||||
@@ -335,7 +346,7 @@ func TestCreateImageFromInstance_DuplicateName(t *testing.T) {
|
||||
},
|
||||
RootVolumeID: "vol-root123",
|
||||
IsRunning: true,
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidAMINameDuplicate, err.Error())
|
||||
}
|
||||
@@ -358,8 +369,51 @@ func TestCreateImageFromInstance_UniqueNameAllowed(t *testing.T) {
|
||||
},
|
||||
RootVolumeID: "vol-root123",
|
||||
IsRunning: true,
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
// Should fail at snapshot, NOT at duplicate name check
|
||||
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
|
||||
}
|
||||
|
||||
func TestDescribeImages_AccountScoping(t *testing.T) {
|
||||
svc, store := setupTestImageService(t)
|
||||
|
||||
// Create an AMI owned by a specific account
|
||||
createTestAMIConfigWithOwner(t, store, "ami-scoped123", "test-ami", "000000000001")
|
||||
|
||||
// DescribeImages from the owning account should return the image with correct OwnerId
|
||||
result, err := svc.DescribeImages(&ec2.DescribeImagesInput{
|
||||
ImageIds: []*string{aws.String("ami-scoped123")},
|
||||
}, "000000000001")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Images, 1)
|
||||
assert.Equal(t, "000000000001", *result.Images[0].OwnerId)
|
||||
|
||||
// DescribeImages from a DIFFERENT account should NOT see the image
|
||||
_, err = svc.DescribeImages(&ec2.DescribeImagesInput{
|
||||
ImageIds: []*string{aws.String("ami-scoped123")},
|
||||
}, "000000000002")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidAMIIDNotFound, err.Error())
|
||||
}
|
||||
|
||||
func TestDescribeImages_SystemAMIVisibleToAll(t *testing.T) {
|
||||
svc, store := setupTestImageService(t)
|
||||
|
||||
// Create a system/pre-phase4 AMI (non-account-ID owner)
|
||||
createTestAMIConfigWithOwner(t, store, "ami-system123", "system-ami", "hive")
|
||||
|
||||
// Any account should be able to see system AMIs
|
||||
result, err := svc.DescribeImages(&ec2.DescribeImagesInput{
|
||||
ImageIds: []*string{aws.String("ami-system123")},
|
||||
}, "000000000001")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Images, 1)
|
||||
assert.Equal(t, "000000000000", *result.Images[0].OwnerId) // System AMIs report global account
|
||||
|
||||
result2, err := svc.DescribeImages(&ec2.DescribeImagesInput{
|
||||
ImageIds: []*string{aws.String("ami-system123")},
|
||||
}, "000000000002")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result2.Images, 1)
|
||||
}
|
||||
|
||||
@@ -19,34 +19,34 @@ func NewNATSImageService(conn *nats.Conn) ImageService {
|
||||
return &NATSImageService{natsConn: conn}
|
||||
}
|
||||
|
||||
func (s *NATSImageService) DescribeImages(input *ec2.DescribeImagesInput) (*ec2.DescribeImagesOutput, error) {
|
||||
return utils.NATSRequest[ec2.DescribeImagesOutput](s.natsConn, "ec2.DescribeImages", input, 30*time.Second)
|
||||
func (s *NATSImageService) DescribeImages(input *ec2.DescribeImagesInput, accountID string) (*ec2.DescribeImagesOutput, error) {
|
||||
return utils.NATSRequest[ec2.DescribeImagesOutput](s.natsConn, "ec2.DescribeImages", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSImageService) CreateImage(input *ec2.CreateImageInput) (*ec2.CreateImageOutput, error) {
|
||||
return utils.NATSRequest[ec2.CreateImageOutput](s.natsConn, "ec2.CreateImage", input, 120*time.Second)
|
||||
func (s *NATSImageService) CreateImage(input *ec2.CreateImageInput, accountID string) (*ec2.CreateImageOutput, error) {
|
||||
return utils.NATSRequest[ec2.CreateImageOutput](s.natsConn, "ec2.CreateImage", input, 120*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSImageService) CopyImage(input *ec2.CopyImageInput) (*ec2.CopyImageOutput, error) {
|
||||
func (s *NATSImageService) CopyImage(input *ec2.CopyImageInput, accountID string) (*ec2.CopyImageOutput, error) {
|
||||
return nil, errors.New("CopyImage not yet implemented")
|
||||
}
|
||||
|
||||
func (s *NATSImageService) DescribeImageAttribute(input *ec2.DescribeImageAttributeInput) (*ec2.DescribeImageAttributeOutput, error) {
|
||||
func (s *NATSImageService) DescribeImageAttribute(input *ec2.DescribeImageAttributeInput, accountID string) (*ec2.DescribeImageAttributeOutput, error) {
|
||||
return nil, errors.New("DescribeImageAttribute not yet implemented")
|
||||
}
|
||||
|
||||
func (s *NATSImageService) RegisterImage(input *ec2.RegisterImageInput) (*ec2.RegisterImageOutput, error) {
|
||||
func (s *NATSImageService) RegisterImage(input *ec2.RegisterImageInput, accountID string) (*ec2.RegisterImageOutput, error) {
|
||||
return nil, errors.New("RegisterImage not yet implemented")
|
||||
}
|
||||
|
||||
func (s *NATSImageService) DeregisterImage(input *ec2.DeregisterImageInput) (*ec2.DeregisterImageOutput, error) {
|
||||
func (s *NATSImageService) DeregisterImage(input *ec2.DeregisterImageInput, accountID string) (*ec2.DeregisterImageOutput, error) {
|
||||
return nil, errors.New("DeregisterImage not yet implemented")
|
||||
}
|
||||
|
||||
func (s *NATSImageService) ModifyImageAttribute(input *ec2.ModifyImageAttributeInput) (*ec2.ModifyImageAttributeOutput, error) {
|
||||
func (s *NATSImageService) ModifyImageAttribute(input *ec2.ModifyImageAttributeInput, accountID string) (*ec2.ModifyImageAttributeOutput, error) {
|
||||
return nil, errors.New("ModifyImageAttribute not yet implemented")
|
||||
}
|
||||
|
||||
func (s *NATSImageService) ResetImageAttribute(input *ec2.ResetImageAttributeInput) (*ec2.ResetImageAttributeOutput, error) {
|
||||
func (s *NATSImageService) ResetImageAttribute(input *ec2.ResetImageAttributeInput, accountID string) (*ec2.ResetImageAttributeOutput, error) {
|
||||
return nil, errors.New("ResetImageAttribute not yet implemented")
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ import "github.com/aws/aws-sdk-go/service/ec2"
|
||||
|
||||
// InstanceService defines the interface for EC2 instance operations business logic
|
||||
type InstanceService interface {
|
||||
RunInstances(input *ec2.RunInstancesInput) (*ec2.Reservation, error)
|
||||
RunInstances(input *ec2.RunInstancesInput, accountID string) (*ec2.Reservation, error)
|
||||
}
|
||||
|
||||
@@ -507,7 +507,7 @@ func (s *InstanceServiceImpl) prepareCloudInitVolume(input *ec2.RunInstancesInpu
|
||||
}
|
||||
|
||||
// Create the cloud-init ISO
|
||||
err = s.createCloudInitISO(input, instance.ID, cloudInitVb)
|
||||
err = s.createCloudInitISO(input, instance.ID, instance.AccountID, cloudInitVb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -534,7 +534,7 @@ func (s *InstanceServiceImpl) prepareCloudInitVolume(input *ec2.RunInstancesInpu
|
||||
}
|
||||
|
||||
// createCloudInitISO generates the cloud-init ISO image
|
||||
func (s *InstanceServiceImpl) createCloudInitISO(input *ec2.RunInstancesInput, instanceId string, cloudInitVb *viperblock.VB) error {
|
||||
func (s *InstanceServiceImpl) createCloudInitISO(input *ec2.RunInstancesInput, instanceId string, accountID string, cloudInitVb *viperblock.VB) error {
|
||||
// Create ISO writer
|
||||
writer, err := iso9660.NewWriter()
|
||||
if err != nil {
|
||||
@@ -552,8 +552,7 @@ func (s *InstanceServiceImpl) createCloudInitISO(input *ec2.RunInstancesInput, i
|
||||
keyName = *input.KeyName
|
||||
}
|
||||
|
||||
// TODO: Mock for account ID, replace with real account ID retrieval
|
||||
keyPath := fmt.Sprintf("/keys/123456789/%s", keyName)
|
||||
keyPath := fmt.Sprintf("keys/%s/%s", accountID, keyName)
|
||||
result, err := s.objectStore.GetObject(&awss3.GetObjectInput{
|
||||
Bucket: aws.String(s.config.Predastore.Bucket),
|
||||
Key: aws.String(keyPath),
|
||||
|
||||
@@ -13,7 +13,7 @@ func NewMockInstanceService() InstanceService {
|
||||
return &MockInstanceService{}
|
||||
}
|
||||
|
||||
func (s *MockInstanceService) RunInstances(input *ec2.RunInstancesInput) (*ec2.Reservation, error) {
|
||||
func (s *MockInstanceService) RunInstances(input *ec2.RunInstancesInput, accountID string) (*ec2.Reservation, error) {
|
||||
instance := &ec2.Instance{
|
||||
InstanceId: aws.String("i-0123456789abcdef0"),
|
||||
State: &ec2.InstanceState{
|
||||
@@ -28,7 +28,7 @@ func (s *MockInstanceService) RunInstances(input *ec2.RunInstancesInput) (*ec2.R
|
||||
|
||||
reservation := &ec2.Reservation{
|
||||
Instances: []*ec2.Instance{instance},
|
||||
OwnerId: aws.String("123456789012"),
|
||||
OwnerId: aws.String(accountID),
|
||||
}
|
||||
|
||||
return reservation, nil
|
||||
|
||||
@@ -20,10 +20,10 @@ func NewNATSInstanceService(conn *nats.Conn) InstanceService {
|
||||
return &NATSInstanceService{natsConn: conn}
|
||||
}
|
||||
|
||||
func (s *NATSInstanceService) RunInstances(input *ec2.RunInstancesInput) (*ec2.Reservation, error) {
|
||||
func (s *NATSInstanceService) RunInstances(input *ec2.RunInstancesInput, accountID string) (*ec2.Reservation, error) {
|
||||
if input == nil || input.InstanceType == nil {
|
||||
return nil, fmt.Errorf("instance type is required")
|
||||
}
|
||||
topic := fmt.Sprintf("ec2.RunInstances.%s", aws.StringValue(input.InstanceType))
|
||||
return utils.NATSRequest[ec2.Reservation](s.natsConn, topic, input, 5*time.Minute)
|
||||
return utils.NATSRequest[ec2.Reservation](s.natsConn, topic, input, 5*time.Minute, accountID)
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ func TestNATSInstanceService_RunInstances_Success(t *testing.T) {
|
||||
input := createValidRunInstancesInput()
|
||||
|
||||
// Call RunInstances
|
||||
reservation, err := service.RunInstances(input)
|
||||
reservation, err := service.RunInstances(input, "123456789012")
|
||||
|
||||
// Verify success
|
||||
require.NoError(t, err, "RunInstances should succeed")
|
||||
@@ -172,7 +172,7 @@ func TestNATSInstanceService_RunInstances_DaemonError(t *testing.T) {
|
||||
}
|
||||
|
||||
// Call RunInstances
|
||||
reservation, err := service.RunInstances(input)
|
||||
reservation, err := service.RunInstances(input, "123456789012")
|
||||
|
||||
// Verify error handling - error should be just the AWS error code for gateway lookup
|
||||
require.Error(t, err, "Should return error when daemon returns error")
|
||||
@@ -200,7 +200,7 @@ func TestNATSInstanceService_RunInstances_NoSubscriber(t *testing.T) {
|
||||
|
||||
// Call RunInstances
|
||||
start := time.Now()
|
||||
reservation, err := service.RunInstances(input)
|
||||
reservation, err := service.RunInstances(input, "123456789012")
|
||||
duration := time.Since(start)
|
||||
|
||||
// Verify error behavior
|
||||
@@ -237,7 +237,7 @@ func TestNATSInstanceService_RunInstances_InvalidResponse(t *testing.T) {
|
||||
input := createValidRunInstancesInput()
|
||||
|
||||
// Call RunInstances
|
||||
reservation, err := service.RunInstances(input)
|
||||
reservation, err := service.RunInstances(input, "123456789012")
|
||||
|
||||
// Verify error handling
|
||||
require.Error(t, err, "Should return error for invalid JSON")
|
||||
@@ -261,7 +261,7 @@ func TestNATSInstanceService_RunInstances_MarshalError(t *testing.T) {
|
||||
service := NewNATSInstanceService(nc)
|
||||
|
||||
// Nil input should be rejected before hitting NATS
|
||||
reservation, err := service.RunInstances(nil)
|
||||
reservation, err := service.RunInstances(nil, "123456789012")
|
||||
require.Error(t, err, "Should handle nil input")
|
||||
assert.Nil(t, reservation)
|
||||
assert.Contains(t, err.Error(), "instance type is required")
|
||||
@@ -318,7 +318,7 @@ func TestNATSInstanceService_RunInstances_MultipleInstances(t *testing.T) {
|
||||
input.MinCount = aws.Int64(3)
|
||||
input.MaxCount = aws.Int64(3)
|
||||
|
||||
reservation, err := service.RunInstances(input)
|
||||
reservation, err := service.RunInstances(input, "123456789012")
|
||||
|
||||
// Verify multiple instances returned
|
||||
require.NoError(t, err)
|
||||
@@ -365,7 +365,7 @@ func TestNATSInstanceService_RunInstances_QueueGroup(t *testing.T) {
|
||||
// Send multiple requests
|
||||
requestCount := 10
|
||||
for range requestCount {
|
||||
reservation, err := service.RunInstances(input)
|
||||
reservation, err := service.RunInstances(input, "123456789012")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, reservation)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import "github.com/aws/aws-sdk-go/service/ec2"
|
||||
|
||||
// KeyService defines the interface for EC2 key pair operations business logic
|
||||
type KeyService interface {
|
||||
CreateKeyPair(input *ec2.CreateKeyPairInput) (*ec2.CreateKeyPairOutput, error)
|
||||
DeleteKeyPair(input *ec2.DeleteKeyPairInput) (*ec2.DeleteKeyPairOutput, error)
|
||||
DescribeKeyPairs(input *ec2.DescribeKeyPairsInput) (*ec2.DescribeKeyPairsOutput, error)
|
||||
ImportKeyPair(input *ec2.ImportKeyPairInput) (*ec2.ImportKeyPairOutput, error)
|
||||
CreateKeyPair(input *ec2.CreateKeyPairInput, accountID string) (*ec2.CreateKeyPairOutput, error)
|
||||
DeleteKeyPair(input *ec2.DeleteKeyPairInput, accountID string) (*ec2.DeleteKeyPairOutput, error)
|
||||
DescribeKeyPairs(input *ec2.DescribeKeyPairsInput, accountID string) (*ec2.DescribeKeyPairsOutput, error)
|
||||
ImportKeyPair(input *ec2.ImportKeyPairInput, accountID string) (*ec2.ImportKeyPairOutput, error)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ type KeyServiceImpl struct {
|
||||
config *config.Config
|
||||
store objectstore.ObjectStore
|
||||
bucketName string
|
||||
accountID string // AWS account ID for S3 key storage path
|
||||
}
|
||||
|
||||
// NewKeyServiceImpl creates a new daemon-side key service
|
||||
@@ -49,21 +48,19 @@ func NewKeyServiceImpl(cfg *config.Config) *KeyServiceImpl {
|
||||
config: cfg,
|
||||
store: store,
|
||||
bucketName: cfg.Predastore.Bucket,
|
||||
accountID: "123456789", // TODO: Implement proper account ID management
|
||||
}
|
||||
}
|
||||
|
||||
// NewKeyServiceImplWithStore creates a key service with a custom object store (for testing)
|
||||
func NewKeyServiceImplWithStore(store objectstore.ObjectStore, bucketName, accountID string) *KeyServiceImpl {
|
||||
func NewKeyServiceImplWithStore(store objectstore.ObjectStore, bucketName string) *KeyServiceImpl {
|
||||
return &KeyServiceImpl{
|
||||
store: store,
|
||||
bucketName: bucketName,
|
||||
accountID: accountID,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateKeyPair generates a new SSH key pair using ssh-keygen
|
||||
func (s *KeyServiceImpl) CreateKeyPair(input *ec2.CreateKeyPairInput) (*ec2.CreateKeyPairOutput, error) {
|
||||
func (s *KeyServiceImpl) CreateKeyPair(input *ec2.CreateKeyPairInput, accountID string) (*ec2.CreateKeyPairOutput, error) {
|
||||
if input == nil || input.KeyName == nil {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
@@ -78,7 +75,7 @@ func (s *KeyServiceImpl) CreateKeyPair(input *ec2.CreateKeyPairInput) (*ec2.Crea
|
||||
}
|
||||
|
||||
// Check if key already exists in S3
|
||||
keyPath := fmt.Sprintf("keys/%s/%s", s.accountID, keyName)
|
||||
keyPath := fmt.Sprintf("keys/%s/%s", accountID, keyName)
|
||||
_, err := s.store.GetObject(&s3.GetObjectInput{
|
||||
Bucket: aws.String(s.bucketName),
|
||||
Key: aws.String(keyPath),
|
||||
@@ -174,7 +171,7 @@ func (s *KeyServiceImpl) CreateKeyPair(input *ec2.CreateKeyPairInput) (*ec2.Crea
|
||||
}
|
||||
|
||||
// Store metadata file (CreateKeyPairOutput without KeyMaterial) for keyPairId lookups
|
||||
err = s.storeKeyPairMetadata(keyPairID, &ec2.CreateKeyPairOutput{
|
||||
err = s.storeKeyPairMetadata(accountID, keyPairID, &ec2.CreateKeyPairOutput{
|
||||
KeyFingerprint: aws.String(fingerprint),
|
||||
KeyName: aws.String(keyName),
|
||||
KeyPairId: aws.String(keyPairID),
|
||||
@@ -237,9 +234,9 @@ func formatFingerprint(hash []byte, algorithm string) string {
|
||||
}
|
||||
|
||||
// storeKeyPairMetadata stores key pair metadata (without private key) to S3 for keyPairId lookups
|
||||
func (s *KeyServiceImpl) storeKeyPairMetadata(keyPairID string, metadata *ec2.CreateKeyPairOutput) error {
|
||||
func (s *KeyServiceImpl) storeKeyPairMetadata(accountID, keyPairID string, metadata *ec2.CreateKeyPairOutput) error {
|
||||
// Store metadata with keyPairId as filename for efficient lookup when keyPairId is provided
|
||||
metadataPath := fmt.Sprintf("keys/%s/%s.json", s.accountID, keyPairID)
|
||||
metadataPath := fmt.Sprintf("keys/%s/%s.json", accountID, keyPairID)
|
||||
|
||||
// Marshal metadata to JSON
|
||||
jsonData, err := json.Marshal(metadata)
|
||||
@@ -261,8 +258,8 @@ func (s *KeyServiceImpl) storeKeyPairMetadata(keyPairID string, metadata *ec2.Cr
|
||||
}
|
||||
|
||||
// getKeyNameFromKeyPairId retrieves the key name by directly reading the metadata file for a given keyPairId
|
||||
func (s *KeyServiceImpl) getKeyNameFromKeyPairId(keyPairID string) (string, error) {
|
||||
metadataPath := fmt.Sprintf("keys/%s/%s.json", s.accountID, keyPairID)
|
||||
func (s *KeyServiceImpl) getKeyNameFromKeyPairId(accountID, keyPairID string) (string, error) {
|
||||
metadataPath := fmt.Sprintf("keys/%s/%s.json", accountID, keyPairID)
|
||||
|
||||
// Get metadata from S3
|
||||
result, err := s.store.GetObject(&s3.GetObjectInput{
|
||||
@@ -300,8 +297,8 @@ func (s *KeyServiceImpl) getKeyNameFromKeyPairId(keyPairID string) (string, erro
|
||||
}
|
||||
|
||||
// findKeyPairIdFromKeyName finds the keyPairId by searching metadata files for a given keyName
|
||||
func (s *KeyServiceImpl) findKeyPairIdFromKeyName(keyName string) (string, error) {
|
||||
prefix := fmt.Sprintf("keys/%s/", s.accountID)
|
||||
func (s *KeyServiceImpl) findKeyPairIdFromKeyName(accountID, keyName string) (string, error) {
|
||||
prefix := fmt.Sprintf("keys/%s/", accountID)
|
||||
|
||||
// List all objects with the keys prefix
|
||||
result, err := s.store.ListObjects(&s3.ListObjectsInput{
|
||||
@@ -364,13 +361,13 @@ func (s *KeyServiceImpl) findKeyPairIdFromKeyName(keyName string) (string, error
|
||||
|
||||
// ValidateKeyPairExists checks if a key pair with the given name exists.
|
||||
// Returns nil if the key pair exists, or an error with ErrorInvalidKeyPairNotFound if not.
|
||||
func (s *KeyServiceImpl) ValidateKeyPairExists(keyName string) error {
|
||||
_, err := s.findKeyPairIdFromKeyName(keyName)
|
||||
func (s *KeyServiceImpl) ValidateKeyPairExists(accountID, keyName string) error {
|
||||
_, err := s.findKeyPairIdFromKeyName(accountID, keyName)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteKeyPair removes a key pair (both public key and metadata from S3)
|
||||
func (s *KeyServiceImpl) DeleteKeyPair(input *ec2.DeleteKeyPairInput) (*ec2.DeleteKeyPairOutput, error) {
|
||||
func (s *KeyServiceImpl) DeleteKeyPair(input *ec2.DeleteKeyPairInput, accountID string) (*ec2.DeleteKeyPairOutput, error) {
|
||||
if input == nil {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
@@ -391,7 +388,7 @@ func (s *KeyServiceImpl) DeleteKeyPair(input *ec2.DeleteKeyPairInput) (*ec2.Dele
|
||||
return nil, errors.New(awserrors.ErrorInvalidKeyPairFormat)
|
||||
}
|
||||
|
||||
keyName, err = s.getKeyNameFromKeyPairId(keyPairID)
|
||||
keyName, err = s.getKeyNameFromKeyPairId(accountID, keyPairID)
|
||||
if err != nil {
|
||||
// AWS DeleteKeyPair is idempotent — return success for non-existent keys
|
||||
if err.Error() == awserrors.ErrorInvalidKeyPairNotFound {
|
||||
@@ -411,7 +408,7 @@ func (s *KeyServiceImpl) DeleteKeyPair(input *ec2.DeleteKeyPairInput) (*ec2.Dele
|
||||
return nil, errors.New(awserrors.ErrorInvalidKeyPairFormat)
|
||||
}
|
||||
|
||||
keyPairID, err = s.findKeyPairIdFromKeyName(keyName)
|
||||
keyPairID, err = s.findKeyPairIdFromKeyName(accountID, keyName)
|
||||
if err != nil {
|
||||
// AWS DeleteKeyPair is idempotent — return success for non-existent keys
|
||||
if err.Error() == awserrors.ErrorInvalidKeyPairNotFound {
|
||||
@@ -428,7 +425,7 @@ func (s *KeyServiceImpl) DeleteKeyPair(input *ec2.DeleteKeyPairInput) (*ec2.Dele
|
||||
slog.Info("Deleting key pair", "keyName", keyName, "keyPairId", keyPairID)
|
||||
|
||||
// Delete public key
|
||||
publicKeyPath := fmt.Sprintf("keys/%s/%s", s.accountID, keyName)
|
||||
publicKeyPath := fmt.Sprintf("keys/%s/%s", accountID, keyName)
|
||||
_, err = s.store.DeleteObject(&s3.DeleteObjectInput{
|
||||
Bucket: aws.String(s.bucketName),
|
||||
Key: aws.String(publicKeyPath),
|
||||
@@ -439,7 +436,7 @@ func (s *KeyServiceImpl) DeleteKeyPair(input *ec2.DeleteKeyPairInput) (*ec2.Dele
|
||||
}
|
||||
|
||||
// Delete metadata file (stored with keyPairID)
|
||||
metadataPath := fmt.Sprintf("keys/%s/%s.json", s.accountID, keyPairID)
|
||||
metadataPath := fmt.Sprintf("keys/%s/%s.json", accountID, keyPairID)
|
||||
_, err = s.store.DeleteObject(&s3.DeleteObjectInput{
|
||||
Bucket: aws.String(s.bucketName),
|
||||
Key: aws.String(metadataPath),
|
||||
@@ -455,14 +452,14 @@ func (s *KeyServiceImpl) DeleteKeyPair(input *ec2.DeleteKeyPairInput) (*ec2.Dele
|
||||
}
|
||||
|
||||
// DescribeKeyPairs lists available key pairs by reading metadata files from S3
|
||||
func (s *KeyServiceImpl) DescribeKeyPairs(input *ec2.DescribeKeyPairsInput) (*ec2.DescribeKeyPairsOutput, error) {
|
||||
func (s *KeyServiceImpl) DescribeKeyPairs(input *ec2.DescribeKeyPairsInput, accountID string) (*ec2.DescribeKeyPairsOutput, error) {
|
||||
if input == nil {
|
||||
input = &ec2.DescribeKeyPairsInput{}
|
||||
}
|
||||
|
||||
slog.Info("Describing key pairs", "filters", input.Filters)
|
||||
|
||||
prefix := fmt.Sprintf("keys/%s/", s.accountID)
|
||||
prefix := fmt.Sprintf("keys/%s/", accountID)
|
||||
|
||||
// List all objects with the keys prefix
|
||||
result, err := s.store.ListObjects(&s3.ListObjectsInput{
|
||||
@@ -574,7 +571,7 @@ func (s *KeyServiceImpl) DescribeKeyPairs(input *ec2.DescribeKeyPairsInput) (*ec
|
||||
}
|
||||
|
||||
// ImportKeyPair imports an existing public key
|
||||
func (s *KeyServiceImpl) ImportKeyPair(input *ec2.ImportKeyPairInput) (*ec2.ImportKeyPairOutput, error) {
|
||||
func (s *KeyServiceImpl) ImportKeyPair(input *ec2.ImportKeyPairInput, accountID string) (*ec2.ImportKeyPairOutput, error) {
|
||||
if input == nil || input.KeyName == nil {
|
||||
return nil, errors.New(awserrors.ErrorMissingParameter)
|
||||
}
|
||||
@@ -593,7 +590,7 @@ func (s *KeyServiceImpl) ImportKeyPair(input *ec2.ImportKeyPairInput) (*ec2.Impo
|
||||
}
|
||||
|
||||
// Check if key already exists in S3
|
||||
keyPath := fmt.Sprintf("keys/%s/%s", s.accountID, keyName)
|
||||
keyPath := fmt.Sprintf("keys/%s/%s", accountID, keyName)
|
||||
_, err := s.store.GetObject(&s3.GetObjectInput{
|
||||
Bucket: aws.String(s.bucketName),
|
||||
Key: aws.String(keyPath),
|
||||
@@ -674,7 +671,7 @@ func (s *KeyServiceImpl) ImportKeyPair(input *ec2.ImportKeyPairInput) (*ec2.Impo
|
||||
KeyPairId: aws.String(keyPairID),
|
||||
}
|
||||
|
||||
err = s.storeKeyPairMetadata(keyPairID, metadataOutput)
|
||||
err = s.storeKeyPairMetadata(accountID, keyPairID, metadataOutput)
|
||||
if err != nil {
|
||||
slog.Error("Failed to store key pair metadata", "err", err, "keyPairId", keyPairID)
|
||||
// Try to cleanup the public key we just uploaded
|
||||
|
||||
@@ -28,7 +28,7 @@ const testRSAPubKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDP9LrByKWpgbX+prxBw
|
||||
|
||||
func newTestKeyService() (*KeyServiceImpl, *objectstore.MemoryObjectStore) {
|
||||
store := objectstore.NewMemoryObjectStore()
|
||||
svc := NewKeyServiceImplWithStore(store, testBucket, testAccountID)
|
||||
svc := NewKeyServiceImplWithStore(store, testBucket)
|
||||
return svc, store
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func importTestKey(t *testing.T, svc *KeyServiceImpl, keyName string) *ec2.Impor
|
||||
out, err := svc.ImportKeyPair(&ec2.ImportKeyPairInput{
|
||||
KeyName: aws.String(keyName),
|
||||
PublicKeyMaterial: []byte(testED25519PubKey),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out)
|
||||
return out
|
||||
@@ -83,7 +83,7 @@ func TestCreateKeyPair_ED25519(t *testing.T) {
|
||||
|
||||
out, err := svc.CreateKeyPair(&ec2.CreateKeyPairInput{
|
||||
KeyName: aws.String("my-ed25519-key"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out)
|
||||
|
||||
@@ -121,7 +121,7 @@ func TestCreateKeyPair_RSA(t *testing.T) {
|
||||
out, err := svc.CreateKeyPair(&ec2.CreateKeyPairInput{
|
||||
KeyName: aws.String("my-rsa-key"),
|
||||
KeyType: aws.String("rsa"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out)
|
||||
|
||||
@@ -134,7 +134,7 @@ func TestCreateKeyPair_RSA(t *testing.T) {
|
||||
func TestCreateKeyPair_NilInput(t *testing.T) {
|
||||
svc, _ := newTestKeyService()
|
||||
|
||||
out, err := svc.CreateKeyPair(nil)
|
||||
out, err := svc.CreateKeyPair(nil, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, out)
|
||||
assert.Equal(t, awserrors.ErrorMissingParameter, err.Error())
|
||||
@@ -143,7 +143,7 @@ func TestCreateKeyPair_NilInput(t *testing.T) {
|
||||
func TestCreateKeyPair_MissingKeyName(t *testing.T) {
|
||||
svc, _ := newTestKeyService()
|
||||
|
||||
out, err := svc.CreateKeyPair(&ec2.CreateKeyPairInput{})
|
||||
out, err := svc.CreateKeyPair(&ec2.CreateKeyPairInput{}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, out)
|
||||
assert.Equal(t, awserrors.ErrorMissingParameter, err.Error())
|
||||
@@ -154,7 +154,7 @@ func TestCreateKeyPair_InvalidKeyName(t *testing.T) {
|
||||
|
||||
out, err := svc.CreateKeyPair(&ec2.CreateKeyPairInput{
|
||||
KeyName: aws.String("invalid key name!@#"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, out)
|
||||
assert.Equal(t, awserrors.ErrorInvalidKeyPairFormat, err.Error())
|
||||
@@ -166,12 +166,12 @@ func TestCreateKeyPair_Duplicate(t *testing.T) {
|
||||
|
||||
_, err := svc.CreateKeyPair(&ec2.CreateKeyPairInput{
|
||||
KeyName: aws.String("dup-key"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := svc.CreateKeyPair(&ec2.CreateKeyPairInput{
|
||||
KeyName: aws.String("dup-key"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, out)
|
||||
assert.Equal(t, awserrors.ErrorInvalidKeyPairDuplicate, err.Error())
|
||||
@@ -183,7 +183,7 @@ func TestCreateKeyPair_InvalidKeyType(t *testing.T) {
|
||||
out, err := svc.CreateKeyPair(&ec2.CreateKeyPairInput{
|
||||
KeyName: aws.String("bad-type-key"),
|
||||
KeyType: aws.String("dsa"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, out)
|
||||
assert.Equal(t, awserrors.ErrorInvalidParameterValue, err.Error())
|
||||
@@ -199,7 +199,7 @@ func TestImportKeyPair_Success_ED25519(t *testing.T) {
|
||||
out, err := svc.ImportKeyPair(&ec2.ImportKeyPairInput{
|
||||
KeyName: aws.String("imported-ed25519"),
|
||||
PublicKeyMaterial: []byte(testED25519PubKey),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out)
|
||||
|
||||
@@ -234,7 +234,7 @@ func TestImportKeyPair_Success_RSA(t *testing.T) {
|
||||
out, err := svc.ImportKeyPair(&ec2.ImportKeyPairInput{
|
||||
KeyName: aws.String("imported-rsa"),
|
||||
PublicKeyMaterial: []byte(testRSAPubKey),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out)
|
||||
|
||||
@@ -249,13 +249,13 @@ func TestImportKeyPair_Duplicate(t *testing.T) {
|
||||
_, err := svc.ImportKeyPair(&ec2.ImportKeyPairInput{
|
||||
KeyName: aws.String("dup-import"),
|
||||
PublicKeyMaterial: []byte(testED25519PubKey),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := svc.ImportKeyPair(&ec2.ImportKeyPairInput{
|
||||
KeyName: aws.String("dup-import"),
|
||||
PublicKeyMaterial: []byte(testED25519PubKey),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, out)
|
||||
assert.Equal(t, awserrors.ErrorInvalidKeyPairDuplicate, err.Error())
|
||||
@@ -267,7 +267,7 @@ func TestImportKeyPair_InvalidKeyName(t *testing.T) {
|
||||
out, err := svc.ImportKeyPair(&ec2.ImportKeyPairInput{
|
||||
KeyName: aws.String("bad name with spaces!"),
|
||||
PublicKeyMaterial: []byte(testED25519PubKey),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, out)
|
||||
assert.Equal(t, awserrors.ErrorInvalidKeyPairFormat, err.Error())
|
||||
@@ -308,7 +308,7 @@ func TestImportKeyPairInvalidKeyFormat(t *testing.T) {
|
||||
_, err := svc.ImportKeyPair(&ec2.ImportKeyPairInput{
|
||||
KeyName: aws.String("test-key"),
|
||||
PublicKeyMaterial: []byte(tt.publicKey),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, tt.expectedErrMsg, err.Error())
|
||||
})
|
||||
@@ -326,7 +326,7 @@ func TestDeleteKeyPair_ByKeyName(t *testing.T) {
|
||||
|
||||
result, err := svc.DeleteKeyPair(&ec2.DeleteKeyPairInput{
|
||||
KeyName: aws.String("to-delete-by-name"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
|
||||
@@ -354,7 +354,7 @@ func TestDeleteKeyPair_ByKeyPairId(t *testing.T) {
|
||||
|
||||
result, err := svc.DeleteKeyPair(&ec2.DeleteKeyPairInput{
|
||||
KeyPairId: imported.KeyPairId,
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
|
||||
@@ -381,7 +381,7 @@ func TestDeleteKeyPairIdempotent(t *testing.T) {
|
||||
t.Run("NonExistentKeyName", func(t *testing.T) {
|
||||
result, err := svc.DeleteKeyPair(&ec2.DeleteKeyPairInput{
|
||||
KeyName: aws.String("no-such-key"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
})
|
||||
@@ -389,7 +389,7 @@ func TestDeleteKeyPairIdempotent(t *testing.T) {
|
||||
t.Run("NonExistentKeyPairId", func(t *testing.T) {
|
||||
result, err := svc.DeleteKeyPair(&ec2.DeleteKeyPairInput{
|
||||
KeyPairId: aws.String("key-0123456789abcdef0"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
})
|
||||
@@ -398,7 +398,7 @@ func TestDeleteKeyPairIdempotent(t *testing.T) {
|
||||
func TestDeleteKeyPair_NilInput(t *testing.T) {
|
||||
svc, _ := newTestKeyService()
|
||||
|
||||
result, err := svc.DeleteKeyPair(nil)
|
||||
result, err := svc.DeleteKeyPair(nil, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Equal(t, awserrors.ErrorMissingParameter, err.Error())
|
||||
@@ -407,7 +407,7 @@ func TestDeleteKeyPair_NilInput(t *testing.T) {
|
||||
func TestDeleteKeyPair_EmptyNameAndId(t *testing.T) {
|
||||
svc, _ := newTestKeyService()
|
||||
|
||||
result, err := svc.DeleteKeyPair(&ec2.DeleteKeyPairInput{})
|
||||
result, err := svc.DeleteKeyPair(&ec2.DeleteKeyPairInput{}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Equal(t, awserrors.ErrorMissingParameter, err.Error())
|
||||
@@ -418,7 +418,7 @@ func TestDeleteKeyPair_InvalidKeyPairIdFormat(t *testing.T) {
|
||||
|
||||
result, err := svc.DeleteKeyPair(&ec2.DeleteKeyPairInput{
|
||||
KeyPairId: aws.String("bad id format!!!"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Equal(t, awserrors.ErrorInvalidKeyPairFormat, err.Error())
|
||||
@@ -431,7 +431,7 @@ func TestDeleteKeyPair_InvalidKeyPairIdFormat(t *testing.T) {
|
||||
func TestDescribeKeyPairs_Empty(t *testing.T) {
|
||||
svc, _ := newTestKeyService()
|
||||
|
||||
out, err := svc.DescribeKeyPairs(&ec2.DescribeKeyPairsInput{})
|
||||
out, err := svc.DescribeKeyPairs(&ec2.DescribeKeyPairsInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out)
|
||||
assert.Empty(t, out.KeyPairs)
|
||||
@@ -443,7 +443,7 @@ func TestDescribeKeyPairs_AllKeys(t *testing.T) {
|
||||
importTestKey(t, svc, "key-alpha")
|
||||
importTestKey(t, svc, "key-beta")
|
||||
|
||||
out, err := svc.DescribeKeyPairs(&ec2.DescribeKeyPairsInput{})
|
||||
out, err := svc.DescribeKeyPairs(&ec2.DescribeKeyPairsInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out)
|
||||
assert.Len(t, out.KeyPairs, 2)
|
||||
@@ -467,7 +467,7 @@ func TestDescribeKeyPairs_FilterByKeyName(t *testing.T) {
|
||||
|
||||
out, err := svc.DescribeKeyPairs(&ec2.DescribeKeyPairsInput{
|
||||
KeyNames: []*string{aws.String("find-me")},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out)
|
||||
require.Len(t, out.KeyPairs, 1)
|
||||
@@ -482,7 +482,7 @@ func TestDescribeKeyPairs_FilterByKeyPairId(t *testing.T) {
|
||||
|
||||
out, err := svc.DescribeKeyPairs(&ec2.DescribeKeyPairsInput{
|
||||
KeyPairIds: []*string{imported.KeyPairId},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out)
|
||||
require.Len(t, out.KeyPairs, 1)
|
||||
@@ -496,7 +496,7 @@ func TestDescribeKeyPairs_FilterNoMatch(t *testing.T) {
|
||||
|
||||
out, err := svc.DescribeKeyPairs(&ec2.DescribeKeyPairsInput{
|
||||
KeyNames: []*string{aws.String("does-not-exist")},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out)
|
||||
assert.Empty(t, out.KeyPairs)
|
||||
@@ -505,7 +505,7 @@ func TestDescribeKeyPairs_FilterNoMatch(t *testing.T) {
|
||||
func TestDescribeKeyPairs_NilInput(t *testing.T) {
|
||||
svc, _ := newTestKeyService()
|
||||
|
||||
out, err := svc.DescribeKeyPairs(nil)
|
||||
out, err := svc.DescribeKeyPairs(nil, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, out)
|
||||
assert.Empty(t, out.KeyPairs)
|
||||
@@ -520,14 +520,14 @@ func TestValidateKeyPairExists_Found(t *testing.T) {
|
||||
|
||||
importTestKey(t, svc, "existing-key")
|
||||
|
||||
err := svc.ValidateKeyPairExists("existing-key")
|
||||
err := svc.ValidateKeyPairExists(testAccountID, "existing-key")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidateKeyPairExists_NotFound(t *testing.T) {
|
||||
svc, _ := newTestKeyService()
|
||||
|
||||
err := svc.ValidateKeyPairExists("ghost-key")
|
||||
err := svc.ValidateKeyPairExists(testAccountID, "ghost-key")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidKeyPairNotFound, err.Error())
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ func NewMockKeyService() KeyService {
|
||||
return &MockKeyService{}
|
||||
}
|
||||
|
||||
func (s *MockKeyService) CreateKeyPair(input *ec2.CreateKeyPairInput) (*ec2.CreateKeyPairOutput, error) {
|
||||
func (s *MockKeyService) CreateKeyPair(input *ec2.CreateKeyPairInput, accountID string) (*ec2.CreateKeyPairOutput, error) {
|
||||
return &ec2.CreateKeyPairOutput{
|
||||
KeyFingerprint: aws.String("1f:51:ae:28:bf:89:e9:d8:1f:25:5d:37:2d:7d:b8:ca:9f:f5:f1:6f"),
|
||||
KeyMaterial: aws.String("-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----"),
|
||||
@@ -22,14 +22,14 @@ func (s *MockKeyService) CreateKeyPair(input *ec2.CreateKeyPairInput) (*ec2.Crea
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MockKeyService) DeleteKeyPair(input *ec2.DeleteKeyPairInput) (*ec2.DeleteKeyPairOutput, error) {
|
||||
func (s *MockKeyService) DeleteKeyPair(input *ec2.DeleteKeyPairInput, accountID string) (*ec2.DeleteKeyPairOutput, error) {
|
||||
return &ec2.DeleteKeyPairOutput{
|
||||
Return: aws.Bool(true),
|
||||
KeyPairId: input.KeyPairId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MockKeyService) DescribeKeyPairs(input *ec2.DescribeKeyPairsInput) (*ec2.DescribeKeyPairsOutput, error) {
|
||||
func (s *MockKeyService) DescribeKeyPairs(input *ec2.DescribeKeyPairsInput, accountID string) (*ec2.DescribeKeyPairsOutput, error) {
|
||||
return &ec2.DescribeKeyPairsOutput{
|
||||
KeyPairs: []*ec2.KeyPairInfo{
|
||||
{
|
||||
@@ -42,7 +42,7 @@ func (s *MockKeyService) DescribeKeyPairs(input *ec2.DescribeKeyPairsInput) (*ec
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MockKeyService) ImportKeyPair(input *ec2.ImportKeyPairInput) (*ec2.ImportKeyPairOutput, error) {
|
||||
func (s *MockKeyService) ImportKeyPair(input *ec2.ImportKeyPairInput, accountID string) (*ec2.ImportKeyPairOutput, error) {
|
||||
return &ec2.ImportKeyPairOutput{
|
||||
KeyFingerprint: aws.String("1f:51:ae:28:bf:89:e9:d8:1f:25:5d:37:2d:7d:b8:ca:9f:f5:f1:6f"),
|
||||
KeyName: input.KeyName,
|
||||
|
||||
@@ -18,18 +18,18 @@ func NewNATSKeyService(conn *nats.Conn) KeyService {
|
||||
return &NATSKeyService{natsConn: conn}
|
||||
}
|
||||
|
||||
func (s *NATSKeyService) CreateKeyPair(input *ec2.CreateKeyPairInput) (*ec2.CreateKeyPairOutput, error) {
|
||||
return utils.NATSRequest[ec2.CreateKeyPairOutput](s.natsConn, "ec2.CreateKeyPair", input, 30*time.Second)
|
||||
func (s *NATSKeyService) CreateKeyPair(input *ec2.CreateKeyPairInput, accountID string) (*ec2.CreateKeyPairOutput, error) {
|
||||
return utils.NATSRequest[ec2.CreateKeyPairOutput](s.natsConn, "ec2.CreateKeyPair", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSKeyService) DeleteKeyPair(input *ec2.DeleteKeyPairInput) (*ec2.DeleteKeyPairOutput, error) {
|
||||
return utils.NATSRequest[ec2.DeleteKeyPairOutput](s.natsConn, "ec2.DeleteKeyPair", input, 30*time.Second)
|
||||
func (s *NATSKeyService) DeleteKeyPair(input *ec2.DeleteKeyPairInput, accountID string) (*ec2.DeleteKeyPairOutput, error) {
|
||||
return utils.NATSRequest[ec2.DeleteKeyPairOutput](s.natsConn, "ec2.DeleteKeyPair", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSKeyService) DescribeKeyPairs(input *ec2.DescribeKeyPairsInput) (*ec2.DescribeKeyPairsOutput, error) {
|
||||
return utils.NATSRequest[ec2.DescribeKeyPairsOutput](s.natsConn, "ec2.DescribeKeyPairs", input, 30*time.Second)
|
||||
func (s *NATSKeyService) DescribeKeyPairs(input *ec2.DescribeKeyPairsInput, accountID string) (*ec2.DescribeKeyPairsOutput, error) {
|
||||
return utils.NATSRequest[ec2.DescribeKeyPairsOutput](s.natsConn, "ec2.DescribeKeyPairs", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
func (s *NATSKeyService) ImportKeyPair(input *ec2.ImportKeyPairInput) (*ec2.ImportKeyPairOutput, error) {
|
||||
return utils.NATSRequest[ec2.ImportKeyPairOutput](s.natsConn, "ec2.ImportKeyPair", input, 30*time.Second)
|
||||
func (s *NATSKeyService) ImportKeyPair(input *ec2.ImportKeyPairInput, accountID string) (*ec2.ImportKeyPairOutput, error) {
|
||||
return utils.NATSRequest[ec2.ImportKeyPairOutput](s.natsConn, "ec2.ImportKeyPair", input, 30*time.Second, accountID)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import "github.com/aws/aws-sdk-go/service/ec2"
|
||||
|
||||
// SnapshotService defines the interface for EC2 snapshot operations
|
||||
type SnapshotService interface {
|
||||
CreateSnapshot(input *ec2.CreateSnapshotInput) (*ec2.Snapshot, error)
|
||||
DescribeSnapshots(input *ec2.DescribeSnapshotsInput) (*ec2.DescribeSnapshotsOutput, error)
|
||||
DeleteSnapshot(input *ec2.DeleteSnapshotInput) (*ec2.DeleteSnapshotOutput, error)
|
||||
CopySnapshot(input *ec2.CopySnapshotInput) (*ec2.CopySnapshotOutput, error)
|
||||
CreateSnapshot(input *ec2.CreateSnapshotInput, accountID string) (*ec2.Snapshot, error)
|
||||
DescribeSnapshots(input *ec2.DescribeSnapshotsInput, accountID string) (*ec2.DescribeSnapshotsOutput, error)
|
||||
DeleteSnapshot(input *ec2.DeleteSnapshotInput, accountID string) (*ec2.DeleteSnapshotOutput, error)
|
||||
CopySnapshot(input *ec2.CopySnapshotInput, accountID string) (*ec2.CopySnapshotOutput, error)
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ func snapshotConfigToEC2(cfg *SnapshotConfig) *ec2.Snapshot {
|
||||
}
|
||||
|
||||
// CreateSnapshot creates a new snapshot from a volume
|
||||
func (s *SnapshotServiceImpl) CreateSnapshot(input *ec2.CreateSnapshotInput) (*ec2.Snapshot, error) {
|
||||
func (s *SnapshotServiceImpl) CreateSnapshot(input *ec2.CreateSnapshotInput, accountID string) (*ec2.Snapshot, error) {
|
||||
if input == nil || input.VolumeId == nil {
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
@@ -216,6 +216,12 @@ func (s *SnapshotServiceImpl) CreateSnapshot(input *ec2.CreateSnapshotInput) (*e
|
||||
}
|
||||
volumeConfig := volumeState.VolumeConfig
|
||||
|
||||
// Verify the caller owns the source volume
|
||||
if accountID != "" && volumeConfig.VolumeMetadata.TenantID != "" && volumeConfig.VolumeMetadata.TenantID != accountID {
|
||||
slog.Warn("CreateSnapshot: account does not own volume", "volumeId", volumeID, "accountID", accountID, "tenantID", volumeConfig.VolumeMetadata.TenantID)
|
||||
return nil, errors.New(awserrors.ErrorInvalidVolumeNotFound)
|
||||
}
|
||||
|
||||
if volumeConfig.VolumeMetadata.SizeGiB == 0 {
|
||||
slog.Error("CreateSnapshot: source volume has zero size in config", "volumeId", volumeID)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
@@ -266,7 +272,7 @@ func (s *SnapshotServiceImpl) CreateSnapshot(input *ec2.CreateSnapshotInput) (*e
|
||||
Progress: "100%",
|
||||
StartTime: now,
|
||||
Encrypted: volumeConfig.VolumeMetadata.IsEncrypted,
|
||||
OwnerID: s.config.Predastore.AccessKey,
|
||||
OwnerID: accountID,
|
||||
AvailabilityZone: volumeConfig.VolumeMetadata.AvailabilityZone,
|
||||
Tags: make(map[string]string),
|
||||
}
|
||||
@@ -303,12 +309,12 @@ func (s *SnapshotServiceImpl) CreateSnapshot(input *ec2.CreateSnapshotInput) (*e
|
||||
return snapshotConfigToEC2(snapshotCfg), nil
|
||||
}
|
||||
|
||||
// DescribeSnapshots lists snapshots matching the specified criteria
|
||||
func (s *SnapshotServiceImpl) DescribeSnapshots(input *ec2.DescribeSnapshotsInput) (*ec2.DescribeSnapshotsOutput, error) {
|
||||
// DescribeSnapshots lists snapshots matching the specified criteria, scoped to the caller's account.
|
||||
func (s *SnapshotServiceImpl) DescribeSnapshots(input *ec2.DescribeSnapshotsInput, accountID string) (*ec2.DescribeSnapshotsOutput, error) {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
slog.Info("DescribeSnapshots request", "snapshotIds", input.SnapshotIds)
|
||||
slog.Info("DescribeSnapshots request", "snapshotIds", input.SnapshotIds, "accountID", accountID)
|
||||
|
||||
snapshotIDFilter := make(map[string]bool)
|
||||
for _, id := range input.SnapshotIds {
|
||||
@@ -345,6 +351,11 @@ func (s *SnapshotServiceImpl) DescribeSnapshots(input *ec2.DescribeSnapshotsInpu
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter by account: only return snapshots owned by the caller
|
||||
if accountID != "" && cfg.OwnerID != "" && cfg.OwnerID != accountID {
|
||||
continue
|
||||
}
|
||||
|
||||
snapshots = append(snapshots, snapshotConfigToEC2(cfg))
|
||||
}
|
||||
|
||||
@@ -396,15 +407,15 @@ func (s *SnapshotServiceImpl) snapshotInUseByVolumes(snapshotID string) (bool, e
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// DeleteSnapshot deletes a snapshot
|
||||
func (s *SnapshotServiceImpl) DeleteSnapshot(input *ec2.DeleteSnapshotInput) (*ec2.DeleteSnapshotOutput, error) {
|
||||
// DeleteSnapshot deletes a snapshot after verifying the caller owns it.
|
||||
func (s *SnapshotServiceImpl) DeleteSnapshot(input *ec2.DeleteSnapshotInput, accountID string) (*ec2.DeleteSnapshotOutput, error) {
|
||||
if input == nil || input.SnapshotId == nil {
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
snapshotID := *input.SnapshotId
|
||||
|
||||
slog.Info("DeleteSnapshot request", "snapshotId", snapshotID)
|
||||
slog.Info("DeleteSnapshot request", "snapshotId", snapshotID, "accountID", accountID)
|
||||
|
||||
cfg, err := s.getSnapshotConfig(snapshotID)
|
||||
if err != nil {
|
||||
@@ -412,6 +423,12 @@ func (s *SnapshotServiceImpl) DeleteSnapshot(input *ec2.DeleteSnapshotInput) (*e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify ownership: caller must own the snapshot
|
||||
if accountID != "" && cfg.OwnerID != "" && cfg.OwnerID != accountID {
|
||||
slog.Warn("DeleteSnapshot: account does not own snapshot", "snapshotId", snapshotID, "accountID", accountID, "ownerID", cfg.OwnerID)
|
||||
return nil, errors.New(awserrors.ErrorUnauthorizedOperation)
|
||||
}
|
||||
|
||||
// Check if any volumes were created from this snapshot
|
||||
inUse, err := s.snapshotInUseByVolumes(snapshotID)
|
||||
if err != nil {
|
||||
@@ -456,15 +473,16 @@ func (s *SnapshotServiceImpl) DeleteSnapshot(input *ec2.DeleteSnapshotInput) (*e
|
||||
return &ec2.DeleteSnapshotOutput{}, nil
|
||||
}
|
||||
|
||||
// CopySnapshot copies a snapshot (within same region for now)
|
||||
func (s *SnapshotServiceImpl) CopySnapshot(input *ec2.CopySnapshotInput) (*ec2.CopySnapshotOutput, error) {
|
||||
// CopySnapshot copies a snapshot (within same region for now).
|
||||
// The copied snapshot is owned by the caller's account.
|
||||
func (s *SnapshotServiceImpl) CopySnapshot(input *ec2.CopySnapshotInput, accountID string) (*ec2.CopySnapshotOutput, error) {
|
||||
if input == nil || input.SourceSnapshotId == nil {
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
|
||||
sourceSnapshotID := *input.SourceSnapshotId
|
||||
|
||||
slog.Info("CopySnapshot request", "sourceSnapshotId", sourceSnapshotID)
|
||||
slog.Info("CopySnapshot request", "sourceSnapshotId", sourceSnapshotID, "accountID", accountID)
|
||||
|
||||
sourceCfg, err := s.getSnapshotConfig(sourceSnapshotID)
|
||||
if err != nil {
|
||||
@@ -472,6 +490,12 @@ func (s *SnapshotServiceImpl) CopySnapshot(input *ec2.CopySnapshotInput) (*ec2.C
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify the caller owns the source snapshot
|
||||
if accountID != "" && sourceCfg.OwnerID != "" && sourceCfg.OwnerID != accountID {
|
||||
slog.Warn("CopySnapshot: account does not own source snapshot", "snapshotId", sourceSnapshotID, "accountID", accountID, "ownerID", sourceCfg.OwnerID)
|
||||
return nil, errors.New(awserrors.ErrorUnauthorizedOperation)
|
||||
}
|
||||
|
||||
newSnapshotID := utils.GenerateResourceID("snap")
|
||||
|
||||
newCfg := &SnapshotConfig{
|
||||
@@ -483,7 +507,7 @@ func (s *SnapshotServiceImpl) CopySnapshot(input *ec2.CopySnapshotInput) (*ec2.C
|
||||
StartTime: time.Now(),
|
||||
Description: sourceCfg.Description,
|
||||
Encrypted: sourceCfg.Encrypted,
|
||||
OwnerID: sourceCfg.OwnerID,
|
||||
OwnerID: accountID,
|
||||
AvailabilityZone: sourceCfg.AvailabilityZone,
|
||||
Tags: make(map[string]string),
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testAccountID = "111122223333"
|
||||
const otherAccountID = "444455556666"
|
||||
|
||||
// setupTestSnapshotService creates a snapshot service with in-memory storage for testing
|
||||
func setupTestSnapshotService(t *testing.T) (*SnapshotServiceImpl, *objectstore.MemoryObjectStore) {
|
||||
store := objectstore.NewMemoryObjectStore()
|
||||
@@ -77,7 +80,7 @@ func TestCreateSnapshot(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}, testAccountID)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
@@ -87,6 +90,7 @@ func TestCreateSnapshot(t *testing.T) {
|
||||
assert.Equal(t, "completed", *result.State)
|
||||
assert.Equal(t, "100%", *result.Progress)
|
||||
assert.Equal(t, "Test snapshot", *result.Description)
|
||||
assert.Equal(t, testAccountID, *result.OwnerId)
|
||||
|
||||
// Verify tags
|
||||
assert.Len(t, result.Tags, 1)
|
||||
@@ -98,7 +102,7 @@ func TestCreateSnapshot(t *testing.T) {
|
||||
func TestCreateSnapshot_MissingVolumeId(t *testing.T) {
|
||||
svc, _ := setupTestSnapshotService(t)
|
||||
|
||||
_, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{})
|
||||
_, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
@@ -127,7 +131,7 @@ func TestCreateSnapshot_VolumeZeroSize(t *testing.T) {
|
||||
|
||||
_, err = svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String("vol-zerosize"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorServerInternal)
|
||||
}
|
||||
@@ -138,7 +142,7 @@ func TestCreateSnapshot_VolumeNotFound(t *testing.T) {
|
||||
|
||||
_, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String("vol-nonexistent"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorInvalidVolumeNotFound)
|
||||
}
|
||||
@@ -155,17 +159,17 @@ func TestDescribeSnapshots(t *testing.T) {
|
||||
snap1, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String("vol-1"),
|
||||
Description: aws.String("Snapshot 1"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
snap2, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String("vol-2"),
|
||||
Description: aws.String("Snapshot 2"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Describe all snapshots
|
||||
result, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{})
|
||||
result, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Len(t, result.Snapshots, 2)
|
||||
@@ -189,18 +193,18 @@ func TestDescribeSnapshots_ByID(t *testing.T) {
|
||||
// Create multiple snapshots
|
||||
snap1, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String("vol-1"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String("vol-1"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Describe only the first snapshot
|
||||
result, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
|
||||
SnapshotIds: []*string{snap1.SnapshotId},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Len(t, result.Snapshots, 1)
|
||||
@@ -211,12 +215,43 @@ func TestDescribeSnapshots_ByID(t *testing.T) {
|
||||
func TestDescribeSnapshots_Empty(t *testing.T) {
|
||||
svc, _ := setupTestSnapshotService(t)
|
||||
|
||||
result, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{})
|
||||
result, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Empty(t, result.Snapshots)
|
||||
}
|
||||
|
||||
// TestDescribeSnapshots_AccountScoping tests that account A cannot see account B's snapshots
|
||||
func TestDescribeSnapshots_AccountScoping(t *testing.T) {
|
||||
svc, store := setupTestSnapshotService(t)
|
||||
|
||||
createTestVolume(t, store, "vol-1", 50)
|
||||
|
||||
// Account A creates a snapshot
|
||||
snapA, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String("vol-1"),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Account B creates a snapshot
|
||||
snapB, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String("vol-1"),
|
||||
}, otherAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Account A should only see its own snapshot
|
||||
resultA, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resultA.Snapshots, 1)
|
||||
assert.Equal(t, *snapA.SnapshotId, *resultA.Snapshots[0].SnapshotId)
|
||||
|
||||
// Account B should only see its own snapshot
|
||||
resultB, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{}, otherAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resultB.Snapshots, 1)
|
||||
assert.Equal(t, *snapB.SnapshotId, *resultB.Snapshots[0].SnapshotId)
|
||||
}
|
||||
|
||||
// TestDeleteSnapshot tests deleting a snapshot
|
||||
func TestDeleteSnapshot(t *testing.T) {
|
||||
svc, store := setupTestSnapshotService(t)
|
||||
@@ -225,30 +260,55 @@ func TestDeleteSnapshot(t *testing.T) {
|
||||
createTestVolume(t, store, "vol-1", 50)
|
||||
snap, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String("vol-1"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify snapshot exists
|
||||
result, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
|
||||
SnapshotIds: []*string{snap.SnapshotId},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result.Snapshots, 1)
|
||||
|
||||
// Delete snapshot
|
||||
_, err = svc.DeleteSnapshot(&ec2.DeleteSnapshotInput{
|
||||
SnapshotId: snap.SnapshotId,
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify snapshot is gone
|
||||
result, err = svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
|
||||
SnapshotIds: []*string{snap.SnapshotId},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, result.Snapshots)
|
||||
}
|
||||
|
||||
// TestDeleteSnapshot_WrongAccount tests that account B cannot delete account A's snapshot
|
||||
func TestDeleteSnapshot_WrongAccount(t *testing.T) {
|
||||
svc, store := setupTestSnapshotService(t)
|
||||
|
||||
createTestVolume(t, store, "vol-1", 50)
|
||||
snap, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String("vol-1"),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Account B tries to delete account A's snapshot — should fail
|
||||
_, err = svc.DeleteSnapshot(&ec2.DeleteSnapshotInput{
|
||||
SnapshotId: snap.SnapshotId,
|
||||
}, otherAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorUnauthorizedOperation)
|
||||
|
||||
// Verify snapshot still exists
|
||||
result, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
|
||||
SnapshotIds: []*string{snap.SnapshotId},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result.Snapshots, 1)
|
||||
}
|
||||
|
||||
// TestDeleteSnapshot_InUseByVolume tests that deleting a snapshot fails when a volume was created from it
|
||||
func TestDeleteSnapshot_InUseByVolume(t *testing.T) {
|
||||
svc, store := setupTestSnapshotService(t)
|
||||
@@ -257,7 +317,7 @@ func TestDeleteSnapshot_InUseByVolume(t *testing.T) {
|
||||
createTestVolume(t, store, "vol-source", 50)
|
||||
snap, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String("vol-source"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a volume that references this snapshot (simulates CreateVolume from snapshot)
|
||||
@@ -281,14 +341,14 @@ func TestDeleteSnapshot_InUseByVolume(t *testing.T) {
|
||||
// Attempt to delete the snapshot — should fail
|
||||
_, err = svc.DeleteSnapshot(&ec2.DeleteSnapshotInput{
|
||||
SnapshotId: snap.SnapshotId,
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorInvalidSnapshotInUse)
|
||||
|
||||
// Verify snapshot still exists
|
||||
result, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
|
||||
SnapshotIds: []*string{snap.SnapshotId},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result.Snapshots, 1)
|
||||
}
|
||||
@@ -299,7 +359,7 @@ func TestDeleteSnapshot_NotFound(t *testing.T) {
|
||||
|
||||
_, err := svc.DeleteSnapshot(&ec2.DeleteSnapshotInput{
|
||||
SnapshotId: aws.String("snap-nonexistent"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorInvalidSnapshotNotFound)
|
||||
}
|
||||
@@ -308,7 +368,7 @@ func TestDeleteSnapshot_NotFound(t *testing.T) {
|
||||
func TestDeleteSnapshot_MissingID(t *testing.T) {
|
||||
svc, _ := setupTestSnapshotService(t)
|
||||
|
||||
_, err := svc.DeleteSnapshot(&ec2.DeleteSnapshotInput{})
|
||||
_, err := svc.DeleteSnapshot(&ec2.DeleteSnapshotInput{}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
@@ -322,32 +382,80 @@ func TestCopySnapshot(t *testing.T) {
|
||||
snap, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String("vol-1"),
|
||||
Description: aws.String("Original snapshot"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Copy snapshot
|
||||
copyResult, err := svc.CopySnapshot(&ec2.CopySnapshotInput{
|
||||
SourceSnapshotId: snap.SnapshotId,
|
||||
Description: aws.String("Copied snapshot"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, copyResult)
|
||||
assert.True(t, strings.HasPrefix(*copyResult.SnapshotId, "snap-"))
|
||||
assert.NotEqual(t, *snap.SnapshotId, *copyResult.SnapshotId)
|
||||
|
||||
// Verify both snapshots exist
|
||||
result, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{})
|
||||
result, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result.Snapshots, 2)
|
||||
}
|
||||
|
||||
// TestCopySnapshot_SetsCallerAsOwner tests that copied snapshot is owned by the caller
|
||||
func TestCopySnapshot_SetsCallerAsOwner(t *testing.T) {
|
||||
svc, store := setupTestSnapshotService(t)
|
||||
|
||||
createTestVolume(t, store, "vol-1", 50)
|
||||
snap, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String("vol-1"),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Copy snapshot as the same account
|
||||
copyResult, err := svc.CopySnapshot(&ec2.CopySnapshotInput{
|
||||
SourceSnapshotId: snap.SnapshotId,
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify copied snapshot owner is the caller
|
||||
result, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
|
||||
SnapshotIds: []*string{copyResult.SnapshotId},
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Snapshots, 1)
|
||||
assert.Equal(t, testAccountID, *result.Snapshots[0].OwnerId)
|
||||
}
|
||||
|
||||
// TestCopySnapshot_WrongAccount tests that account B cannot copy account A's snapshot
|
||||
func TestCopySnapshot_WrongAccount(t *testing.T) {
|
||||
svc, store := setupTestSnapshotService(t)
|
||||
|
||||
createTestVolume(t, store, "vol-1", 50)
|
||||
_, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String("vol-1"),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get the snapshot ID
|
||||
result, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Snapshots, 1)
|
||||
|
||||
// Account B tries to copy account A's snapshot — should fail
|
||||
_, err = svc.CopySnapshot(&ec2.CopySnapshotInput{
|
||||
SourceSnapshotId: result.Snapshots[0].SnapshotId,
|
||||
}, otherAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorUnauthorizedOperation)
|
||||
}
|
||||
|
||||
// TestCopySnapshot_NotFound tests copying a non-existent snapshot
|
||||
func TestCopySnapshot_NotFound(t *testing.T) {
|
||||
svc, _ := setupTestSnapshotService(t)
|
||||
|
||||
_, err := svc.CopySnapshot(&ec2.CopySnapshotInput{
|
||||
SourceSnapshotId: aws.String("snap-nonexistent"),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorInvalidSnapshotNotFound)
|
||||
}
|
||||
@@ -356,7 +464,7 @@ func TestCopySnapshot_NotFound(t *testing.T) {
|
||||
func TestCopySnapshot_MissingSourceID(t *testing.T) {
|
||||
svc, _ := setupTestSnapshotService(t)
|
||||
|
||||
_, err := svc.CopySnapshot(&ec2.CopySnapshotInput{})
|
||||
_, err := svc.CopySnapshot(&ec2.CopySnapshotInput{}, testAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
@@ -377,19 +485,19 @@ func TestCopySnapshot_PreservesTags(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Copy snapshot
|
||||
copyResult, err := svc.CopySnapshot(&ec2.CopySnapshotInput{
|
||||
SourceSnapshotId: snap.SnapshotId,
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify copied snapshot has tags
|
||||
result, err := svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
|
||||
SnapshotIds: []*string{copyResult.SnapshotId},
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Snapshots, 1)
|
||||
assert.Len(t, result.Snapshots[0].Tags, 1)
|
||||
@@ -522,7 +630,7 @@ func TestCreateSnapshot_WritesKVEntry(t *testing.T) {
|
||||
|
||||
snap, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String(volumeID),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify KV entry exists
|
||||
@@ -554,13 +662,13 @@ func TestDeleteSnapshot_RemovesKVEntry(t *testing.T) {
|
||||
|
||||
snap, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String(volumeID),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Delete the snapshot
|
||||
_, err = svc.DeleteSnapshot(&ec2.DeleteSnapshotInput{
|
||||
SnapshotId: snap.SnapshotId,
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// KV should now be empty for this volume
|
||||
@@ -585,12 +693,12 @@ func TestCopySnapshot_AddsKVEntry(t *testing.T) {
|
||||
|
||||
snap, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String(volumeID),
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
copyResult, err := svc.CopySnapshot(&ec2.CopySnapshotInput{
|
||||
SourceSnapshotId: snap.SnapshotId,
|
||||
})
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Both snapshot IDs should be in KV
|
||||
@@ -602,3 +710,58 @@ func TestCopySnapshot_AddsKVEntry(t *testing.T) {
|
||||
assert.Contains(t, snapshots, *copyResult.SnapshotId)
|
||||
assert.Len(t, snapshots, 2)
|
||||
}
|
||||
|
||||
// TestCreateSnapshot_CrossAccountVolumeRejected tests that snapshotting another account's volume is rejected.
|
||||
func TestCreateSnapshot_CrossAccountVolumeRejected(t *testing.T) {
|
||||
svc, store := setupTestSnapshotService(t)
|
||||
|
||||
// Create a volume owned by testAccountID (via TenantID)
|
||||
volumeID := "vol-owned-by-alpha"
|
||||
volumeState := viperblock.VBState{
|
||||
VolumeConfig: viperblock.VolumeConfig{
|
||||
VolumeMetadata: viperblock.VolumeMetadata{
|
||||
SizeGiB: 100,
|
||||
TenantID: testAccountID,
|
||||
AvailabilityZone: "us-east-1a",
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := json.Marshal(volumeState)
|
||||
require.NoError(t, err)
|
||||
_, err = store.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String("test-bucket"),
|
||||
Key: aws.String(volumeID + "/config.json"),
|
||||
Body: strings.NewReader(string(data)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Another account tries to snapshot the volume — should fail
|
||||
_, err = svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String(volumeID),
|
||||
}, otherAccountID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorInvalidVolumeNotFound)
|
||||
|
||||
// Same account can snapshot — should succeed
|
||||
result, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String(volumeID),
|
||||
}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testAccountID, *result.OwnerId)
|
||||
}
|
||||
|
||||
// TestCreateSnapshot_PrePhase4VolumeAllowed tests that volumes without TenantID (pre-phase4) are allowed.
|
||||
func TestCreateSnapshot_PrePhase4VolumeAllowed(t *testing.T) {
|
||||
svc, store := setupTestSnapshotService(t)
|
||||
|
||||
// Create a volume with no TenantID (pre-phase4)
|
||||
volumeID := "vol-legacy"
|
||||
createTestVolume(t, store, volumeID, 50)
|
||||
|
||||
// Any account can snapshot — backward compatibility
|
||||
result, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{
|
||||
VolumeId: aws.String(volumeID),
|
||||
}, otherAccountID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, otherAccountID, *result.OwnerId)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user