mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
fix(elbv2): tag-based LB AMI lookup with hard error on miss (#137)
Replace fragile image-name substring match (-lb-/-alb-) with a DescribeImages tag filter on spinifex:managed-by=elbv2. Remove the silent fallback that picks the first image in the store when no LB image exists — CreateLoadBalancer now returns ServerInternal with a remediation hint instead of persisting a record in StateFailed. - daemon: tag filter + error return from the resolver closure - service_impl: resolver signature is func() (string, error); caches only successful results so the next call retries after an error - CreateLoadBalancer gate now requires launcher + networking; AMI is resolved inside the gate and failures short-circuit before persist - admin images import: add --name+--file combo (catalog metadata with a local file source) and a --tag key=value flag for explicit tagging of file-only imports - build-system-image.sh + lb.conf: propagate SYSTEM_TAG via --tag so locally built system AMIs get the right tag even when the catalog is bypassed Closes mulga-969
This commit is contained in:
+36
-16
@@ -279,6 +279,7 @@ func init() {
|
||||
imagesImportCmd.Flags().String("version", "", "Specified distro version (e.g 12)")
|
||||
imagesImportCmd.Flags().String("arch", "", "Specified distro arch (e.g aarch64, arm64, x86_64)")
|
||||
imagesImportCmd.Flags().String("platform", "Linux/UNIX", "Specified platform (e.g Linux/UNIX, Windows)")
|
||||
imagesImportCmd.Flags().StringSlice("tag", nil, "Tag to apply to the imported AMI as key=value (repeatable; e.g. --tag spinifex:managed-by=elbv2)")
|
||||
imagesImportCmd.Flags().Bool("force", false, "Force command execution (overwrites existing files)")
|
||||
}
|
||||
|
||||
@@ -313,35 +314,54 @@ func runimagesImportCmd(cmd *cobra.Command, args []string) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Determine, if name specified, or file
|
||||
// --name pulls metadata (including Tags) from the catalog; --file supplies
|
||||
// the local image source. When both are set, the catalog provides metadata
|
||||
// and the file is used directly (no download). When only --file is set,
|
||||
// metadata comes from flags and no catalog tags are applied.
|
||||
imageName, _ := cmd.Flags().GetString("name")
|
||||
localFile, _ := cmd.Flags().GetString("file")
|
||||
|
||||
if imageName == "" && localFile == "" {
|
||||
fmt.Fprintf(os.Stderr, "Either --name or --file is required to import an image\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if imageName != "" {
|
||||
var exists bool
|
||||
// Confirm the image exists
|
||||
image, exists = utils.AvailableImages[imageName]
|
||||
|
||||
if !exists {
|
||||
fmt.Fprintf(os.Stderr, "Image name not found in available images")
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
imageFile, _ = cmd.Flags().GetString("file")
|
||||
}
|
||||
|
||||
if imageFile == "" {
|
||||
fmt.Fprintf(os.Stderr, "File required to import image")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(imageFile); err != nil {
|
||||
if localFile != "" {
|
||||
if _, err := os.Stat(localFile); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "File could not be found: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
imageFile = localFile
|
||||
if imageName == "" {
|
||||
image.Distro, _ = cmd.Flags().GetString("distro")
|
||||
image.Version, _ = cmd.Flags().GetString("version")
|
||||
image.Arch, _ = cmd.Flags().GetString("arch")
|
||||
image.Platform, _ = cmd.Flags().GetString("platform")
|
||||
}
|
||||
}
|
||||
|
||||
image.Distro, _ = cmd.Flags().GetString("distro")
|
||||
image.Version, _ = cmd.Flags().GetString("version")
|
||||
image.Arch, _ = cmd.Flags().GetString("arch")
|
||||
image.Platform, _ = cmd.Flags().GetString("platform")
|
||||
// --tag k=v (repeatable) merges user-supplied tags into the AMI. Overrides
|
||||
// catalog tags on key collision so operators can re-tag a known image.
|
||||
tagFlags, _ := cmd.Flags().GetStringSlice("tag")
|
||||
for _, kv := range tagFlags {
|
||||
k, v, ok := strings.Cut(kv, "=")
|
||||
if !ok || k == "" {
|
||||
fmt.Fprintf(os.Stderr, "Invalid --tag %q: expected key=value\n", kv)
|
||||
os.Exit(1)
|
||||
}
|
||||
if image.Tags == nil {
|
||||
image.Tags = map[string]string{}
|
||||
}
|
||||
image.Tags[k] = v
|
||||
}
|
||||
|
||||
if image.Distro == "" {
|
||||
@@ -377,7 +397,7 @@ func runimagesImportCmd(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("✅ Created config directory: %s\n", imagePath)
|
||||
|
||||
// Next, if the file is selected to download, fetch it, extract disk image, and save to path
|
||||
if imageName != "" {
|
||||
if imageName != "" && localFile == "" {
|
||||
// Download the file to the image path
|
||||
filename := path.Base(image.URL)
|
||||
imageFile = fmt.Sprintf("%s/%s", imagePath, filename)
|
||||
|
||||
@@ -147,11 +147,11 @@ if [[ -f "$OUTPUT_RAW" ]] && [[ $(( $(date +%s) - $(stat -c %Y "$OUTPUT_RAW") ))
|
||||
|
||||
if [[ "$DO_IMPORT" == true ]]; then
|
||||
echo "Importing as AMI..."
|
||||
(cd "$PROJECT_DIR" && ./bin/spx admin images import \
|
||||
--file "$OUTPUT_RAW" \
|
||||
--distro alpine \
|
||||
--version "${ALPINE_VERSION}-${IMAGE_NAME}" \
|
||||
--arch x86_64)
|
||||
IMPORT_ARGS=(--file "$OUTPUT_RAW" --distro alpine --version "${ALPINE_VERSION}" --arch x86_64)
|
||||
if [[ -n "${SYSTEM_TAG:-}" ]]; then
|
||||
IMPORT_ARGS+=(--tag "$SYSTEM_TAG")
|
||||
fi
|
||||
(cd "$PROJECT_DIR" && ./bin/spx admin images import "${IMPORT_ARGS[@]}")
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
@@ -318,14 +318,19 @@ echo ""
|
||||
|
||||
if [[ "$DO_IMPORT" == true ]]; then
|
||||
echo "Importing as AMI..."
|
||||
(cd "$PROJECT_DIR" && sudo -u spinifex-storage ./bin/spx admin images import \
|
||||
--file "$OUTPUT_RAW" \
|
||||
--distro alpine \
|
||||
--version "${ALPINE_VERSION}-${IMAGE_NAME}" \
|
||||
--arch x86_64)
|
||||
IMPORT_ARGS=(--file "$OUTPUT_RAW" --distro alpine --version "${ALPINE_VERSION}" --arch x86_64)
|
||||
if [[ -n "${SYSTEM_TAG:-}" ]]; then
|
||||
IMPORT_ARGS+=(--tag "$SYSTEM_TAG")
|
||||
fi
|
||||
(cd "$PROJECT_DIR" && sudo -u spinifex-storage ./bin/spx admin images import "${IMPORT_ARGS[@]}")
|
||||
else
|
||||
echo "To import as AMI, run:"
|
||||
echo " cd $PROJECT_DIR && ./bin/spx admin images import \\"
|
||||
echo " --file $OUTPUT_RAW \\"
|
||||
echo " --distro alpine --version ${ALPINE_VERSION}-${IMAGE_NAME} --arch x86_64"
|
||||
if [[ -n "${SYSTEM_TAG:-}" ]]; then
|
||||
echo " --distro alpine --version ${ALPINE_VERSION} --arch x86_64 \\"
|
||||
echo " --tag ${SYSTEM_TAG}"
|
||||
else
|
||||
echo " --distro alpine --version ${ALPINE_VERSION} --arch x86_64"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -6,6 +6,10 @@ IMAGE_DESCRIPTION="LB VM with HAProxy and lb-agent"
|
||||
ALPINE_VERSION="3.21.6"
|
||||
IMAGE_SIZE="256M"
|
||||
|
||||
# Tag applied to the imported AMI so the daemon can discover this as the
|
||||
# system image for ELBv2 load balancers. Must match tags.ManagedByELBv2.
|
||||
SYSTEM_TAG="spinifex:managed-by=elbv2"
|
||||
|
||||
# Packages to install via apk
|
||||
APK_PACKAGES="haproxy curl ca-certificates"
|
||||
|
||||
|
||||
+15
-17
@@ -50,6 +50,7 @@ import (
|
||||
"github.com/mulgadc/spinifex/spinifex/instancetypes"
|
||||
"github.com/mulgadc/spinifex/spinifex/objectstore"
|
||||
"github.com/mulgadc/spinifex/spinifex/qmp"
|
||||
"github.com/mulgadc/spinifex/spinifex/tags"
|
||||
"github.com/mulgadc/spinifex/spinifex/types"
|
||||
"github.com/mulgadc/spinifex/spinifex/utils"
|
||||
"github.com/mulgadc/spinifex/spinifex/vm"
|
||||
@@ -745,25 +746,22 @@ func (d *Daemon) Start() error {
|
||||
// at daemon startup (imported later), so we resolve it at request time.
|
||||
if d.imageService != nil {
|
||||
imgSvc := d.imageService
|
||||
d.elbv2Service.SetSystemAMIFunc(func() string {
|
||||
imagesOut, imgErr := imgSvc.DescribeImages(&ec2.DescribeImagesInput{}, utils.GlobalAccountID)
|
||||
if imgErr != nil || len(imagesOut.Images) == 0 {
|
||||
return ""
|
||||
d.elbv2Service.SetSystemAMIFunc(func() (string, error) {
|
||||
imagesOut, imgErr := imgSvc.DescribeImages(&ec2.DescribeImagesInput{
|
||||
Filters: []*ec2.Filter{{
|
||||
Name: aws.String("tag:" + tags.ManagedByKey),
|
||||
Values: []*string{aws.String(tags.ManagedByELBv2)},
|
||||
}},
|
||||
}, utils.GlobalAccountID)
|
||||
if imgErr != nil {
|
||||
return "", fmt.Errorf("describe LB system images: %w", imgErr)
|
||||
}
|
||||
systemAMI := aws.StringValue(imagesOut.Images[0].ImageId)
|
||||
// Prefer the new "-lb-" image name, fall back to legacy "-alb-" name.
|
||||
for _, img := range imagesOut.Images {
|
||||
name := aws.StringValue(img.Name)
|
||||
if strings.Contains(name, "-lb-") {
|
||||
systemAMI = aws.StringValue(img.ImageId)
|
||||
break
|
||||
}
|
||||
if strings.Contains(name, "-alb-") {
|
||||
systemAMI = aws.StringValue(img.ImageId)
|
||||
}
|
||||
if len(imagesOut.Images) == 0 {
|
||||
return "", errors.New("LB system image not imported; run: spx admin images import --name lb-alpine-3.21.6-x86_64")
|
||||
}
|
||||
slog.Info("System AMI resolved for LB VMs", "amiId", systemAMI)
|
||||
return systemAMI
|
||||
amiID := aws.StringValue(imagesOut.Images[0].ImageId)
|
||||
slog.Info("System AMI resolved for LB VMs", "amiId", amiID, "name", aws.StringValue(imagesOut.Images[0].Name))
|
||||
return amiID, nil
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -93,14 +93,14 @@ type ELBv2ServiceImpl struct {
|
||||
MgmtRouteTarget string // AWSGW bind IP to route via mgmt NIC
|
||||
nodeID string
|
||||
region string
|
||||
systemAMI string // AMI ID for system VMs (ALB VMs); resolved lazily via systemAMIFunc
|
||||
systemAMIFunc func() string // returns the current system AMI ID (queries image store)
|
||||
systemAMIMu sync.Mutex // guards lazy resolution of systemAMI
|
||||
systemAMIResolved bool // true once systemAMI has been resolved to a non-empty value
|
||||
systemInstanceType string // instance type for system VMs; resolved lazily via systemInstanceTypeFunc
|
||||
systemInstanceTypeFunc func() string // returns the smallest available instance type
|
||||
systemInstanceTypeMu sync.Mutex // guards lazy resolution of systemInstanceType
|
||||
systemInstanceTypeResolved bool // true once systemInstanceType has been resolved to a non-empty value
|
||||
systemAMI string // AMI ID for system VMs (ALB VMs); resolved lazily via systemAMIFunc
|
||||
systemAMIFunc func() (string, error) // returns the current system AMI ID (queries image store)
|
||||
systemAMIMu sync.Mutex // guards lazy resolution of systemAMI
|
||||
systemAMIResolved bool // true once systemAMI has been resolved to a non-empty value
|
||||
systemInstanceType string // instance type for system VMs; resolved lazily via systemInstanceTypeFunc
|
||||
systemInstanceTypeFunc func() string // returns the smallest available instance type
|
||||
systemInstanceTypeMu sync.Mutex // guards lazy resolution of systemInstanceType
|
||||
systemInstanceTypeResolved bool // true once systemInstanceType has been resolved to a non-empty value
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
hc *healthChecker
|
||||
@@ -147,7 +147,7 @@ func (s *ELBv2ServiceImpl) Close() {
|
||||
// SetSystemAMIFunc sets a function that resolves the current system AMI ID.
|
||||
// This is called at request time so the AMI is discovered even if imported
|
||||
// after the daemon starts.
|
||||
func (s *ELBv2ServiceImpl) SetSystemAMIFunc(fn func() string) {
|
||||
func (s *ELBv2ServiceImpl) SetSystemAMIFunc(fn func() (string, error)) {
|
||||
s.systemAMIMu.Lock()
|
||||
defer s.systemAMIMu.Unlock()
|
||||
s.systemAMIFunc = fn
|
||||
@@ -156,21 +156,25 @@ func (s *ELBv2ServiceImpl) SetSystemAMIFunc(fn func() string) {
|
||||
}
|
||||
|
||||
// getSystemAMI returns the system AMI ID, resolving it lazily if needed.
|
||||
// Only caches non-empty results so the resolver retries when the image
|
||||
// has not been imported yet.
|
||||
func (s *ELBv2ServiceImpl) getSystemAMI() string {
|
||||
// Caches only successful resolutions so the resolver retries after errors.
|
||||
func (s *ELBv2ServiceImpl) getSystemAMI() (string, error) {
|
||||
s.systemAMIMu.Lock()
|
||||
defer s.systemAMIMu.Unlock()
|
||||
if s.systemAMIResolved {
|
||||
return s.systemAMI
|
||||
return s.systemAMI, nil
|
||||
}
|
||||
if s.systemAMIFunc != nil {
|
||||
s.systemAMI = s.systemAMIFunc()
|
||||
if s.systemAMIFunc == nil {
|
||||
return "", nil
|
||||
}
|
||||
if s.systemAMI != "" {
|
||||
ami, err := s.systemAMIFunc()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ami != "" {
|
||||
s.systemAMI = ami
|
||||
s.systemAMIResolved = true
|
||||
}
|
||||
return s.systemAMI
|
||||
return ami, nil
|
||||
}
|
||||
|
||||
// SetSystemInstanceTypeFunc sets a function that resolves the smallest available
|
||||
@@ -608,7 +612,16 @@ func (s *ELBv2ServiceImpl) CreateLoadBalancer(input *elbv2.CreateLoadBalancerInp
|
||||
var albVPCIP string
|
||||
var hostPorts map[int]int
|
||||
var launchFailed bool
|
||||
if s.InstanceLauncher != nil && s.getSystemAMI() != "" && len(eniIDs) > 0 && len(subnets) > 0 {
|
||||
if s.InstanceLauncher != nil && len(eniIDs) > 0 && len(subnets) > 0 {
|
||||
systemAMI, amiErr := s.getSystemAMI()
|
||||
if amiErr != nil {
|
||||
slog.Error("CreateLoadBalancer: cannot resolve LB system AMI", "lbId", lbID, "err", amiErr)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
if systemAMI == "" {
|
||||
slog.Error("CreateLoadBalancer: LB system AMI not resolved", "lbId", lbID)
|
||||
return nil, errors.New(awserrors.ErrorServerInternal)
|
||||
}
|
||||
// Resolve MAC/IP for every ENI in one describe call.
|
||||
eniDetails := make(map[string]*ec2.NetworkInterface, len(eniIDs))
|
||||
if s.VPCService != nil {
|
||||
@@ -657,7 +670,7 @@ func (s *ELBv2ServiceImpl) CreateLoadBalancer(input *elbv2.CreateLoadBalancerInp
|
||||
} else {
|
||||
launchInput := &SystemInstanceInput{
|
||||
InstanceType: s.getSystemInstanceType(),
|
||||
ImageID: s.getSystemAMI(),
|
||||
ImageID: systemAMI,
|
||||
SubnetID: subnets[0],
|
||||
UserData: userData,
|
||||
ENIID: eniIDs[0],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers_elbv2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -1086,7 +1087,7 @@ func TestCreateLoadBalancer_InternetFacingScheme_PassesSchemeToLauncher(t *testi
|
||||
},
|
||||
}
|
||||
svc.InstanceLauncher = mock
|
||||
svc.SetSystemAMIFunc(func() string { return "ami-alb-test" })
|
||||
svc.SetSystemAMIFunc(func() (string, error) { return "ami-alb-test", nil })
|
||||
|
||||
out, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
||||
Name: aws.String("public-alb"),
|
||||
@@ -1111,7 +1112,7 @@ func TestCreateLoadBalancer_InternalScheme_PassesSchemeToLauncher(t *testing.T)
|
||||
},
|
||||
}
|
||||
svc.InstanceLauncher = mock
|
||||
svc.SetSystemAMIFunc(func() string { return "ami-alb-test" })
|
||||
svc.SetSystemAMIFunc(func() (string, error) { return "ami-alb-test", nil })
|
||||
|
||||
out, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
||||
Name: aws.String("private-alb"),
|
||||
@@ -1516,41 +1517,53 @@ func TestSetSystemInstanceTypeFunc(t *testing.T) {
|
||||
func TestGetSystemAMI(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
|
||||
// Before setting, should return empty
|
||||
assert.Empty(t, svc.getSystemAMI())
|
||||
// Before setting, should return empty with no error
|
||||
ami, err := svc.getSystemAMI()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, ami)
|
||||
|
||||
// Set the resolver
|
||||
svc.SetSystemAMIFunc(func() string { return "ami-system-001" })
|
||||
assert.Equal(t, "ami-system-001", svc.getSystemAMI())
|
||||
svc.SetSystemAMIFunc(func() (string, error) { return "ami-system-001", nil })
|
||||
ami, err = svc.getSystemAMI()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "ami-system-001", ami)
|
||||
|
||||
// Once resolved, caches the value
|
||||
svc.systemAMIFunc = func() string { return "ami-system-002" }
|
||||
assert.Equal(t, "ami-system-001", svc.getSystemAMI())
|
||||
svc.systemAMIFunc = func() (string, error) { return "ami-system-002", nil }
|
||||
ami, err = svc.getSystemAMI()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "ami-system-001", ami)
|
||||
}
|
||||
|
||||
func TestGetSystemAMI_RetryOnEmpty(t *testing.T) {
|
||||
func TestGetSystemAMI_RetryOnError(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
|
||||
calls := 0
|
||||
svc.SetSystemAMIFunc(func() string {
|
||||
svc.SetSystemAMIFunc(func() (string, error) {
|
||||
calls++
|
||||
if calls < 3 {
|
||||
return "" // simulate image not imported yet
|
||||
return "", errors.New("LB system image not imported")
|
||||
}
|
||||
return "ami-system-001"
|
||||
return "ami-system-001", nil
|
||||
})
|
||||
|
||||
// First two calls return empty — should NOT be cached
|
||||
assert.Empty(t, svc.getSystemAMI())
|
||||
assert.Empty(t, svc.getSystemAMI())
|
||||
// First two calls error — must NOT be cached
|
||||
_, err := svc.getSystemAMI()
|
||||
require.Error(t, err)
|
||||
_, err = svc.getSystemAMI()
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, 2, calls)
|
||||
|
||||
// Third call finds the image — should be cached from now on
|
||||
assert.Equal(t, "ami-system-001", svc.getSystemAMI())
|
||||
// Third call succeeds — cached from now on
|
||||
ami, err := svc.getSystemAMI()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "ami-system-001", ami)
|
||||
assert.Equal(t, 3, calls)
|
||||
|
||||
// Subsequent calls return cached value without calling the func again
|
||||
assert.Equal(t, "ami-system-001", svc.getSystemAMI())
|
||||
// Subsequent calls return cached value without re-invoking
|
||||
ami, err = svc.getSystemAMI()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "ami-system-001", ami)
|
||||
assert.Equal(t, 3, calls)
|
||||
}
|
||||
|
||||
@@ -1579,12 +1592,13 @@ func TestGetSystemInstanceType_RetryOnEmpty(t *testing.T) {
|
||||
|
||||
func TestGetSystemAMI_Concurrent(t *testing.T) {
|
||||
svc := setupTestService(t)
|
||||
svc.SetSystemAMIFunc(func() string { return "ami-system-001" })
|
||||
svc.SetSystemAMIFunc(func() (string, error) { return "ami-system-001", nil })
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for range 50 {
|
||||
wg.Go(func() {
|
||||
got := svc.getSystemAMI()
|
||||
got, err := svc.getSystemAMI()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ami-system-001", got)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package handlers_elbv2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/elbv2"
|
||||
"github.com/mulgadc/spinifex/spinifex/awserrors"
|
||||
"github.com/mulgadc/spinifex/spinifex/config"
|
||||
handlers_ec2_vpc "github.com/mulgadc/spinifex/spinifex/handlers/ec2/vpc"
|
||||
"github.com/mulgadc/spinifex/spinifex/testutil"
|
||||
@@ -157,7 +159,7 @@ func TestCreateLoadBalancer_MultiSubnet_AllENIsPassedToLauncher(t *testing.T) {
|
||||
},
|
||||
}
|
||||
svc.InstanceLauncher = mock
|
||||
svc.SetSystemAMIFunc(func() string { return "ami-alb-test" })
|
||||
svc.SetSystemAMIFunc(func() (string, error) { return "ami-alb-test", nil })
|
||||
svc.GatewayURL = "https://10.0.0.1:9999"
|
||||
svc.SystemAccessKey = "AKID"
|
||||
svc.SystemSecretKey = "SECRET"
|
||||
@@ -278,7 +280,7 @@ func TestCreateLoadBalancer_InternetFacing_AllocatesPublicIP(t *testing.T) {
|
||||
},
|
||||
}
|
||||
svc.InstanceLauncher = mock
|
||||
svc.SetSystemAMIFunc(func() string { return "ami-alb-test" })
|
||||
svc.SetSystemAMIFunc(func() (string, error) { return "ami-alb-test", nil })
|
||||
svc.GatewayURL = "https://10.0.0.1:9999"
|
||||
svc.SystemAccessKey = "AKID"
|
||||
svc.SystemSecretKey = "SECRET"
|
||||
@@ -327,7 +329,7 @@ func TestCreateLoadBalancer_Internal_NoPublicIP(t *testing.T) {
|
||||
},
|
||||
}
|
||||
svc.InstanceLauncher = mock
|
||||
svc.SetSystemAMIFunc(func() string { return "ami-alb-test" })
|
||||
svc.SetSystemAMIFunc(func() (string, error) { return "ami-alb-test", nil })
|
||||
svc.GatewayURL = "https://10.0.0.1:9999"
|
||||
svc.SystemAccessKey = "AKID"
|
||||
svc.SystemSecretKey = "SECRET"
|
||||
@@ -368,7 +370,7 @@ func TestCreateLoadBalancer_NLB_Internal_NoPublicIP(t *testing.T) {
|
||||
},
|
||||
}
|
||||
svc.InstanceLauncher = mock
|
||||
svc.SetSystemAMIFunc(func() string { return "ami-nlb-test" })
|
||||
svc.SetSystemAMIFunc(func() (string, error) { return "ami-nlb-test", nil })
|
||||
svc.GatewayURL = "https://10.0.0.1:9999"
|
||||
svc.SystemAccessKey = "AKID"
|
||||
svc.SystemSecretKey = "SECRET"
|
||||
@@ -417,7 +419,7 @@ func TestDeleteLoadBalancer_TerminatesVM_WithPublicIP(t *testing.T) {
|
||||
terminateDone: make(chan struct{}),
|
||||
}
|
||||
svc.InstanceLauncher = mock
|
||||
svc.SetSystemAMIFunc(func() string { return "ami-alb-test" })
|
||||
svc.SetSystemAMIFunc(func() (string, error) { return "ami-alb-test", nil })
|
||||
svc.GatewayURL = "https://10.0.0.1:9999"
|
||||
svc.SystemAccessKey = "AKID"
|
||||
svc.SystemSecretKey = "SECRET"
|
||||
@@ -465,7 +467,7 @@ func TestDescribeLoadBalancers_InternetFacing_IncludesPublicIP(t *testing.T) {
|
||||
},
|
||||
}
|
||||
svc.InstanceLauncher = mock
|
||||
svc.SetSystemAMIFunc(func() string { return "ami-alb-test" })
|
||||
svc.SetSystemAMIFunc(func() (string, error) { return "ami-alb-test", nil })
|
||||
svc.GatewayURL = "https://10.0.0.1:9999"
|
||||
svc.SystemAccessKey = "AKID"
|
||||
svc.SystemSecretKey = "SECRET"
|
||||
@@ -515,7 +517,7 @@ func TestDescribeLoadBalancers_Internal_NoPublicIP(t *testing.T) {
|
||||
},
|
||||
}
|
||||
svc.InstanceLauncher = mock
|
||||
svc.SetSystemAMIFunc(func() string { return "ami-alb-test" })
|
||||
svc.SetSystemAMIFunc(func() (string, error) { return "ami-alb-test", nil })
|
||||
svc.GatewayURL = "https://10.0.0.1:9999"
|
||||
svc.SystemAccessKey = "AKID"
|
||||
svc.SystemSecretKey = "SECRET"
|
||||
@@ -554,7 +556,7 @@ func TestCreateLoadBalancer_LaunchFailure_SetsStateFailed(t *testing.T) {
|
||||
launchErr: assert.AnError,
|
||||
}
|
||||
svc.InstanceLauncher = mock
|
||||
svc.SetSystemAMIFunc(func() string { return "ami-alb-test" })
|
||||
svc.SetSystemAMIFunc(func() (string, error) { return "ami-alb-test", nil })
|
||||
svc.GatewayURL = "https://10.0.0.1:9999"
|
||||
svc.SystemAccessKey = "AKID"
|
||||
svc.SystemSecretKey = "SECRET"
|
||||
@@ -583,7 +585,7 @@ func TestCreateLoadBalancer_MissingCredentials_SetsStateFailed(t *testing.T) {
|
||||
},
|
||||
}
|
||||
svc.InstanceLauncher = mock
|
||||
svc.SetSystemAMIFunc(func() string { return "ami-alb-test" })
|
||||
svc.SetSystemAMIFunc(func() (string, error) { return "ami-alb-test", nil })
|
||||
// Deliberately NOT setting credentials
|
||||
|
||||
out, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
||||
@@ -639,3 +641,40 @@ func TestENI_RequesterManagedFlag(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateLoadBalancer_AMIResolverError_ReturnsServerInternal verifies that
|
||||
// when the launch path would fire (launcher + networking present) but the LB
|
||||
// system AMI cannot be resolved, CreateLoadBalancer returns ServerInternal and
|
||||
// does not persist a record or invoke the launcher.
|
||||
func TestCreateLoadBalancer_AMIResolverError_ReturnsServerInternal(t *testing.T) {
|
||||
svc, vpcSvc := setupTestServiceWithVPC(t)
|
||||
|
||||
subnets, err := vpcSvc.DescribeSubnets(&ec2.DescribeSubnetsInput{}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
subnetID := *subnets.Subnets[0].SubnetId
|
||||
|
||||
mock := &mockSystemInstanceLauncher{
|
||||
launchResult: &SystemInstanceOutput{InstanceID: "i-should-not-launch"},
|
||||
}
|
||||
svc.InstanceLauncher = mock
|
||||
svc.SetSystemAMIFunc(func() (string, error) {
|
||||
return "", errors.New("LB system image not imported")
|
||||
})
|
||||
svc.GatewayURL = "https://10.0.0.1:9999"
|
||||
svc.SystemAccessKey = "AKID"
|
||||
svc.SystemSecretKey = "SECRET"
|
||||
|
||||
out, err := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{
|
||||
Name: aws.String("ami-err-alb"),
|
||||
Subnets: []*string{aws.String(subnetID)},
|
||||
}, testAccountID)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
|
||||
assert.Nil(t, out)
|
||||
assert.Empty(t, mock.launchCalls, "launcher must not be invoked when AMI resolver errors")
|
||||
|
||||
lbs, err := svc.store.ListLoadBalancers()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, lbs, "no LB record should be persisted when AMI resolver errors")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user