feat(coderd): add retention policy configuration (#21021)

Add `RetentionConfig` with server flags for configuring data retention:

- `--audit-logs-retention`: retention for audit log entries
- `--connection-logs-retention`: retention for connection logs
- `--api-keys-retention`: retention for expired API keys (default 7d)

Updates #20743
This commit is contained in:
Mathias Fredriksson
2025-12-02 16:04:06 +02:00
committed by GitHub
parent 74d0c39cb3
commit 56e7858570
11 changed files with 314 additions and 0 deletions
+57
View File
@@ -501,6 +501,7 @@ type DeploymentValues struct {
WebTerminalRenderer serpent.String `json:"web_terminal_renderer,omitempty" typescript:",notnull"`
AllowWorkspaceRenames serpent.Bool `json:"allow_workspace_renames,omitempty" typescript:",notnull"`
Healthcheck HealthcheckConfig `json:"healthcheck,omitempty" typescript:",notnull"`
Retention RetentionConfig `json:"retention,omitempty" typescript:",notnull"`
CLIUpgradeMessage serpent.String `json:"cli_upgrade_message,omitempty" typescript:",notnull"`
TermsOfServiceURL serpent.String `json:"terms_of_service_url,omitempty" typescript:",notnull"`
Notifications NotificationsConfig `json:"notifications,omitempty" typescript:",notnull"`
@@ -813,6 +814,23 @@ type HealthcheckConfig struct {
ThresholdDatabase serpent.Duration `json:"threshold_database" typescript:",notnull"`
}
// RetentionConfig contains configuration for data retention policies.
// These settings control how long various types of data are retained in the database
// before being automatically purged. Setting a value to 0 disables retention for that
// data type (data is kept indefinitely).
type RetentionConfig struct {
// AuditLogs controls how long audit log entries are retained.
// Set to 0 to disable (keep indefinitely).
AuditLogs serpent.Duration `json:"audit_logs" typescript:",notnull"`
// ConnectionLogs controls how long connection log entries are retained.
// Set to 0 to disable (keep indefinitely).
ConnectionLogs serpent.Duration `json:"connection_logs" typescript:",notnull"`
// APIKeys controls how long expired API keys are retained before being deleted.
// Keys are only deleted if they have been expired for at least this duration.
// Defaults to 7 days to preserve existing behavior.
APIKeys serpent.Duration `json:"api_keys" typescript:",notnull"`
}
type NotificationsConfig struct {
// The upper limit of attempts to send a notification.
MaxSendAttempts serpent.Int64 `json:"max_send_attempts" typescript:",notnull"`
@@ -1180,6 +1198,11 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
Name: "AI Bridge",
YAML: "aibridge",
}
deploymentGroupRetention = serpent.Group{
Name: "Retention",
Description: "Configure data retention policies for various database tables. Retention policies automatically purge old data to reduce database size and improve performance. Setting a retention duration to 0 disables automatic purging for that data type.",
YAML: "retention",
}
)
httpAddress := serpent.Option{
@@ -3363,6 +3386,40 @@ Write out the current server config as YAML to stdout.`,
YAML: "retention",
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
},
// Retention settings
{
Name: "Audit Logs Retention",
Description: "How long audit log entries are retained. Set to 0 to disable (keep indefinitely). We advise keeping audit logs for at least a year, and in accordance with your compliance requirements.",
Flag: "audit-logs-retention",
Env: "CODER_AUDIT_LOGS_RETENTION",
Value: &c.Retention.AuditLogs,
Default: "0",
Group: &deploymentGroupRetention,
YAML: "audit_logs",
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
},
{
Name: "Connection Logs Retention",
Description: "How long connection log entries are retained. Set to 0 to disable (keep indefinitely).",
Flag: "connection-logs-retention",
Env: "CODER_CONNECTION_LOGS_RETENTION",
Value: &c.Retention.ConnectionLogs,
Default: "0",
Group: &deploymentGroupRetention,
YAML: "connection_logs",
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
},
{
Name: "API Keys Retention",
Description: "How long expired API keys are retained before being deleted. Keeping expired keys allows the backend to return a more helpful error when a user tries to use an expired key. Set to 0 to disable automatic deletion of expired keys.",
Flag: "api-keys-retention",
Env: "CODER_API_KEYS_RETENTION",
Value: &c.Retention.APIKeys,
Default: "7d",
Group: &deploymentGroupRetention,
YAML: "api_keys",
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
},
{
Name: "Enable Authorization Recordings",
Description: "All api requests will have a header including all authorization calls made during the request. " +
+62
View File
@@ -703,3 +703,65 @@ func TestNotificationsCanBeDisabled(t *testing.T) {
})
}
}
func TestRetentionConfigParsing(t *testing.T) {
t.Parallel()
tests := []struct {
name string
environment []serpent.EnvVar
expectedAuditLogs time.Duration
expectedConnectionLogs time.Duration
expectedAPIKeys time.Duration
}{
{
name: "Defaults",
environment: []serpent.EnvVar{},
expectedAuditLogs: 0,
expectedConnectionLogs: 0,
expectedAPIKeys: 7 * 24 * time.Hour, // 7 days default
},
{
name: "IndividualRetentionSet",
environment: []serpent.EnvVar{
{Name: "CODER_AUDIT_LOGS_RETENTION", Value: "30d"},
{Name: "CODER_CONNECTION_LOGS_RETENTION", Value: "60d"},
{Name: "CODER_API_KEYS_RETENTION", Value: "14d"},
},
expectedAuditLogs: 30 * 24 * time.Hour,
expectedConnectionLogs: 60 * 24 * time.Hour,
expectedAPIKeys: 14 * 24 * time.Hour,
},
{
name: "AllRetentionSet",
environment: []serpent.EnvVar{
{Name: "CODER_AUDIT_LOGS_RETENTION", Value: "365d"},
{Name: "CODER_CONNECTION_LOGS_RETENTION", Value: "30d"},
{Name: "CODER_API_KEYS_RETENTION", Value: "0"},
},
expectedAuditLogs: 365 * 24 * time.Hour,
expectedConnectionLogs: 30 * 24 * time.Hour,
expectedAPIKeys: 0, // Explicitly disabled
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
dv := codersdk.DeploymentValues{}
opts := dv.Options()
err := opts.SetDefaults()
require.NoError(t, err)
err = opts.ParseEnv(tt.environment)
require.NoError(t, err)
assert.Equal(t, tt.expectedAuditLogs, dv.Retention.AuditLogs.Value(), "audit logs retention mismatch")
assert.Equal(t, tt.expectedConnectionLogs, dv.Retention.ConnectionLogs.Value(), "connection logs retention mismatch")
assert.Equal(t, tt.expectedAPIKeys, dv.Retention.APIKeys.Value(), "api keys retention mismatch")
})
}
}