fix: name the missing RDS engine image instead of answering ServerInternal

This commit is contained in:
Julian Sommer
2026-08-19 13:39:54 +10:00
parent 52f12586f5
commit 6ef7dd8692
2 changed files with 56 additions and 2 deletions
+20 -2
View File
@@ -52,6 +52,24 @@ const (
// validation failure rather than launching something else.
var ErrEngineAMINotFound = errors.New("rds: no AMI found for engine")
// The catalog registers every engine's system image under this prefix, so the
// error can name the image an operator has to import.
const engineImageNamePrefix = "spinifex-rds-"
// An engine image no deployment ever imported is a gap no retry closes, so the
// failure carries a code: a bare error resolves to ServerInternal, which the
// caller reads as a transient fault and retries.
func engineAMINotFound(engine, version string) error {
requested := "an engine image for " + engine
if version != "" {
requested = "version " + version + " for " + engine
}
return fmt.Errorf("%w: %w", ErrEngineAMINotFound, awserrors.Errorf(
awserrors.ErrorInvalidParameterCombination,
"Cannot find %s; this deployment has no %s%s system image installed",
requested, engineImageNamePrefix, engine))
}
type launchVPCProvisioner interface {
CreateNetworkInterface(ctx context.Context, input *ec2.CreateNetworkInterfaceInput, accountID string) (*ec2.CreateNetworkInterfaceOutput, error)
DeleteNetworkInterface(ctx context.Context, input *ec2.DeleteNetworkInterfaceInput, accountID string) (*ec2.DeleteNetworkInterfaceOutput, error)
@@ -460,7 +478,7 @@ func resolveEngineAMI(ctx context.Context, amiSvc launchAMIResolver, engine, ver
return "", fmt.Errorf("rds: describe %s AMI: %w", engine, err)
}
if out == nil {
return "", fmt.Errorf("%w: %s %s", ErrEngineAMINotFound, engine, version)
return "", engineAMINotFound(engine, version)
}
// Several builds of one engine version can be registered; skip malformed
@@ -485,7 +503,7 @@ func resolveEngineAMI(ctx context.Context, amiSvc launchAMIResolver, engine, ver
}
}
if newestID == "" {
return "", fmt.Errorf("%w: %s %s", ErrEngineAMINotFound, engine, version)
return "", engineAMINotFound(engine, version)
}
if matches > 1 {
slog.WarnContext(ctx, "rds: multiple AMIs match the requested engine; using newest",
+36
View File
@@ -928,5 +928,41 @@ func TestLaunchDBInstanceVMFailsWithoutAnEngineAMI(t *testing.T) {
_, err := LaunchDBInstanceVM(t.Context(), h.deps(), testLaunchInput())
require.ErrorIs(t, err, ErrEngineAMINotFound,
"a missing engine image must be a named failure, never a fallback to some other AMI")
code, _, ok := awserrors.ResolveErrorDetail(err)
require.True(t, ok, "the launch must not swallow the code on its way out")
assert.Equal(t, awserrors.ErrorInvalidParameterCombination, code)
assert.Empty(t, h.enis.created)
}
func TestResolveEngineAMINotFoundCarriesAClientCode(t *testing.T) {
t.Parallel()
h := newLaunchHarness()
h.images.images = nil
_, err := resolveEngineAMI(t.Context(), h.images, "postgres", "18")
require.ErrorIs(t, err, ErrEngineAMINotFound)
code, message, ok := awserrors.ResolveErrorDetail(err)
require.True(t, ok,
"an uncoded error reaches the caller as a retried ServerInternal, which says nothing about what is missing")
assert.Equal(t, awserrors.ErrorInvalidParameterCombination, code)
assert.Contains(t, message, "version 18 for postgres")
assert.Contains(t, message, "spinifex-rds-postgres",
"the message names the image an operator has to import")
}
func TestResolveEngineAMINotFoundWithoutARequestedVersion(t *testing.T) {
t.Parallel()
h := newLaunchHarness()
h.images.images = nil
_, err := resolveEngineAMI(t.Context(), h.images, "mariadb", "")
require.ErrorIs(t, err, ErrEngineAMINotFound)
code, message, ok := awserrors.ResolveErrorDetail(err)
require.True(t, ok)
assert.Equal(t, awserrors.ErrorInvalidParameterCombination, code)
assert.Contains(t, message, "an engine image for mariadb",
"an unset version must not read back as an empty one")
assert.Contains(t, message, "spinifex-rds-mariadb")
}