Reject invalid deployment configuration early
This commit is contained in:
parent
2020dd4ed3
commit
0e351c3640
7 changed files with 543 additions and 55 deletions
|
|
@ -104,7 +104,7 @@ impl ApiApplicationConfig {
|
|||
let lease_ttl = env_duration("REMORA_LEASE_TTL_SECONDS", 30)?;
|
||||
let target = TargetStoreConfig::from_env().map_err(runtime_error)?;
|
||||
let vault = VaultConfig::from_env().map_err(runtime_error)?;
|
||||
Ok(Self {
|
||||
let config = Self {
|
||||
public_bind: env_parse("REMORA_PUBLIC_BIND", "0.0.0.0:8080")?,
|
||||
worker_bind: env_parse("REMORA_WORKER_BIND", "0.0.0.0:8443")?,
|
||||
management: ManagementConfig::from_env(crate::COMPONENT_NAME)?,
|
||||
|
|
@ -156,10 +156,54 @@ impl ApiApplicationConfig {
|
|||
private_key: PathBuf::from(required_env("REMORA_WORKER_TLS_KEY_FILE")?),
|
||||
client_ca: PathBuf::from(required_env("REMORA_WORKER_TLS_CLIENT_CA_FILE")?),
|
||||
},
|
||||
})
|
||||
};
|
||||
validate_api_limits(
|
||||
config.database_connections,
|
||||
&config.coordination,
|
||||
&config.attempt,
|
||||
&config.recovery,
|
||||
&config.public_api,
|
||||
)?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_api_limits(
|
||||
database_connections: u32,
|
||||
coordination: &StoreConfig,
|
||||
attempt: &AttemptConfig,
|
||||
recovery: &RecoveryConfig,
|
||||
public_api: &PublicApiConfig,
|
||||
) -> Result<(), RuntimeError> {
|
||||
if database_connections == 0
|
||||
|| coordination.global_limit == 0
|
||||
|| coordination.lease_ttl.is_zero()
|
||||
|| coordination.reserve_ttl.is_zero()
|
||||
|| coordination.progress_ttl.is_zero()
|
||||
|| attempt.max_attempts == 0
|
||||
|| recovery.retry_delay.is_zero()
|
||||
|| recovery.cleanup_claim_ttl.is_zero()
|
||||
|| recovery.batch_size == 0
|
||||
|| public_api.cursor_secret.is_empty()
|
||||
|| public_api.member_access_ttl.is_zero()
|
||||
|| public_api.event_retention.is_zero()
|
||||
|| public_api.event_poll_interval.is_zero()
|
||||
|| public_api.event_heartbeat_interval.is_zero()
|
||||
|| public_api.event_ticket_ttl.is_zero()
|
||||
|| public_api.event_ticket_ttl > Duration::from_secs(60)
|
||||
|| public_api.event_allowed_origins.is_empty()
|
||||
|| public_api
|
||||
.event_allowed_origins
|
||||
.iter()
|
||||
.any(|origin| origin.is_empty() || origin.trim() != origin)
|
||||
{
|
||||
return Err(RuntimeError::message(
|
||||
"API limits and timing must be positive and event ticket expiry must not exceed 60 seconds",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl ApiApplication {
|
||||
#[must_use]
|
||||
pub fn new(config: ApiApplicationConfig) -> Self {
|
||||
|
|
@ -657,4 +701,33 @@ mod tests {
|
|||
"cannot read REMORA_CURSOR_SECRET_FILE: entity not found"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_api_limits_before_external_connections() {
|
||||
let coordination = StoreConfig {
|
||||
global_limit: 1,
|
||||
lease_ttl: Duration::from_secs(30),
|
||||
reserve_ttl: Duration::from_secs(30),
|
||||
progress_ttl: Duration::from_secs(60),
|
||||
cancel_ttl: None,
|
||||
};
|
||||
let attempt = AttemptConfig { max_attempts: 1 };
|
||||
let recovery = RecoveryConfig {
|
||||
retry_delay: Duration::from_secs(1),
|
||||
cleanup_claim_ttl: Duration::from_secs(30),
|
||||
batch_size: 1,
|
||||
};
|
||||
let mut public_api = PublicApiConfig {
|
||||
cursor_secret: vec![1; 32],
|
||||
member_access_ttl: Duration::from_secs(60),
|
||||
event_retention: Duration::from_secs(3600),
|
||||
event_poll_interval: Duration::from_millis(250),
|
||||
event_heartbeat_interval: Duration::from_secs(15),
|
||||
event_ticket_ttl: Duration::from_secs(30),
|
||||
event_allowed_origins: vec!["https://remora.example".into()],
|
||||
};
|
||||
assert!(validate_api_limits(1, &coordination, &attempt, &recovery, &public_api).is_ok());
|
||||
public_api.event_ticket_ttl = Duration::from_secs(61);
|
||||
assert!(validate_api_limits(1, &coordination, &attempt, &recovery, &public_api).is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ impl WorkerApplicationConfig {
|
|||
endpoint: target.endpoint.clone(),
|
||||
region: target.region.clone(),
|
||||
});
|
||||
Ok(Self {
|
||||
let config = Self {
|
||||
management: ManagementConfig::from_env(crate::COMPONENT_NAME)?,
|
||||
database_url: required_env("DATABASE_URL")?,
|
||||
database_connections: env_parse("REMORA_DATABASE_MAX_CONNECTIONS", "16")?,
|
||||
|
|
@ -124,7 +124,9 @@ impl WorkerApplicationConfig {
|
|||
"REMORA_HUGGING_FACE_ENDPOINT",
|
||||
"https://huggingface.co",
|
||||
),
|
||||
})
|
||||
};
|
||||
validate_worker_timing(&config.coordination, &config.worker)?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -324,6 +326,27 @@ fn parse_scheme_ports(input: &str) -> Result<Vec<(String, u16)>, RuntimeError> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn validate_worker_timing(
|
||||
coordination: &StoreConfig,
|
||||
worker: &WorkerConfig,
|
||||
) -> Result<(), RuntimeError> {
|
||||
if coordination.global_limit == 0
|
||||
|| coordination.lease_ttl.is_zero()
|
||||
|| coordination.reserve_ttl.is_zero()
|
||||
|| coordination.progress_ttl.is_zero()
|
||||
|| worker.local_limit == 0
|
||||
|| worker.idle_delay.is_zero()
|
||||
|| worker.renew_interval.is_zero()
|
||||
|| worker.renew_interval >= coordination.lease_ttl
|
||||
|| worker.cancel_poll_interval.is_zero()
|
||||
{
|
||||
return Err(RuntimeError::message(
|
||||
"worker limits and timing must be positive, and renewal must precede lease expiry",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_shutdown() -> Result<(), RuntimeError> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
|
|
@ -387,4 +410,19 @@ mod tests {
|
|||
assert!(parse_scheme_ports("https").is_err());
|
||||
assert!(parse_cidrs("private").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_worker_timing_before_external_connections() {
|
||||
let coordination = StoreConfig {
|
||||
global_limit: 1,
|
||||
lease_ttl: Duration::from_secs(30),
|
||||
reserve_ttl: Duration::from_secs(30),
|
||||
progress_ttl: Duration::from_secs(60),
|
||||
cancel_ttl: None,
|
||||
};
|
||||
let mut worker = WorkerConfig::default();
|
||||
assert!(validate_worker_timing(&coordination, &worker).is_ok());
|
||||
worker.renew_interval = coordination.lease_ttl;
|
||||
assert!(validate_worker_timing(&coordination, &worker).is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,36 @@
|
|||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "remora.validateValues" -}}
|
||||
{{- if ge (int .Values.runtime.workerRenewSeconds) (int .Values.coordination.leaseTtlSeconds) -}}
|
||||
{{- fail "runtime.workerRenewSeconds must be less than coordination.leaseTtlSeconds" -}}
|
||||
{{- end -}}
|
||||
{{- range $entry := splitList "," .Values.outbound.allowedSchemePorts -}}
|
||||
{{- $parts := splitList ":" $entry -}}
|
||||
{{- $port := int (index $parts 1) -}}
|
||||
{{- if or (lt $port 1) (gt $port 65535) -}}
|
||||
{{- fail "outbound.allowedSchemePorts contains a port outside 1..65535" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if and .Values.networkPolicy.enabled .Values.outbound.allowedCidrs -}}
|
||||
{{- range $cidr := splitList "," .Values.outbound.allowedCidrs -}}
|
||||
{{- if not (has $cidr $.Values.networkPolicy.externalEgressCidrs) -}}
|
||||
{{- fail "every outbound.allowedCidrs entry must appear in networkPolicy.externalEgressCidrs" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if .Values.podDisruptionBudget.enabled -}}
|
||||
{{- $minimum := toString .Values.podDisruptionBudget.minAvailable -}}
|
||||
{{- if not (hasSuffix "%" $minimum) -}}
|
||||
{{- range $component := list "api" "worker" "web" -}}
|
||||
{{- if gt (int $minimum) (int (get $.Values $component).replicas) -}}
|
||||
{{- fail "podDisruptionBudget.minAvailable cannot exceed a workload replica count" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "remora.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{{- include "remora.validateValues" . }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
|
|
@ -24,6 +25,7 @@ data:
|
|||
REMORA_RESERVED_TARGET_SEGMENTS: {{ required "runtime.reservedTargetSegments is required" .Values.runtime.reservedTargetSegments | quote }}
|
||||
REMORA_ADMISSION_POLICY_VERSION: {{ required "runtime.admissionPolicyVersion is required" .Values.runtime.admissionPolicyVersion | quote }}
|
||||
REMORA_MAX_ATTEMPTS: {{ .Values.runtime.maxAttempts | quote }}
|
||||
REMORA_WORKER_RENEW_SECONDS: {{ .Values.runtime.workerRenewSeconds | quote }}
|
||||
REMORA_RECOVERY_INTERVAL_SECONDS: {{ .Values.runtime.recoveryIntervalSeconds | quote }}
|
||||
REMORA_EVENT_ALLOWED_ORIGINS: {{ required "runtime.eventAllowedOrigins is required" .Values.runtime.eventAllowedOrigins | quote }}
|
||||
REMORA_EVENT_RETENTION_SECONDS: {{ .Values.runtime.eventRetentionSeconds | quote }}
|
||||
|
|
|
|||
|
|
@ -1,98 +1,423 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["api", "worker", "web", "database", "coordination", "oidc", "target", "vault"],
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"nameOverride",
|
||||
"fullnameOverride",
|
||||
"imagePullSecrets",
|
||||
"podDnsPolicy",
|
||||
"podDnsConfig",
|
||||
"api",
|
||||
"worker",
|
||||
"web",
|
||||
"database",
|
||||
"coordination",
|
||||
"oidc",
|
||||
"target",
|
||||
"vault",
|
||||
"runtime",
|
||||
"outbound",
|
||||
"ingress",
|
||||
"networkPolicy",
|
||||
"observability",
|
||||
"podDisruptionBudget"
|
||||
],
|
||||
"properties": {
|
||||
"api": {"$ref": "#/$defs/workload"},
|
||||
"worker": {"$ref": "#/$defs/workload"},
|
||||
"podDnsPolicy": {"enum": ["ClusterFirst", "ClusterFirstWithHostNet", "Default", "None"]},
|
||||
"podDnsConfig": {"type": "object"},
|
||||
"web": {
|
||||
"allOf": [{"$ref": "#/$defs/workload"}],
|
||||
"type": "object",
|
||||
"required": ["publicOrigin", "oidcClientId"],
|
||||
"properties": {
|
||||
"publicOrigin": {"type": "string", "format": "uri"},
|
||||
"oidcClientId": {"type": "string", "minLength": 1}
|
||||
"nameOverride": {"$ref": "#/$defs/dnsLabelOrEmpty"},
|
||||
"fullnameOverride": {"$ref": "#/$defs/dnsLabelOrEmpty"},
|
||||
"imagePullSecrets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name"],
|
||||
"properties": {"name": {"$ref": "#/$defs/nonEmpty"}}
|
||||
}
|
||||
},
|
||||
"podDnsPolicy": {"enum": ["ClusterFirst", "ClusterFirstWithHostNet", "Default", "None"]},
|
||||
"podDnsConfig": {"$ref": "#/$defs/podDnsConfig"},
|
||||
"api": {"$ref": "#/$defs/workload"},
|
||||
"worker": {"$ref": "#/$defs/workload"},
|
||||
"web": {"$ref": "#/$defs/webWorkload"},
|
||||
"database": {
|
||||
"type": "object",
|
||||
"required": ["urlSecret"],
|
||||
"properties": {"urlSecret": {"$ref": "#/$defs/secretRef"}}
|
||||
"additionalProperties": false,
|
||||
"required": ["urlSecret", "maxApiConnections", "maxWorkerConnections"],
|
||||
"properties": {
|
||||
"urlSecret": {"$ref": "#/$defs/secretRef"},
|
||||
"maxApiConnections": {"$ref": "#/$defs/positiveInteger"},
|
||||
"maxWorkerConnections": {"$ref": "#/$defs/positiveInteger"}
|
||||
}
|
||||
},
|
||||
"coordination": {
|
||||
"type": "object",
|
||||
"required": ["urlSecret", "namespace", "globalAttemptLimit"],
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"urlSecret",
|
||||
"namespace",
|
||||
"globalAttemptLimit",
|
||||
"leaseTtlSeconds",
|
||||
"reserveTtlSeconds",
|
||||
"progressTtlSeconds"
|
||||
],
|
||||
"properties": {
|
||||
"urlSecret": {"$ref": "#/$defs/secretRef"},
|
||||
"namespace": {"type": "string", "minLength": 1},
|
||||
"globalAttemptLimit": {"type": "integer", "minimum": 1}
|
||||
"namespace": {"$ref": "#/$defs/nonEmpty"},
|
||||
"globalAttemptLimit": {"$ref": "#/$defs/positiveInteger"},
|
||||
"leaseTtlSeconds": {"$ref": "#/$defs/positiveInteger"},
|
||||
"reserveTtlSeconds": {"$ref": "#/$defs/positiveInteger"},
|
||||
"progressTtlSeconds": {"$ref": "#/$defs/positiveInteger"}
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"type": "object",
|
||||
"required": ["issuer", "audience", "resourceClient"],
|
||||
"additionalProperties": false,
|
||||
"required": ["issuer", "audience", "resourceClient", "projectRoleClaims"],
|
||||
"properties": {
|
||||
"issuer": {"type": "string", "format": "uri"},
|
||||
"audience": {"type": "string", "minLength": 1},
|
||||
"resourceClient": {"type": "string", "minLength": 1}
|
||||
"issuer": {"$ref": "#/$defs/httpUrl"},
|
||||
"audience": {"$ref": "#/$defs/nonEmpty"},
|
||||
"resourceClient": {"$ref": "#/$defs/nonEmpty"},
|
||||
"projectRoleClaims": {"$ref": "#/$defs/commaList"}
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"type": "object",
|
||||
"required": ["storeId", "endpoint", "bucket", "exclusiveWriter", "credentialsSecret"],
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"storeId",
|
||||
"endpoint",
|
||||
"bucket",
|
||||
"region",
|
||||
"root",
|
||||
"defaultPrefix",
|
||||
"exclusiveWriter",
|
||||
"credentialsSecret"
|
||||
],
|
||||
"properties": {
|
||||
"storeId": {"type": "string", "minLength": 1},
|
||||
"endpoint": {"type": "string", "format": "uri"},
|
||||
"bucket": {"type": "string", "minLength": 1},
|
||||
"storeId": {"$ref": "#/$defs/nonEmpty"},
|
||||
"endpoint": {"$ref": "#/$defs/httpUrl"},
|
||||
"bucket": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{"pattern": "^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$"},
|
||||
{"not": {"pattern": "\\.\\."}}
|
||||
]
|
||||
},
|
||||
"region": {"$ref": "#/$defs/nonEmpty"},
|
||||
"root": {"type": "string", "maxLength": 1024},
|
||||
"defaultPrefix": {"type": "string", "maxLength": 1024},
|
||||
"exclusiveWriter": {"const": true},
|
||||
"credentialsSecret": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "accessKey", "secretKey"],
|
||||
"properties": {
|
||||
"name": {"type": "string", "minLength": 1},
|
||||
"accessKey": {"type": "string", "minLength": 1},
|
||||
"secretKey": {"type": "string", "minLength": 1}
|
||||
"name": {"$ref": "#/$defs/nonEmpty"},
|
||||
"accessKey": {"$ref": "#/$defs/nonEmpty"},
|
||||
"secretKey": {"$ref": "#/$defs/nonEmpty"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vault": {
|
||||
"type": "object",
|
||||
"required": ["address", "path", "apiRole", "workerRole"],
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"image",
|
||||
"address",
|
||||
"path",
|
||||
"kubernetesAuthMount",
|
||||
"audience",
|
||||
"apiRole",
|
||||
"workerRole",
|
||||
"caSecret"
|
||||
],
|
||||
"properties": {
|
||||
"address": {"type": "string", "format": "uri"},
|
||||
"path": {"type": "string", "minLength": 1},
|
||||
"apiRole": {"type": "string", "minLength": 1},
|
||||
"workerRole": {"type": "string", "minLength": 1}
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"workload": {
|
||||
"type": "object",
|
||||
"required": ["replicas", "image"],
|
||||
"properties": {
|
||||
"replicas": {"type": "integer", "minimum": 1},
|
||||
"image": {
|
||||
"image": {"$ref": "#/$defs/nonEmpty"},
|
||||
"address": {"$ref": "#/$defs/httpUrl"},
|
||||
"path": {"$ref": "#/$defs/nonEmpty"},
|
||||
"kubernetesAuthMount": {"$ref": "#/$defs/nonEmpty"},
|
||||
"audience": {"$ref": "#/$defs/nonEmpty"},
|
||||
"apiRole": {"$ref": "#/$defs/nonEmpty"},
|
||||
"workerRole": {"$ref": "#/$defs/nonEmpty"},
|
||||
"caSecret": {
|
||||
"type": "object",
|
||||
"required": ["repository", "tag", "pullPolicy"],
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "key"],
|
||||
"properties": {
|
||||
"repository": {"type": "string", "minLength": 1},
|
||||
"tag": {"type": "string", "minLength": 1},
|
||||
"digest": {"type": "string", "pattern": "^(|sha256:[0-9a-f]{64})$"},
|
||||
"pullPolicy": {"enum": ["Always", "IfNotPresent", "Never"]}
|
||||
"name": {"type": "string"},
|
||||
"key": {"$ref": "#/$defs/nonEmpty"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"runtime": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"reservedTargetSegments",
|
||||
"admissionPolicyVersion",
|
||||
"maxAttempts",
|
||||
"workerLocalAttemptLimit",
|
||||
"workerRenewSeconds",
|
||||
"recoveryIntervalSeconds",
|
||||
"eventAllowedOrigins",
|
||||
"eventRetentionSeconds",
|
||||
"logFilter"
|
||||
],
|
||||
"properties": {
|
||||
"reservedTargetSegments": {"$ref": "#/$defs/commaList"},
|
||||
"admissionPolicyVersion": {"$ref": "#/$defs/nonEmpty"},
|
||||
"maxAttempts": {"$ref": "#/$defs/positiveInteger"},
|
||||
"workerLocalAttemptLimit": {"$ref": "#/$defs/positiveInteger"},
|
||||
"workerRenewSeconds": {"$ref": "#/$defs/positiveInteger"},
|
||||
"recoveryIntervalSeconds": {"$ref": "#/$defs/positiveInteger"},
|
||||
"eventAllowedOrigins": {"$ref": "#/$defs/httpUrlList"},
|
||||
"eventRetentionSeconds": {"$ref": "#/$defs/positiveInteger"},
|
||||
"logFilter": {"$ref": "#/$defs/nonEmpty"}
|
||||
}
|
||||
},
|
||||
"outbound": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["allowedSchemePorts", "allowedCidrs", "maxRedirects"],
|
||||
"properties": {
|
||||
"allowedSchemePorts": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9+.-]*:[1-9][0-9]{0,4}(,[a-z][a-z0-9+.-]*:[1-9][0-9]{0,4})*$"
|
||||
},
|
||||
"allowedCidrs": {"$ref": "#/$defs/cidrListOrEmpty"},
|
||||
"maxRedirects": {"type": "integer", "minimum": 0, "maximum": 100}
|
||||
}
|
||||
},
|
||||
"ingress": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["enabled", "className", "annotations", "host", "tlsSecretName"],
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"className": {"type": "string"},
|
||||
"annotations": {"$ref": "#/$defs/stringMap"},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$"
|
||||
},
|
||||
"tlsSecretName": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"networkPolicy": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"enabled",
|
||||
"ingressNamespaceSelector",
|
||||
"ingressPodSelector",
|
||||
"monitoringNamespaceSelector",
|
||||
"monitoringPodSelector",
|
||||
"dnsPodLabels",
|
||||
"dependencyPodLabels",
|
||||
"externalEgressCidrs",
|
||||
"allowPublicSourceEgress"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"ingressNamespaceSelector": {"$ref": "#/$defs/labelSelector"},
|
||||
"ingressPodSelector": {"$ref": "#/$defs/labelSelector"},
|
||||
"monitoringNamespaceSelector": {"$ref": "#/$defs/labelSelector"},
|
||||
"monitoringPodSelector": {"$ref": "#/$defs/labelSelector"},
|
||||
"dnsPodLabels": {"$ref": "#/$defs/labelMapList"},
|
||||
"dependencyPodLabels": {"$ref": "#/$defs/labelMapList"},
|
||||
"externalEgressCidrs": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {"$ref": "#/$defs/cidr"}
|
||||
},
|
||||
"allowPublicSourceEgress": {"type": "boolean"}
|
||||
}
|
||||
},
|
||||
"observability": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["serviceMonitor", "prometheusRule"],
|
||||
"properties": {
|
||||
"serviceMonitor": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["enabled", "interval"],
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"interval": {"type": "string", "pattern": "^[1-9][0-9]*(ms|s|m|h)$"}
|
||||
}
|
||||
},
|
||||
"prometheusRule": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["enabled"],
|
||||
"properties": {"enabled": {"type": "boolean"}}
|
||||
}
|
||||
}
|
||||
},
|
||||
"podDisruptionBudget": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["enabled", "minAvailable"],
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"minAvailable": {
|
||||
"oneOf": [
|
||||
{"type": "integer", "minimum": 0},
|
||||
{"type": "string", "pattern": "^(100|[0-9]{1,2})%$"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"nonEmpty": {"type": "string", "minLength": 1},
|
||||
"positiveInteger": {"type": "integer", "minimum": 1, "maximum": 2147483647},
|
||||
"dnsLabelOrEmpty": {
|
||||
"type": "string",
|
||||
"pattern": "^(|[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)$",
|
||||
"maxLength": 63
|
||||
},
|
||||
"httpUrl": {"type": "string", "pattern": "^https?://[^[:space:]]+$"},
|
||||
"httpUrlList": {
|
||||
"type": "string",
|
||||
"pattern": "^https?://[^,[:space:]]+(,https?://[^,[:space:]]+)*$"
|
||||
},
|
||||
"commaList": {"type": "string", "pattern": "^[^,[:space:]]+(,[^,[:space:]]+)*$"},
|
||||
"cidr": {"type": "string", "pattern": "^[0-9A-Fa-f:.]+/[0-9]{1,3}$"},
|
||||
"cidrListOrEmpty": {
|
||||
"type": "string",
|
||||
"pattern": "^(|[0-9A-Fa-f:.]+/[0-9]{1,3}(,[0-9A-Fa-f:.]+/[0-9]{1,3})*)$"
|
||||
},
|
||||
"stringMap": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"}
|
||||
},
|
||||
"labelMapList": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/$defs/stringMap"}
|
||||
},
|
||||
"labelSelector": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"matchLabels": {"$ref": "#/$defs/stringMap"},
|
||||
"matchExpressions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["key", "operator"],
|
||||
"properties": {
|
||||
"key": {"$ref": "#/$defs/nonEmpty"},
|
||||
"operator": {"enum": ["In", "NotIn", "Exists", "DoesNotExist"]},
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"podDnsConfig": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"nameservers": {
|
||||
"type": "array",
|
||||
"maxItems": 3,
|
||||
"items": {"type": "string", "minLength": 1}
|
||||
},
|
||||
"searches": {
|
||||
"type": "array",
|
||||
"maxItems": 32,
|
||||
"items": {"type": "string", "minLength": 1}
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": {"$ref": "#/$defs/nonEmpty"},
|
||||
"value": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["repository", "tag", "digest", "pullPolicy"],
|
||||
"properties": {
|
||||
"repository": {"$ref": "#/$defs/nonEmpty"},
|
||||
"tag": {"$ref": "#/$defs/nonEmpty"},
|
||||
"digest": {"type": "string", "pattern": "^(|sha256:[0-9a-f]{64})$"},
|
||||
"pullPolicy": {"enum": ["Always", "IfNotPresent", "Never"]}
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["requests", "limits"],
|
||||
"properties": {
|
||||
"requests": {"$ref": "#/$defs/resourceMap"},
|
||||
"limits": {"$ref": "#/$defs/resourceMap"}
|
||||
}
|
||||
},
|
||||
"resourceMap": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": ["string", "number"]}
|
||||
},
|
||||
"workload": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["replicas", "image", "resources"],
|
||||
"properties": {
|
||||
"replicas": {"$ref": "#/$defs/positiveInteger"},
|
||||
"image": {"$ref": "#/$defs/image"},
|
||||
"resources": {"$ref": "#/$defs/resources"}
|
||||
}
|
||||
},
|
||||
"webWorkload": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"replicas",
|
||||
"image",
|
||||
"publicOrigin",
|
||||
"apiUrl",
|
||||
"eventsUrl",
|
||||
"oidcClientId",
|
||||
"oidcScope",
|
||||
"cspConnectSrc",
|
||||
"resources"
|
||||
],
|
||||
"properties": {
|
||||
"replicas": {"$ref": "#/$defs/positiveInteger"},
|
||||
"image": {"$ref": "#/$defs/image"},
|
||||
"publicOrigin": {"$ref": "#/$defs/httpUrl"},
|
||||
"apiUrl": {"type": "string", "pattern": "^(https?://|/).+"},
|
||||
"eventsUrl": {"type": "string", "pattern": "^(|https?://.+|wss?://.+|/.+)$"},
|
||||
"oidcClientId": {"$ref": "#/$defs/nonEmpty"},
|
||||
"oidcScope": {"$ref": "#/$defs/nonEmpty"},
|
||||
"cspConnectSrc": {"$ref": "#/$defs/nonEmpty"},
|
||||
"resources": {"$ref": "#/$defs/resources"}
|
||||
}
|
||||
},
|
||||
"secretRef": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "key"],
|
||||
"properties": {
|
||||
"name": {"type": "string", "minLength": 1},
|
||||
"key": {"type": "string", "minLength": 1}
|
||||
"name": {"$ref": "#/$defs/nonEmpty"},
|
||||
"key": {"$ref": "#/$defs/nonEmpty"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ runtime:
|
|||
admissionPolicyVersion: deployment-v1
|
||||
maxAttempts: 3
|
||||
workerLocalAttemptLimit: 4
|
||||
workerRenewSeconds: 10
|
||||
recoveryIntervalSeconds: 2
|
||||
eventAllowedOrigins: https://remora.example.com
|
||||
eventRetentionSeconds: 3600
|
||||
|
|
|
|||
|
|
@ -376,6 +376,17 @@ rollout_remora() {
|
|||
done
|
||||
}
|
||||
|
||||
reject_chart_values() {
|
||||
local reason="$1"
|
||||
shift
|
||||
if helm template remora "$ROOT/deploy/charts/remora" \
|
||||
--namespace "$FIXTURE_NAMESPACE" \
|
||||
"$@" >/dev/null 2>&1; then
|
||||
printf 'chart accepted %s\n' "$reason" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_deployment_assets() {
|
||||
local chart="$ROOT/deploy/charts/remora"
|
||||
helm lint "$chart"
|
||||
|
|
@ -392,12 +403,20 @@ validate_deployment_assets() {
|
|||
grep -q 'absent(remora_ready{component="remora-worker"})' "$RUNTIME/deployment-policy-rendered.yaml"
|
||||
grep -q 'automountServiceAccountToken: false' "$RUNTIME/deployment-policy-rendered.yaml"
|
||||
grep -q 'readOnlyRootFilesystem: true' "$RUNTIME/deployment-policy-rendered.yaml"
|
||||
if helm template remora "$chart" \
|
||||
--namespace "$FIXTURE_NAMESPACE" \
|
||||
--set target.exclusiveWriter=false >/dev/null 2>&1; then
|
||||
printf 'chart accepted an unprotected target namespace\n' >&2
|
||||
return 1
|
||||
fi
|
||||
reject_chart_values "an unprotected target namespace" --set target.exclusiveWriter=false
|
||||
reject_chart_values "an unknown top-level value" --set unexpected=true
|
||||
reject_chart_values "an unknown nested value" --set runtime.unexpected=true
|
||||
reject_chart_values "a zero lease TTL" --set coordination.leaseTtlSeconds=0
|
||||
reject_chart_values "an invalid OIDC issuer" --set-string oidc.issuer=identity.invalid
|
||||
reject_chart_values "an invalid target bucket" --set-string target.bucket=two..dots
|
||||
reject_chart_values "an out-of-range source port" \
|
||||
--set-string outbound.allowedSchemePorts=https:70000
|
||||
reject_chart_values "renewal at lease expiry" \
|
||||
--set runtime.workerRenewSeconds=30
|
||||
reject_chart_values "application-only CIDR reachability" \
|
||||
--set-string outbound.allowedCidrs=10.0.0.0/8
|
||||
reject_chart_values "an impossible disruption budget" \
|
||||
--set podDisruptionBudget.minAvailable=3
|
||||
if grep -R -E \
|
||||
'(BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY|postgres://[^[:space:]]+:[^[:space:]]+@|VAULT_TOKEN[=:][^$])' \
|
||||
"$chart" "$ROOT/docs/spec" >/dev/null; then
|
||||
|
|
|
|||
Loading…
Reference in a new issue