feat(gingress): add Git UA routing and convert gingress to Helm templates
- Route requests with git/JGit User-Agent directly to gitserver backend - Parse gingress.io/git-backend annotation (format: namespace/name:port) - Convert static gingress YAML to Helm templates under deploy/templates/gingress/ - Add gingress config block to values.yaml (namespace, replicas, ports, resources)
This commit is contained in:
parent
670bcc8c06
commit
7148c8fd39
@ -131,7 +131,7 @@ fn process_ingress(ingress: &Ingress, store: &ConfigStore, _ingress_class: &str)
|
||||
|
||||
// Process annotations for advanced features
|
||||
let annotations = ingress.annotations();
|
||||
process_annotations(&annotations, &ingress_prefix, store);
|
||||
process_annotations(&annotations, &ingress_prefix, &namespace, store);
|
||||
|
||||
store.signal_reload();
|
||||
}
|
||||
@ -163,6 +163,7 @@ const ANN_RATE_LIMIT_BURST: &str = "gingress.io/rate-limit-burst";
|
||||
const ANN_REQUEST_HEADERS: &str = "gingress.io/request-headers";
|
||||
const ANN_WEBSOCKET: &str = "gingress.io/websocket";
|
||||
const ANN_SESSION_AFFINITY: &str = "gingress.io/session-affinity";
|
||||
const ANN_GIT_BACKEND: &str = "gingress.io/git-backend";
|
||||
|
||||
/// Parse Ingress annotations and write corresponding ConfigStore entries.
|
||||
///
|
||||
@ -175,6 +176,7 @@ const ANN_SESSION_AFFINITY: &str = "gingress.io/session-affinity";
|
||||
fn process_annotations(
|
||||
annotations: &BTreeMap<String, String>,
|
||||
ingress_prefix: &str,
|
||||
namespace: &str,
|
||||
store: &ConfigStore,
|
||||
) {
|
||||
// Collect hosts from the ingress routes that were just stored
|
||||
@ -256,6 +258,22 @@ fn process_annotations(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Git backend ──
|
||||
// When present, requests with Git User-Agent (git/*, JGit/*) are routed to
|
||||
// this backend instead of normal host+path matching.
|
||||
// Value format: "namespace/service-name:port" or "service-name:port" (namespace from Ingress)
|
||||
if let Some(val) = annotations.get(ANN_GIT_BACKEND) {
|
||||
if let Some(backend) = parse_git_backend(val, &namespace) {
|
||||
store.set("git_backend", &backend);
|
||||
tracing::info!(
|
||||
backend = format!("{}/{}:{}", backend.namespace, backend.name, backend.port),
|
||||
"Git backend configured"
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(annotation = %ANN_GIT_BACKEND, value = %val, "Invalid git-backend format, expected 'namespace/name:port' or 'name:port'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_rate_limit(val: &str, burst_override: Option<&String>) -> (u32, u32) {
|
||||
@ -321,6 +339,34 @@ fn parse_session_affinity(val: &str) -> (bool, String, u64) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse git-backend annotation value.
|
||||
///
|
||||
/// Format: "namespace/name:port" or "name:port" (namespace defaults to Ingress namespace).
|
||||
fn parse_git_backend(val: &str, default_namespace: &str) -> Option<gingress_proxy::config::Backend> {
|
||||
let val = val.trim();
|
||||
// Split off port: "namespace/name:port" → ("namespace/name", "port")
|
||||
let (ns_name, port_str) = val.rsplit_once(':').unwrap_or((val, ""));
|
||||
let port: u16 = port_str.parse().ok()?;
|
||||
|
||||
// Split namespace and name: "namespace/name" → ("namespace", "name")
|
||||
let (namespace, name) = if let Some((ns, n)) = ns_name.rsplit_once('/') {
|
||||
(ns.to_string(), n.to_string())
|
||||
} else {
|
||||
// No namespace specified — use the Ingress namespace
|
||||
(default_namespace.to_string(), ns_name.to_string())
|
||||
};
|
||||
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(gingress_proxy::config::Backend {
|
||||
namespace,
|
||||
name,
|
||||
port,
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove a set of hosts from the global websocket host list (scoped cleanup).
|
||||
fn prune_websocket_hosts(store: &ConfigStore, hosts_to_remove: &[String]) {
|
||||
if let Some(mut existing) = store.get::<Vec<String>>("websocket:hosts") {
|
||||
|
||||
@ -71,6 +71,7 @@ impl Reconciler {
|
||||
let headers = self.collect_headers();
|
||||
let session_affinity = self.collect_session_affinity(&routes);
|
||||
let websocket_hosts = self.collect_websocket_hosts();
|
||||
let git_upstream = self.collect_git_backend();
|
||||
|
||||
// Step 5: Build the complete ProxyConfig
|
||||
let cfg = ProxyConfig {
|
||||
@ -81,6 +82,7 @@ impl Reconciler {
|
||||
headers,
|
||||
session_affinity,
|
||||
websocket_hosts,
|
||||
git_upstream,
|
||||
};
|
||||
|
||||
// Step 6: Validate
|
||||
@ -231,4 +233,9 @@ impl Reconciler {
|
||||
.get::<Vec<String>>("websocket:hosts")
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Collect git backend configuration from annotation.
|
||||
fn collect_git_backend(&self) -> Option<gingress_proxy::config::Backend> {
|
||||
self.store.get("git_backend")
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,11 +2,11 @@ apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gingress-controller
|
||||
namespace: gingress-system
|
||||
namespace: {{ .Values.gingress.namespace | default "gingress-system" }}
|
||||
labels:
|
||||
app: gingress
|
||||
spec:
|
||||
replicas: 2
|
||||
replicas: {{ .Values.gingress.replicaCount | default 2 }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: gingress
|
||||
@ -18,28 +18,30 @@ spec:
|
||||
serviceAccountName: gingress-controller
|
||||
containers:
|
||||
- name: gingress
|
||||
image: gingress:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
image: "{{ .Values.imageRegistry }}/{{ .Values.gingress.repository }}:{{ .Values.imageTag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.gingress.imagePullPolicy | default "IfNotPresent" }}
|
||||
{{- with .Values.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
args:
|
||||
- "--ingress-class=gingress"
|
||||
- "--bind-http=0.0.0.0:80"
|
||||
- "--bind-https=0.0.0.0:443"
|
||||
- "--bind-http=0.0.0.0:{{ .Values.gingress.httpPort | default 80 }}"
|
||||
- "--bind-https=0.0.0.0:{{ .Values.gingress.httpsPort | default 443 }}"
|
||||
- "--metrics-bind=0.0.0.0:8080"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
containerPort: {{ .Values.gingress.httpPort | default 80 }}
|
||||
protocol: TCP
|
||||
- name: https
|
||||
containerPort: 443
|
||||
containerPort: {{ .Values.gingress.httpsPort | default 443 }}
|
||||
protocol: TCP
|
||||
- name: metrics
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: RUST_LOG
|
||||
value: "info"
|
||||
- name: METRICS_PUSH_URL
|
||||
value: "" # Optional: push to metrics aggregator
|
||||
value: {{ .Values.gingress.logLevel | default "info" | quote }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
@ -52,13 +54,10 @@ spec:
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
{{- with .Values.gingress.resources }}
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
@ -67,27 +66,4 @@ spec:
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app: gingress
|
||||
topologyKey: kubernetes.io/hostname
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gingress
|
||||
namespace: gingress-system
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
app: gingress
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 80
|
||||
protocol: TCP
|
||||
- name: https
|
||||
port: 443
|
||||
targetPort: 443
|
||||
protocol: TCP
|
||||
- name: metrics
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
topologyKey: kubernetes.io/hostname
|
||||
@ -1,13 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: gingress-system
|
||||
name: {{ .Values.gingress.namespace | default "gingress-system" }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: gingress-controller
|
||||
namespace: gingress-system
|
||||
namespace: {{ .Values.gingress.namespace | default "gingress-system" }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
@ -38,11 +38,11 @@ roleRef:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: gingress-controller
|
||||
namespace: gingress-system
|
||||
namespace: {{ .Values.gingress.namespace | default "gingress-system" }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: IngressClass
|
||||
metadata:
|
||||
name: gingress
|
||||
spec:
|
||||
controller: gingress.io/gingress-controller
|
||||
controller: gingress.io/gingress-controller
|
||||
20
deploy/templates/gingress/service.yaml
Normal file
20
deploy/templates/gingress/service.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gingress
|
||||
namespace: {{ .Values.gingress.namespace | default "gingress-system" }}
|
||||
labels:
|
||||
app: gingress
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
app: gingress
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.gingress.httpPort | default 80 }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
- name: https
|
||||
port: {{ .Values.gingress.httpsPort | default 443 }}
|
||||
targetPort: https
|
||||
protocol: TCP
|
||||
@ -137,6 +137,22 @@ services:
|
||||
mountPath: /data
|
||||
subPath: static
|
||||
|
||||
# Gingress controller configuration
|
||||
gingress:
|
||||
namespace: "gingress-system"
|
||||
repository: gingress
|
||||
replicaCount: 2
|
||||
httpPort: 80
|
||||
httpsPort: 443
|
||||
logLevel: "info"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
|
||||
# External PVC (managed outside Helm — not deleted on uninstall)
|
||||
pvcName: "shared-data"
|
||||
|
||||
@ -146,6 +162,7 @@ ingress:
|
||||
className: "gingress"
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
gingress.io/git-backend: "deploy-gitserver:8021"
|
||||
hosts:
|
||||
- host: gitdata.ai
|
||||
paths:
|
||||
|
||||
@ -90,6 +90,8 @@ pub struct ProxyConfig {
|
||||
pub session_affinity: HashMap<String, SessionAffinityConfig>,
|
||||
/// WebSocket enabled hosts
|
||||
pub websocket_hosts: Vec<String>,
|
||||
/// Git backend — requests with Git User-Agent are routed here
|
||||
pub git_upstream: Option<Backend>,
|
||||
}
|
||||
|
||||
/// The shared configuration store: read-heavy, write-light.
|
||||
|
||||
@ -41,6 +41,23 @@ impl GIngressProxy {
|
||||
pub fn filter_chain(&self) -> &Arc<std::sync::RwLock<FilterChain>> {
|
||||
&self.filter_chain
|
||||
}
|
||||
|
||||
/// Match a request to a route rule based on host and path.
|
||||
fn match_route(cfg: &crate::config::ProxyConfig, host: &str, path: &str) -> Option<String> {
|
||||
cfg.routes.get(host).and_then(|rules| {
|
||||
rules.iter().find(|r| match r.path_type {
|
||||
crate::config::PathType::Prefix | crate::config::PathType::ImplementationSpecific => {
|
||||
path.starts_with(&r.path)
|
||||
}
|
||||
crate::config::PathType::Exact => path == r.path,
|
||||
})
|
||||
}).map(|r| {
|
||||
format!(
|
||||
"upstream:{}/{}:{}",
|
||||
r.backend.namespace, r.backend.name, r.backend.port
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@ -66,23 +83,29 @@ impl ProxyHttp for GIngressProxy {
|
||||
|
||||
let cfg = self.config.assemble_proxy_config();
|
||||
|
||||
// Match path to a route rule
|
||||
let path = session.req_header().uri.path();
|
||||
let route = cfg.routes.get(&host).and_then(|rules| {
|
||||
rules.iter().find(|r| match r.path_type {
|
||||
crate::config::PathType::Prefix | crate::config::PathType::ImplementationSpecific => {
|
||||
path.starts_with(&r.path)
|
||||
}
|
||||
crate::config::PathType::Exact => path == r.path,
|
||||
})
|
||||
});
|
||||
// Git User-Agent override: requests from git clients (git/2.x, JGit, etc.)
|
||||
// are routed directly to the git backend regardless of host/path matching.
|
||||
let backend_key = if let Some(ref git_backend) = cfg.git_upstream {
|
||||
let ua = session
|
||||
.req_header()
|
||||
.headers
|
||||
.get("user-agent")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
let backend_key = route.map(|r| {
|
||||
format!(
|
||||
"upstream:{}/{}:{}",
|
||||
r.backend.namespace, r.backend.name, r.backend.port
|
||||
)
|
||||
});
|
||||
if ua.starts_with("git/") || ua.starts_with("JGit/") {
|
||||
tracing::debug!(host, ua, "Git UA detected, routing to git backend");
|
||||
Some(format!(
|
||||
"upstream:{}/{}:{}",
|
||||
git_backend.namespace, git_backend.name, git_backend.port
|
||||
))
|
||||
} else {
|
||||
// Normal route matching for non-git requests
|
||||
Self::match_route(&cfg, &host, session.req_header().uri.path())
|
||||
}
|
||||
} else {
|
||||
Self::match_route(&cfg, &host, session.req_header().uri.path())
|
||||
};
|
||||
|
||||
// Select endpoint via load balancer
|
||||
let endpoint = backend_key
|
||||
@ -105,7 +128,7 @@ impl ProxyHttp for GIngressProxy {
|
||||
}
|
||||
None => pingora::Error::e_explain(
|
||||
pingora::ErrorType::InternalError,
|
||||
format!("no upstream found for host '{}' path '{}'", host, path),
|
||||
format!("no upstream found for host '{}' path '{}'", host, session.req_header().uri.path()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user