feat: runtime manager
This commit is contained in:
@@ -363,3 +363,13 @@ The implementation is complete only when all of the following hold:
|
||||
notification types
|
||||
- admin notifications remain `email`-only
|
||||
- auth-code email still bypasses `Notification Service`
|
||||
|
||||
## Note: Runtime Manager Catalog Extension
|
||||
|
||||
The three administrator-only `runtime.*` notification types
|
||||
(`runtime.image_pull_failed`, `runtime.container_start_failed`,
|
||||
`runtime.start_config_invalid`) are added by the Runtime Manager
|
||||
implementation plan, not by this document. See
|
||||
[`../rtmanager/PLAN.md`](../rtmanager/PLAN.md) §«Stage 07. Notification
|
||||
intent constructors and catalog extension». No new stages are added here
|
||||
for that catalog growth.
|
||||
|
||||
+16
-1
@@ -208,6 +208,9 @@ Primary configuration groups:
|
||||
- `NOTIFICATION_ADMIN_EMAILS_GAME_GENERATION_FAILED`
|
||||
- `NOTIFICATION_ADMIN_EMAILS_LOBBY_RUNTIME_PAUSED_AFTER_START`
|
||||
- `NOTIFICATION_ADMIN_EMAILS_LOBBY_APPLICATION_SUBMITTED`
|
||||
- `NOTIFICATION_ADMIN_EMAILS_RUNTIME_IMAGE_PULL_FAILED`
|
||||
- `NOTIFICATION_ADMIN_EMAILS_RUNTIME_CONTAINER_START_FAILED`
|
||||
- `NOTIFICATION_ADMIN_EMAILS_RUNTIME_START_CONFIG_INVALID`
|
||||
- OpenTelemetry:
|
||||
- standard `OTEL_*` variables
|
||||
- `NOTIFICATION_OTEL_STDOUT_TRACES_ENABLED`
|
||||
@@ -292,10 +295,13 @@ Accepted intents use the original Redis Stream `stream_entry_id` as
|
||||
| `lobby.race_name.registration_eligible` | `Game Lobby` (`game_lobby`) | capable member (`audience_kind=user`) | `push+email` | `game_id`, `game_name`, `race_name`, `eligible_until_ms` |
|
||||
| `lobby.race_name.registered` | `Game Lobby` (`game_lobby`) | registering user (`audience_kind=user`) | `push+email` | `race_name` |
|
||||
| `lobby.race_name.registration_denied` | `Game Lobby` (`game_lobby`) | incapable member (`audience_kind=user`) | `email` | `game_id`, `game_name`, `race_name`, `reason` |
|
||||
| `runtime.image_pull_failed` | `Runtime Manager` (`runtime_manager`) | configured admin email list (`audience_kind=admin_email`) | `email` | `game_id`, `image_ref`, `error_code`, `error_message`, `attempted_at_ms` |
|
||||
| `runtime.container_start_failed` | `Runtime Manager` (`runtime_manager`) | configured admin email list (`audience_kind=admin_email`) | `email` | `game_id`, `image_ref`, `error_code`, `error_message`, `attempted_at_ms` |
|
||||
| `runtime.start_config_invalid` | `Runtime Manager` (`runtime_manager`) | configured admin email list (`audience_kind=admin_email`) | `email` | `game_id`, `image_ref`, `error_code`, `error_message`, `attempted_at_ms` |
|
||||
|
||||
Rules:
|
||||
|
||||
- v1 supports exactly the fifteen `notification_type` values listed above
|
||||
- v1 supports exactly the eighteen `notification_type` values listed above
|
||||
- `lobby.application.submitted` keeps one stable `notification_type` and one
|
||||
stable `payload_json` shape; private games publish `audience_kind=user`
|
||||
while public games publish `audience_kind=admin_email`
|
||||
@@ -308,6 +314,12 @@ Rules:
|
||||
with a 30-day `eligible_until_ms` window
|
||||
- `lobby.race_name.registered` is emitted on successful
|
||||
`lobby.race_name.register` commit
|
||||
- the three `runtime.*` types are emitted by `Runtime Manager` only on
|
||||
first-touch start failures (image pull, container create/start, start
|
||||
configuration validation); they are administrator-only in v1 and have no
|
||||
push counterpart. `Runtime Manager` does not publish notifications for
|
||||
ongoing health changes — those flow through `runtime:health_events` and
|
||||
are escalated by `Game Master` if needed.
|
||||
|
||||
## Recipient Enrichment And Locale Policy
|
||||
|
||||
@@ -436,6 +448,9 @@ Initial notification-owned template assets:
|
||||
| `lobby.race_name.registration_eligible` | `lobby.race_name.registration_eligible` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
| `lobby.race_name.registered` | `lobby.race_name.registered` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
| `lobby.race_name.registration_denied` | `lobby.race_name.registration_denied` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
| `runtime.image_pull_failed` | `runtime.image_pull_failed` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
| `runtime.container_start_failed` | `runtime.container_start_failed` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
| `runtime.start_config_invalid` | `runtime.start_config_invalid` | `en/subject.tmpl`, `en/text.tmpl` |
|
||||
|
||||
`auth.login_code` does not belong to the notification-owned template set.
|
||||
|
||||
|
||||
@@ -58,6 +58,15 @@ components:
|
||||
idempotency_key: game-lobby:game-456:application-submitted:user-42
|
||||
occurred_at_ms: "1775121700002"
|
||||
payload_json: '{"game_id":"game-456","game_name":"Orion Front","applicant_user_id":"user-42","applicant_name":"Nova Pilot"}'
|
||||
- name: runtimeImagePullFailed
|
||||
summary: Administrator email notification about a failed engine image pull.
|
||||
payload:
|
||||
notification_type: runtime.image_pull_failed
|
||||
producer: runtime_manager
|
||||
audience_kind: admin_email
|
||||
idempotency_key: runtime-manager:game-789:image-pull-failed:1775121700003
|
||||
occurred_at_ms: "1775121700003"
|
||||
payload_json: '{"game_id":"game-789","image_ref":"galaxy/game:1.4.7","error_code":"image_pull_failed","error_message":"manifest unknown","attempted_at_ms":1775121700003}'
|
||||
schemas:
|
||||
NotificationIntentEnvelope:
|
||||
type: object
|
||||
@@ -98,6 +107,9 @@ components:
|
||||
- lobby.race_name.registration_eligible
|
||||
- lobby.race_name.registered
|
||||
- lobby.race_name.registration_denied
|
||||
- runtime.image_pull_failed
|
||||
- runtime.container_start_failed
|
||||
- runtime.start_config_invalid
|
||||
description: |
|
||||
Exact v1 notification type catalog. `lobby.invite.revoked`
|
||||
deliberately remains outside the supported catalog because it
|
||||
@@ -108,6 +120,7 @@ components:
|
||||
- geoprofile
|
||||
- game_master
|
||||
- game_lobby
|
||||
- runtime_manager
|
||||
description: |
|
||||
Stable producer identifier. The exact producer value is frozen per
|
||||
`notification_type` by the v1 catalog.
|
||||
@@ -419,6 +432,51 @@ components:
|
||||
payload_json:
|
||||
contentSchema:
|
||||
$ref: '#/components/schemas/LobbyRaceNameRegistrationDeniedPayload'
|
||||
- if:
|
||||
properties:
|
||||
notification_type:
|
||||
const: runtime.image_pull_failed
|
||||
required:
|
||||
- notification_type
|
||||
then:
|
||||
properties:
|
||||
producer:
|
||||
const: runtime_manager
|
||||
audience_kind:
|
||||
const: admin_email
|
||||
payload_json:
|
||||
contentSchema:
|
||||
$ref: '#/components/schemas/RuntimeImagePullFailedPayload'
|
||||
- if:
|
||||
properties:
|
||||
notification_type:
|
||||
const: runtime.container_start_failed
|
||||
required:
|
||||
- notification_type
|
||||
then:
|
||||
properties:
|
||||
producer:
|
||||
const: runtime_manager
|
||||
audience_kind:
|
||||
const: admin_email
|
||||
payload_json:
|
||||
contentSchema:
|
||||
$ref: '#/components/schemas/RuntimeContainerStartFailedPayload'
|
||||
- if:
|
||||
properties:
|
||||
notification_type:
|
||||
const: runtime.start_config_invalid
|
||||
required:
|
||||
- notification_type
|
||||
then:
|
||||
properties:
|
||||
producer:
|
||||
const: runtime_manager
|
||||
audience_kind:
|
||||
const: admin_email
|
||||
payload_json:
|
||||
contentSchema:
|
||||
$ref: '#/components/schemas/RuntimeStartConfigInvalidPayload'
|
||||
GeoReviewRecommendedPayload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
@@ -697,3 +755,78 @@ components:
|
||||
reason:
|
||||
type: string
|
||||
minLength: 1
|
||||
RuntimeImagePullFailedPayload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- game_id
|
||||
- image_ref
|
||||
- error_code
|
||||
- error_message
|
||||
- attempted_at_ms
|
||||
properties:
|
||||
game_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
image_ref:
|
||||
type: string
|
||||
minLength: 1
|
||||
error_code:
|
||||
type: string
|
||||
minLength: 1
|
||||
error_message:
|
||||
type: string
|
||||
minLength: 1
|
||||
attempted_at_ms:
|
||||
type: integer
|
||||
minimum: 1
|
||||
RuntimeContainerStartFailedPayload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- game_id
|
||||
- image_ref
|
||||
- error_code
|
||||
- error_message
|
||||
- attempted_at_ms
|
||||
properties:
|
||||
game_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
image_ref:
|
||||
type: string
|
||||
minLength: 1
|
||||
error_code:
|
||||
type: string
|
||||
minLength: 1
|
||||
error_message:
|
||||
type: string
|
||||
minLength: 1
|
||||
attempted_at_ms:
|
||||
type: integer
|
||||
minimum: 1
|
||||
RuntimeStartConfigInvalidPayload:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- game_id
|
||||
- image_ref
|
||||
- error_code
|
||||
- error_message
|
||||
- attempted_at_ms
|
||||
properties:
|
||||
game_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
image_ref:
|
||||
type: string
|
||||
minLength: 1
|
||||
error_code:
|
||||
type: string
|
||||
minLength: 1
|
||||
error_message:
|
||||
type: string
|
||||
minLength: 1
|
||||
attempted_at_ms:
|
||||
type: integer
|
||||
minimum: 1
|
||||
|
||||
@@ -35,6 +35,9 @@ var expectedNotificationTypeCatalog = []string{
|
||||
"lobby.race_name.registration_eligible",
|
||||
"lobby.race_name.registered",
|
||||
"lobby.race_name.registration_denied",
|
||||
"runtime.image_pull_failed",
|
||||
"runtime.container_start_failed",
|
||||
"runtime.start_config_invalid",
|
||||
}
|
||||
|
||||
var expectedNotificationCatalog = map[string]notificationCatalogExpectation{
|
||||
@@ -128,6 +131,24 @@ var expectedNotificationCatalog = map[string]notificationCatalogExpectation{
|
||||
payloadSchema: "LobbyRaceNameRegistrationDeniedPayload",
|
||||
requiredFields: []string{"game_id", "game_name", "race_name", "reason"},
|
||||
},
|
||||
"runtime.image_pull_failed": {
|
||||
producer: "runtime_manager",
|
||||
audienceKind: "admin_email",
|
||||
payloadSchema: "RuntimeImagePullFailedPayload",
|
||||
requiredFields: []string{"game_id", "image_ref", "error_code", "error_message", "attempted_at_ms"},
|
||||
},
|
||||
"runtime.container_start_failed": {
|
||||
producer: "runtime_manager",
|
||||
audienceKind: "admin_email",
|
||||
payloadSchema: "RuntimeContainerStartFailedPayload",
|
||||
requiredFields: []string{"game_id", "image_ref", "error_code", "error_message", "attempted_at_ms"},
|
||||
},
|
||||
"runtime.start_config_invalid": {
|
||||
producer: "runtime_manager",
|
||||
audienceKind: "admin_email",
|
||||
payloadSchema: "RuntimeStartConfigInvalidPayload",
|
||||
requiredFields: []string{"game_id", "image_ref", "error_code", "error_message", "attempted_at_ms"},
|
||||
},
|
||||
}
|
||||
|
||||
const expectedNotificationCatalogTable = `| ` + "`notification_type`" + ` | Producer | Audience | Channels | Required ` + "`payload_json`" + ` fields |
|
||||
@@ -146,7 +167,10 @@ const expectedNotificationCatalogTable = `| ` + "`notification_type`" + ` | Prod
|
||||
| ` + "`lobby.invite.expired`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | private-game owner (` + "`audience_kind=user`" + `) | ` + "`email`" + ` | ` + "`game_id`" + `, ` + "`game_name`" + `, ` + "`invitee_user_id`" + `, ` + "`invitee_name`" + ` |
|
||||
| ` + "`lobby.race_name.registration_eligible`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | capable member (` + "`audience_kind=user`" + `) | ` + "`push+email`" + ` | ` + "`game_id`" + `, ` + "`game_name`" + `, ` + "`race_name`" + `, ` + "`eligible_until_ms`" + ` |
|
||||
| ` + "`lobby.race_name.registered`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | registering user (` + "`audience_kind=user`" + `) | ` + "`push+email`" + ` | ` + "`race_name`" + ` |
|
||||
| ` + "`lobby.race_name.registration_denied`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | incapable member (` + "`audience_kind=user`" + `) | ` + "`email`" + ` | ` + "`game_id`" + `, ` + "`game_name`" + `, ` + "`race_name`" + `, ` + "`reason`" + ` |`
|
||||
| ` + "`lobby.race_name.registration_denied`" + ` | ` + "`Game Lobby`" + ` (` + "`game_lobby`" + `) | incapable member (` + "`audience_kind=user`" + `) | ` + "`email`" + ` | ` + "`game_id`" + `, ` + "`game_name`" + `, ` + "`race_name`" + `, ` + "`reason`" + ` |
|
||||
| ` + "`runtime.image_pull_failed`" + ` | ` + "`Runtime Manager`" + ` (` + "`runtime_manager`" + `) | configured admin email list (` + "`audience_kind=admin_email`" + `) | ` + "`email`" + ` | ` + "`game_id`" + `, ` + "`image_ref`" + `, ` + "`error_code`" + `, ` + "`error_message`" + `, ` + "`attempted_at_ms`" + ` |
|
||||
| ` + "`runtime.container_start_failed`" + ` | ` + "`Runtime Manager`" + ` (` + "`runtime_manager`" + `) | configured admin email list (` + "`audience_kind=admin_email`" + `) | ` + "`email`" + ` | ` + "`game_id`" + `, ` + "`image_ref`" + `, ` + "`error_code`" + `, ` + "`error_message`" + `, ` + "`attempted_at_ms`" + ` |
|
||||
| ` + "`runtime.start_config_invalid`" + ` | ` + "`Runtime Manager`" + ` (` + "`runtime_manager`" + `) | configured admin email list (` + "`audience_kind=admin_email`" + `) | ` + "`email`" + ` | ` + "`game_id`" + `, ` + "`image_ref`" + `, ` + "`error_code`" + `, ` + "`error_message`" + `, ` + "`attempted_at_ms`" + ` |`
|
||||
|
||||
var expectedSharedDocumentationSnippets = []string{
|
||||
"`lobby.application.submitted` keeps one stable `notification_type` and one stable `payload_json` shape",
|
||||
@@ -234,7 +258,7 @@ func TestIntentAsyncAPISpecFreezesEnvelopeSchema(t *testing.T) {
|
||||
|
||||
producer := getMapValue(t, properties, "producer")
|
||||
require.Equal(t, "string", getStringValue(t, producer, "type"))
|
||||
require.Equal(t, []string{"geoprofile", "game_master", "game_lobby"}, getStringSlice(t, producer, "enum"))
|
||||
require.Equal(t, []string{"geoprofile", "game_master", "game_lobby", "runtime_manager"}, getStringSlice(t, producer, "enum"))
|
||||
|
||||
occurredAt := getMapValue(t, properties, "occurred_at_ms")
|
||||
require.Equal(t, "string", getStringValue(t, occurredAt, "type"))
|
||||
|
||||
@@ -85,6 +85,18 @@ const (
|
||||
// NotificationTypeLobbyRaceNameRegistrationDenied identifies the
|
||||
// `lobby.race_name.registration_denied` notification.
|
||||
NotificationTypeLobbyRaceNameRegistrationDenied = notificationintent.NotificationTypeLobbyRaceNameRegistrationDenied
|
||||
|
||||
// NotificationTypeRuntimeImagePullFailed identifies the
|
||||
// `runtime.image_pull_failed` notification.
|
||||
NotificationTypeRuntimeImagePullFailed = notificationintent.NotificationTypeRuntimeImagePullFailed
|
||||
|
||||
// NotificationTypeRuntimeContainerStartFailed identifies the
|
||||
// `runtime.container_start_failed` notification.
|
||||
NotificationTypeRuntimeContainerStartFailed = notificationintent.NotificationTypeRuntimeContainerStartFailed
|
||||
|
||||
// NotificationTypeRuntimeStartConfigInvalid identifies the
|
||||
// `runtime.start_config_invalid` notification.
|
||||
NotificationTypeRuntimeStartConfigInvalid = notificationintent.NotificationTypeRuntimeStartConfigInvalid
|
||||
)
|
||||
|
||||
// Producer identifies one supported upstream producer.
|
||||
@@ -99,6 +111,9 @@ const (
|
||||
|
||||
// ProducerGameLobby identifies Game Lobby.
|
||||
ProducerGameLobby = notificationintent.ProducerGameLobby
|
||||
|
||||
// ProducerRuntimeManager identifies Runtime Manager.
|
||||
ProducerRuntimeManager = notificationintent.ProducerRuntimeManager
|
||||
)
|
||||
|
||||
// AudienceKind identifies one supported target-audience kind.
|
||||
|
||||
@@ -46,10 +46,13 @@ const (
|
||||
userServiceBaseURLEnvVar = "NOTIFICATION_USER_SERVICE_BASE_URL"
|
||||
userServiceTimeoutEnvVar = "NOTIFICATION_USER_SERVICE_TIMEOUT"
|
||||
|
||||
adminEmailsGeoReviewRecommendedEnvVar = "NOTIFICATION_ADMIN_EMAILS_GEO_REVIEW_RECOMMENDED"
|
||||
adminEmailsGameGenerationFailedEnvVar = "NOTIFICATION_ADMIN_EMAILS_GAME_GENERATION_FAILED"
|
||||
adminEmailsLobbyRuntimePausedAfterEnvVar = "NOTIFICATION_ADMIN_EMAILS_LOBBY_RUNTIME_PAUSED_AFTER_START"
|
||||
adminEmailsLobbyApplicationSubmittedEnvVar = "NOTIFICATION_ADMIN_EMAILS_LOBBY_APPLICATION_SUBMITTED"
|
||||
adminEmailsGeoReviewRecommendedEnvVar = "NOTIFICATION_ADMIN_EMAILS_GEO_REVIEW_RECOMMENDED"
|
||||
adminEmailsGameGenerationFailedEnvVar = "NOTIFICATION_ADMIN_EMAILS_GAME_GENERATION_FAILED"
|
||||
adminEmailsLobbyRuntimePausedAfterEnvVar = "NOTIFICATION_ADMIN_EMAILS_LOBBY_RUNTIME_PAUSED_AFTER_START"
|
||||
adminEmailsLobbyApplicationSubmittedEnvVar = "NOTIFICATION_ADMIN_EMAILS_LOBBY_APPLICATION_SUBMITTED"
|
||||
adminEmailsRuntimeImagePullFailedEnvVar = "NOTIFICATION_ADMIN_EMAILS_RUNTIME_IMAGE_PULL_FAILED"
|
||||
adminEmailsRuntimeContainerStartFailedEnvVar = "NOTIFICATION_ADMIN_EMAILS_RUNTIME_CONTAINER_START_FAILED"
|
||||
adminEmailsRuntimeStartConfigInvalidEnvVar = "NOTIFICATION_ADMIN_EMAILS_RUNTIME_START_CONFIG_INVALID"
|
||||
|
||||
otelServiceNameEnvVar = "OTEL_SERVICE_NAME"
|
||||
otelTracesExporterEnvVar = "OTEL_TRACES_EXPORTER"
|
||||
@@ -60,18 +63,18 @@ const (
|
||||
otelStdoutTracesEnabledEnvVar = "NOTIFICATION_OTEL_STDOUT_TRACES_ENABLED"
|
||||
otelStdoutMetricsEnabledEnvVar = "NOTIFICATION_OTEL_STDOUT_METRICS_ENABLED"
|
||||
|
||||
defaultShutdownTimeout = 5 * time.Second
|
||||
defaultLogLevel = "info"
|
||||
defaultInternalHTTPAddr = ":8092"
|
||||
defaultReadHeaderTimeout = 2 * time.Second
|
||||
defaultReadTimeout = 10 * time.Second
|
||||
defaultIdleTimeout = time.Minute
|
||||
defaultShutdownTimeout = 5 * time.Second
|
||||
defaultLogLevel = "info"
|
||||
defaultInternalHTTPAddr = ":8092"
|
||||
defaultReadHeaderTimeout = 2 * time.Second
|
||||
defaultReadTimeout = 10 * time.Second
|
||||
defaultIdleTimeout = time.Minute
|
||||
|
||||
defaultIntentsStream = "notification:intents"
|
||||
defaultIntentsReadBlockTimeout = 2 * time.Second
|
||||
defaultGatewayClientEventsStream = "gateway:client-events"
|
||||
defaultIntentsStream = "notification:intents"
|
||||
defaultIntentsReadBlockTimeout = 2 * time.Second
|
||||
defaultGatewayClientEventsStream = "gateway:client-events"
|
||||
defaultGatewayClientEventsStreamMaxLen int64 = 1024
|
||||
defaultMailDeliveryCommandsStream = "mail:delivery_commands"
|
||||
defaultMailDeliveryCommandsStream = "mail:delivery_commands"
|
||||
|
||||
defaultPushRetryMaxAttempts = 3
|
||||
defaultEmailRetryMaxAttempts = 7
|
||||
@@ -352,6 +355,18 @@ type AdminRoutingConfig struct {
|
||||
// LobbyApplicationSubmitted stores recipients for public
|
||||
// `lobby.application.submitted` notifications.
|
||||
LobbyApplicationSubmitted []string
|
||||
|
||||
// RuntimeImagePullFailed stores recipients for
|
||||
// `runtime.image_pull_failed`.
|
||||
RuntimeImagePullFailed []string
|
||||
|
||||
// RuntimeContainerStartFailed stores recipients for
|
||||
// `runtime.container_start_failed`.
|
||||
RuntimeContainerStartFailed []string
|
||||
|
||||
// RuntimeStartConfigInvalid stores recipients for
|
||||
// `runtime.start_config_invalid`.
|
||||
RuntimeStartConfigInvalid []string
|
||||
}
|
||||
|
||||
// Validate reports whether cfg stores valid normalized administrator email
|
||||
@@ -369,6 +384,15 @@ func (cfg AdminRoutingConfig) Validate() error {
|
||||
if err := validateNormalizedEmailList("lobby.application.submitted", cfg.LobbyApplicationSubmitted); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateNormalizedEmailList("runtime.image_pull_failed", cfg.RuntimeImagePullFailed); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateNormalizedEmailList("runtime.container_start_failed", cfg.RuntimeContainerStartFailed); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateNormalizedEmailList("runtime.start_config_invalid", cfg.RuntimeStartConfigInvalid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ const (
|
||||
envRedisTLSEnabled = "NOTIFICATION_REDIS_TLS_ENABLED"
|
||||
envRedisUsername = "NOTIFICATION_REDIS_USERNAME"
|
||||
|
||||
envPostgresPrimaryDSN = "NOTIFICATION_POSTGRES_PRIMARY_DSN"
|
||||
envPostgresOpTimeout = "NOTIFICATION_POSTGRES_OPERATION_TIMEOUT"
|
||||
envPostgresMaxOpenConns = "NOTIFICATION_POSTGRES_MAX_OPEN_CONNS"
|
||||
envPostgresMaxIdleConns = "NOTIFICATION_POSTGRES_MAX_IDLE_CONNS"
|
||||
envPostgresConnMaxLife = "NOTIFICATION_POSTGRES_CONN_MAX_LIFETIME"
|
||||
envPostgresPrimaryDSN = "NOTIFICATION_POSTGRES_PRIMARY_DSN"
|
||||
envPostgresOpTimeout = "NOTIFICATION_POSTGRES_OPERATION_TIMEOUT"
|
||||
envPostgresMaxOpenConns = "NOTIFICATION_POSTGRES_MAX_OPEN_CONNS"
|
||||
envPostgresMaxIdleConns = "NOTIFICATION_POSTGRES_MAX_IDLE_CONNS"
|
||||
envPostgresConnMaxLife = "NOTIFICATION_POSTGRES_CONN_MAX_LIFETIME"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -104,6 +104,9 @@ func TestLoadFromEnvAppliesOverrides(t *testing.T) {
|
||||
t.Setenv(adminEmailsGameGenerationFailedEnvVar, "ops@example.com")
|
||||
t.Setenv(adminEmailsLobbyRuntimePausedAfterEnvVar, "pause@example.com, PAUSE@example.com")
|
||||
t.Setenv(adminEmailsLobbyApplicationSubmittedEnvVar, "owner@example.com, OWNER@example.com")
|
||||
t.Setenv(adminEmailsRuntimeImagePullFailedEnvVar, "image-pull-ops@example.com, IMAGE-PULL-OPS@example.com")
|
||||
t.Setenv(adminEmailsRuntimeContainerStartFailedEnvVar, "container-start-ops@example.com")
|
||||
t.Setenv(adminEmailsRuntimeStartConfigInvalidEnvVar, "start-config-ops@example.com, START-CONFIG-OPS@example.com")
|
||||
t.Setenv(otelServiceNameEnvVar, "custom-notification")
|
||||
t.Setenv(otelTracesExporterEnvVar, "otlp")
|
||||
t.Setenv(otelMetricsExporterEnvVar, "otlp")
|
||||
@@ -172,6 +175,9 @@ func TestLoadFromEnvAppliesOverrides(t *testing.T) {
|
||||
GameGenerationFailed: []string{"ops@example.com"},
|
||||
LobbyRuntimePausedAfterStart: []string{"pause@example.com"},
|
||||
LobbyApplicationSubmitted: []string{"owner@example.com"},
|
||||
RuntimeImagePullFailed: []string{"image-pull-ops@example.com"},
|
||||
RuntimeContainerStartFailed: []string{"container-start-ops@example.com"},
|
||||
RuntimeStartConfigInvalid: []string{"start-config-ops@example.com"},
|
||||
}, cfg.AdminRouting)
|
||||
require.Equal(t, TelemetryConfig{
|
||||
ServiceName: "custom-notification",
|
||||
@@ -295,6 +301,7 @@ func TestLoadFromEnvRejectsInvalidConfiguration(t *testing.T) {
|
||||
{name: "invalid admin email", envName: adminEmailsGeoReviewRecommendedEnvVar, envVal: "broken-email", want: "invalid email address"},
|
||||
{name: "blank admin email slot", envName: adminEmailsGameGenerationFailedEnvVar, envVal: "ops@example.com, , second@example.com", want: "must not be empty"},
|
||||
{name: "invalid public application admin email", envName: adminEmailsLobbyApplicationSubmittedEnvVar, envVal: "Owner <owner@example.com>", want: "must not include a display name"},
|
||||
{name: "invalid runtime image pull admin email", envName: adminEmailsRuntimeImagePullFailedEnvVar, envVal: "broken-runtime-email", want: "invalid email address"},
|
||||
{name: "nonpositive gateway client events stream max len", envName: gatewayClientEventsStreamMaxEnvVar, envVal: "0", want: "must be positive"},
|
||||
{name: "backoff min above max", envName: routeBackoffMinEnvVar, envVal: "10m", want: "must not exceed"},
|
||||
}
|
||||
|
||||
@@ -128,6 +128,18 @@ func LoadFromEnv() (Config, error) {
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
cfg.AdminRouting.RuntimeImagePullFailed, err = emailListEnv(adminEmailsRuntimeImagePullFailedEnvVar, cfg.AdminRouting.RuntimeImagePullFailed)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
cfg.AdminRouting.RuntimeContainerStartFailed, err = emailListEnv(adminEmailsRuntimeContainerStartFailedEnvVar, cfg.AdminRouting.RuntimeContainerStartFailed)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
cfg.AdminRouting.RuntimeStartConfigInvalid, err = emailListEnv(adminEmailsRuntimeStartConfigInvalidEnvVar, cfg.AdminRouting.RuntimeStartConfigInvalid)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
cfg.Telemetry.ServiceName = stringEnv(otelServiceNameEnvVar, cfg.Telemetry.ServiceName)
|
||||
cfg.Telemetry.TracesExporter = normalizeExporterValue(stringEnv(otelTracesExporterEnvVar, cfg.Telemetry.TracesExporter))
|
||||
|
||||
@@ -809,6 +809,12 @@ func (service *Service) adminEmailsFor(notificationType intentstream.Notificatio
|
||||
return append([]string(nil), service.adminRouting.LobbyRuntimePausedAfterStart...)
|
||||
case intentstream.NotificationTypeLobbyApplicationSubmitted:
|
||||
return append([]string(nil), service.adminRouting.LobbyApplicationSubmitted...)
|
||||
case intentstream.NotificationTypeRuntimeImagePullFailed:
|
||||
return append([]string(nil), service.adminRouting.RuntimeImagePullFailed...)
|
||||
case intentstream.NotificationTypeRuntimeContainerStartFailed:
|
||||
return append([]string(nil), service.adminRouting.RuntimeContainerStartFailed...)
|
||||
case intentstream.NotificationTypeRuntimeStartConfigInvalid:
|
||||
return append([]string(nil), service.adminRouting.RuntimeStartConfigInvalid...)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,7 +26,10 @@ const expectedNotificationMailTemplateTable = `| ` + "`notification_type`" + ` |
|
||||
| ` + "`lobby.invite.expired`" + ` | ` + "`lobby.invite.expired`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`lobby.race_name.registration_eligible`" + ` | ` + "`lobby.race_name.registration_eligible`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`lobby.race_name.registered`" + ` | ` + "`lobby.race_name.registered`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`lobby.race_name.registration_denied`" + ` | ` + "`lobby.race_name.registration_denied`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |`
|
||||
| ` + "`lobby.race_name.registration_denied`" + ` | ` + "`lobby.race_name.registration_denied`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`runtime.image_pull_failed`" + ` | ` + "`runtime.image_pull_failed`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`runtime.container_start_failed`" + ` | ` + "`runtime.container_start_failed`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |
|
||||
| ` + "`runtime.start_config_invalid`" + ` | ` + "`runtime.start_config_invalid`" + ` | ` + "`en/subject.tmpl`" + `, ` + "`en/text.tmpl`" + ` |`
|
||||
|
||||
var expectedNotificationMailReadmeSnippets = []string{
|
||||
"`payload_mode` is always `template`",
|
||||
|
||||
Reference in New Issue
Block a user