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:
Josh Siviour
2026-04-14 12:52:28 +10:00
committed by GitHub
parent b32111102d
commit 13eae4d40b
7 changed files with 186 additions and 93 deletions
+36 -16
View File
@@ -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)
+16 -11
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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
})
}
+32 -19
View File
@@ -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],
+35 -21
View File
@@ -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")
}