Compare commits

...
33 Commits
Author SHA1 Message Date
Ara Sadoyan e7b4f9a327 Merge pull request #58 from sergelogvinov/build
add compat versions
2026-09-27 10:50:33 +02:00
Ara Sadoyan 1fab52456f Optimize structured logging. 2026-09-26 18:49:37 +02:00
Serge Logvinov c4732cafd2 feat: add compat verions 2026-09-26 16:04:10 +02:00
Ara Sadoyan d623a2da72 Optimize structured logging. 2026-09-26 15:58:46 +02:00
Ara Sadoyan a145c5e71b file delete 2026-09-25 18:07:03 +02:00
Ara Sadoyan 9de8c9ba97 version change 2026-09-25 18:01:15 +02:00
Ara Sadoyan 502c2ed4c2 Merge pull request #57 from sergelogvinov/build
feat: github actions
2026-09-25 17:54:45 +02:00
Ara Sadoyan a8b3f804f4 Done with logging to ElasticSearch. 2026-09-25 17:52:01 +02:00
Ara Sadoyan 1b733bc879 Intermediate ElasticSearch commit . 2026-09-25 15:26:31 +02:00
Ara Sadoyan 92ed7cebde Intermediate ElasticSearch commit . 2026-09-25 12:55:38 +02:00
Ara Sadoyan 3fe037de5a Making custom Elsticsearch logger 2. 2026-09-24 18:27:53 +02:00
Ara Sadoyan d56968c4c3 Making custom Elsticsearch logger. 2026-09-23 17:57:18 +02:00
Ara Sadoyan 71ebc6376e improving logging 2026-09-23 15:28:41 +02:00
Ara Sadoyan 54ac35a4c9 added inventory for immutable core logging files 2026-09-22 18:48:42 +02:00
Ara Sadoyan 639365c43b restructurization of logging subsystem 2026-09-22 18:25:52 +02:00
Ara Sadoyan 4cd9b459b0 Code restructurization 2026-09-21 15:31:10 +02:00
Ara Sadoyan 4ae92a264b Structural changes, move auth to separate module . 2026-09-20 18:57:45 +02:00
Serge Logvinov efcf83033f feat: github actions 2026-09-19 12:41:38 +02:00
Ara Sadoyan 5fdfa17265 Update to Pingora 0.9, improved logging, Fix cache key bug. 2026-09-18 18:33:35 +02:00
Ara Sadoyan 5efcf07dd0 Updated example main.yaml 2026-09-17 11:35:52 +02:00
Ara Sadoyan 02a329e66b Added log pattern to main.cfg 2026-09-16 19:18:18 +02:00
Ara Sadoyan 444579eefe Minor changes 2026-09-14 10:55:44 +02:00
Ara Sadoyan 0831ffb801 Merge pull request #53 from sergelogvinov/chart
feat: add aralez helm chart
2026-09-09 19:08:06 +02:00
Ara Sadoyan 5d55e2b120 configs 2026-09-09 19:01:38 +02:00
Ara Sadoyan b3c614e592 configs 2026-09-09 18:54:25 +02:00
Ara Sadoyan b22bb2fe16 Config files update, cleanup 2026-09-09 18:52:32 +02:00
Ara Sadoyan e6204a2b1d version change 2026-09-09 18:43:49 +02:00
Ara Sadoyan 6a78acd486 version change 2026-09-09 18:43:28 +02:00
Ara Sadoyan 1a2485b87a Implemented EndpointSlices #55 2026-09-09 18:42:41 +02:00
Ara Sadoyan 3329c34a09 working on #55 2026-09-09 15:39:28 +02:00
Serge 6df63f69cd feat: add aralez helm chart
Add basic Aralez deployment configuration, Helm chart linting workflow,
and Dependabot configuration.
2026-09-06 11:33:16 +02:00
Ara Sadoyan 4a4f3e6187 fixed closed #52 2026-09-04 16:24:49 +02:00
Ara Sadoyan 31f513cc2a hcl examples 2026-09-01 12:53:42 +02:00
85 changed files with 3286 additions and 1039 deletions
+9
View File
@@ -0,0 +1,9 @@
.git/
.github/
.gitignore
.dockerignore
/charts/
#
docker-compose.yml
#
/.cargo/
+9
View File
@@ -0,0 +1,9 @@
helm-extra-args: --timeout 300s
check-version-increment: true
debug: true
chart-dirs:
- charts
validate-maintainers: true
namespace: default
release-label: test
target-branch: main
+23
View File
@@ -0,0 +1,23 @@
---
# See https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
commit-message:
prefix: "chore:"
open-pull-requests-limit: 8
rebase-strategy: disabled
schedule:
interval: "monthly"
day: "monday"
time: "10:00"
timezone: "UTC"
cooldown:
default-days: 7
groups:
actions:
patterns:
- "actions/*"
- "helm/*"
+50
View File
@@ -0,0 +1,50 @@
name: Build check
on:
workflow_dispatch: {}
pull_request:
branches:
- main
paths:
- "src/**"
- "Dockerfile"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build and Test
timeout-minutes: 15
runs-on: ubuntu-24.04
if: github.event.pull_request.draft == false
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Rust toolchain
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: "1.98"
components: clippy, rustfmt
- name: Install cargo-deny
uses: taiki-e/install-action@3f74d7c16a4242f1c95561e98edc25d36adb4375 # v2.87.12
with:
tool: cargo-deny
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Rust build
run: make build test
- name: Rust fmt
run: make fmt
- name: Rust linter
run: make lint
- name: Rust dependencies check
run: make deps
+28
View File
@@ -0,0 +1,28 @@
name: Helm chart check
on:
pull_request:
branches:
- main
paths:
- "charts/**"
jobs:
helm-lint:
name: Helm chart check
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install chart-testing tools
id: lint
uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0
- name: Run helm chart linter
run: ct --config .github/ct.yml lint
- name: Run helm template
run: helm lint charts/aralez
+58
View File
@@ -0,0 +1,58 @@
name: Release
on:
workflow_dispatch: {}
# push:
# tags:
# - "v*"
jobs:
goreleaser:
name: GoReleaser
runs-on: ubuntu-24.04
permissions:
contents: write
packages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Zig
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
- name: Rust toolchain
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: "1.98"
components: clippy, rustfmt
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Set up QEMU
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
with:
platforms: arm64
- name: Set up docker buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Github registry login
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser
timeout-minutes: 60
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
version: "v2.18.2"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
USERNAME: ${{ github.repository_owner }}
+1
View File
@@ -21,6 +21,7 @@ com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
/target
/.zig-cache/
/z_shpo
/configs
Makefile
+126
View File
@@ -0,0 +1,126 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
project_name: aralez
before:
hooks:
- cargo install --locked cargo-zigbuild
dist: target
builds:
- id: aralez
builder: rust
flags:
- --release
targets:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnu
env:
- RUSTFLAGS={{ if eq .Arch "amd64" }}-C target-cpu=x86-64-v3 -C target-feature=+aes,+ssse3{{else}}-C target-cpu=neoverse-n1 -C target-feature=+aes,+sha2,+neon,+crc{{ end }}
- id: aralez-musl
builder: rust
flags:
- --release
targets:
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-musl
env:
- RUSTFLAGS={{ if eq .Arch "amd64" }}-C target-cpu=x86-64-v3 -C target-feature=+aes,+ssse3{{else}}-C target-cpu=neoverse-n1 -C target-feature=+aes,+sha2,+neon,+crc{{ end }}
- id: aralez-compat
builder: rust
flags:
- --release
targets:
- x86_64-unknown-linux-gnu
env:
- RUSTFLAGS=-C target-cpu=x86-64
- id: aralez-compat-musl
builder: rust
flags:
- --release
targets:
- x86_64-unknown-linux-musl
env:
- RUSTFLAGS=-C target-cpu=x86-64
snapshot:
version_template: edge
archives:
- id: aralez
ids:
- aralez
formats: [binary]
name_template: "{{ .ProjectName }}_{{ .Arch }}-glibc"
- id: aralez-musl
ids:
- aralez-musl
formats: [binary]
name_template: "{{ .ProjectName }}_{{ .Arch }}-musl"
- id: aralez-compat
ids:
- aralez-compat
formats: [binary]
name_template: "{{ .ProjectName }}_{{ .Arch }}-compat-glibc"
- id: aralez-compat-musl
ids:
- aralez-compat-musl
formats: [binary]
name_template: "{{ .ProjectName }}_{{ .Arch }}-compat-musl"
dockers_v2:
- id: aralez
ids:
- aralez
flags:
- --target=release
images:
- "ghcr.io/{{ .Env.USERNAME }}/{{ .ProjectName }}"
# - "{{ .Env.USERNAME }}/{{ .ProjectName }}"
tags:
- "{{ if not .IsSnapshot }}v{{.Version}}{{ else }}edge{{ end }}"
platforms:
- linux/amd64
- linux/arm64
annotations:
org.opencontainers.image.name: "{{ .ProjectName }}"
org.opencontainers.image.licenses: "Apache-2.0"
org.opencontainers.image.source: "https://github.com/{{ .Env.USERNAME }}/{{ .ProjectName }}"
org.opencontainers.image.url: "{{ .GitURL }}"
org.opencontainers.image.created: "{{ .Date }}"
org.opencontainers.image.version: "{{ .Version }}"
org.opencontainers.image.revision: "{{ .FullCommit }}"
io.artifacthub.package.readme-url: "https://raw.githubusercontent.com/{{ .Env.USERNAME }}/{{ .ProjectName }}/main/README.md"
io.artifacthub.package.license: "Apache-2.0"
docker_signs:
- id: docker
ids:
- aralez
cmd: cosign
args:
- sign
- "--yes"
- "${artifact}"
output: true
checksum:
name_template: checksums.txt
algorithm: sha256
signs:
- artifacts: checksum
signature: "${artifact}.sigstore.json"
cmd: cosign
args:
- sign-blob
- "--yes"
- "--bundle=${signature}"
- "${artifact}"
output: true
Generated
+752 -342
View File
File diff suppressed because it is too large Load Diff
+25 -18
View File
@@ -1,6 +1,6 @@
[package]
name = "aralez"
version = "0.93.2"
version = "0.94.1"
edition = "2021"
license = "Apache-2.0"
description = "Reverse proxy built on top of Cloudflare's Pingora"
@@ -16,49 +16,56 @@ strip = true
[dependencies]
tokio = { version = "1.53.1", features = ["full"] }
pingora = { version = "0.8.1", features = ["lb", "openssl"] } # openssl, rustls, boringssl
pingora-core = "0.8.1"
pingora-proxy = "0.8.1"
pingora-http = "0.8.1"
pingora-limits = "0.8.1"
pingora-cache = "0.8.1"
pingora = { version = "0.9.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl
pingora-core = "0.9.0"
pingora-proxy = "0.9.0"
pingora-http = "0.9.0"
pingora-limits = "0.9.0"
pingora-cache = "0.9.0"
dashmap = "7.0.0-rc2"
async-trait = "0.1.91"
log = "0.4.33"
async-trait = "0.1.92"
log = "0.4.34"
notify = "9.0.0-rc.4"
axum = { version = "0.8.9" }
serde = { version = "1.0.229", features = ["derive"] }
noyalib = { version = "0.0.18", features = ["compat-serde-yaml"] }
noyalib = { version = "0.0.44", features = ["compat-serde-yaml"] }
rand = "0.10.2"
base64 = "0.23.1"
jsonwebtoken = { version = "11.0.0", default-features = false, features = ["use_pem", "rust_crypto"] }
sha2 = { version = "0.11.0-rc.5", default-features = false }
jsonwebtoken = { version = "11.1.0", default-features = false, features = ["use_pem", "rust_crypto"] }
#sha2 = { version = "0.11.0-rc.5", default-features = false }
sha2 = { version = "0.11.0", default-features = false }
base16ct = { version = "1.0.0", features = ["alloc"] }
urlencoding = "2.1.3"
arc-swap = "1.9.2"
prometheus = "0.14.0"
x509-parser = "0.18.1"
rustls-pemfile = "2.2.0"
tower-http = { version = "0.7.0", features = ["fs"] }
tower-http = { version = "0.7.1", features = ["fs"] }
privdrop = "0.5.6"
serde_json = "1.0.151"
subtle = "2.6.1"
moka = { version = "0.12.15", features = ["sync"] }
moka = { version = "0.12.16", features = ["sync"] }
ahash = "0.8.12"
instant-acme = "0.8.5"
rcgen = "0.14.8"
rcgen = "0.14.10"
log4rs = { version = "1.4.0", features = ["rolling_file_appender", "compound_policy", "size_trigger", "fixed_window_roller", "gzip"] }
mimalloc = { version = "0.1.52", default-features = false }
signal-hook = "0.4.4"
sd-notify = "0.5.0"
libc = "0.2.186"
tokio-native-tls = "0.3.1"
hyper = "1.11.0"
hyper = "1.11.1"
hyper-util = "0.1.20"
http-body-util = "0.1.4"
http-body-util = "0.1.5"
bytes = "1.12.1"
rustls = { version = "0.23", default-features = false, features = ["ring"] }
kube = { version = "4.2.0", features = ["runtime", "client", "derive"] }
k8s-openapi = { version = "0.28.0", features = ["v1_32"] }
futures = "0.3"
futures = "0.3.34"
lazy_static = "1.5.0"
anyhow = "1.0.103"
inventory = "0.3.24"
elasticsearch = "9.1.0-alpha.1"
humantime = "2.4.0"
+49
View File
@@ -0,0 +1,49 @@
# syntax = docker/dockerfile:1.22
########################################
FROM rust:1.98-trixie AS builder
ARG TARGETARCH
RUN apt-get update && \
apt-get install -y build-essential make cmake pkg-config libssl-dev musl-dev musl-tools
RUN case "${TARGETARCH}" in \
amd64) \
apt-get install -y --no-install-recommends gcc-x86-64-linux-gnu && \
rustup target add x86_64-unknown-linux-gnu; \
rustup target add x86_64-unknown-linux-musl; \
;; \
arm64) \
apt-get install -y --no-install-recommends gcc-aarch64-linux-gnu && \
rustup target add aarch64-unknown-linux-gnu; \
rustup target add aarch64-unknown-linux-musl; \
;; \
*) echo "Unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \
esac
RUN rustup component add rustfmt clippy && \
cargo install cargo-deny
RUN ARCH=$(case "${TARGETARCH}" in \
amd64) echo "x86_64" ;; \
arm64) echo "aarch64" ;; \
*) echo "${TARGETARCH}" ;; \
esac) && curl -LO https://ziglang.org/download/0.16.0/zig-${ARCH}-linux-0.16.0.tar.xz \
&& tar -xf zig-${ARCH}-linux-0.16.0.tar.xz -C /opt \
&& ln -s /opt/zig-${ARCH}-linux-0.16.0/zig /usr/local/bin/zig
RUN echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | tee /etc/apt/sources.list.d/goreleaser.list && \
apt-get update && apt-get install -y goreleaser
RUN cargo install cargo-binstall --locked && \
cargo binstall cargo-zigbuild --no-confirm
COPY [".", "/src"]
########################################
FROM --platform=${TARGETARCH} gcr.io/distroless/cc-debian13:nonroot AS release
ARG TARGETPLATFORM
COPY ${TARGETPLATFORM}/aralez /usr/local/bin/aralez
ENTRYPOINT ["/usr/local/bin/aralez"]
+68
View File
@@ -0,0 +1,68 @@
########################################################
USERNAME ?= aralez
TARGET ?= --all-targets
FEATURES ?= --all-features
########################################################
.PHONY: help
help:
@awk 'BEGIN {FS = ":.*?## "} /^[0-9a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
########################################################
.PHONY: all
all: build test fmt checkup ## Build and test project
.PHONY: build
build: ## Build project
cargo build --workspace $(TARGET) $(FEATURES)
.PHONY: test
test: ## Run tests
cargo test --workspace $(TARGET) $(FEATURES)
.PHONY: fmt
fmt: ## Check code formatting
cargo fmt --all -- --check
.PHONY: lint
lint: ## Run linter
cargo clippy --workspace $(TARGET) $(FEATURES) -- -D warnings
.PHONY: checkup
checkup: lint ## Run linter and check project
cargo check --workspace $(TARGET) $(FEATURES)
.PHONY: deps
deps: ## Check project dependencies for advisories
cargo deny check advisories
########################################################
.PHONY: update
update:
cargo update --verbose
.PHONY: features
features:
cargo features
########################################################
.PHONY: fix
fix:
cargo fix
.PHONY: fix-all
fix-all:
cargo fix --all
cargo clippy --workspace $(TARGET) $(FEATURES) --fix
########################################################
.PHONY: docker-build
docker-build: ## Build project in Docker
docker build --target=builder -t builder .
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}:/src -e USERNAME=${USERNAME} -w /src -ti builder:latest
+23
View File
@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
+18
View File
@@ -0,0 +1,18 @@
# yaml-language-server: $schema=https://json.schemastore.org/chart.json
apiVersion: v2
name: aralez
description: Ingress controller for Kubernetes
type: application
home: https://github.com/sadoyan/aralez/charts
sources:
- https://github.com/sadoyan/helm-charts/tree/main/charts/aralez
keywords:
- ingress-controller
- aralez
maintainers: []
#
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.0.1
#
# renovate: datasource=docker depName=ghcr.io/sadoyan/aralez
appVersion: 0.93.3
+17
View File
@@ -0,0 +1,17 @@
{{ template "chart.header" . }}
{{ template "chart.deprecationWarning" . }}
{{ template "chart.badgesSection" . }}
{{ template "chart.description" . }}
{{ template "chart.homepageLine" . }}
{{ template "chart.maintainersSection" . }}
{{ template "chart.sourcesSection" . }}
{{ template "chart.requirementsSection" . }}
{{ template "chart.valuesSection" . }}
+18
View File
@@ -0,0 +1,18 @@
ingressClass: aralez
ingressClassResource:
enabled: true
# hostNetwork: true
# hostPort:
# ports:
# http: 8080
# https: 8443
priorityClassName: system-cluster-critical
env: []
args: []
useDaemonSet: true
terminationGracePeriodSeconds: 120
View File
+104
View File
@@ -0,0 +1,104 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "aralez.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "aralez.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "aralez.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "aralez.labels" -}}
helm.sh/chart: {{ include "aralez.chart" . }}
{{ include "aralez.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- with .Values.podLabels }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "aralez.selectorLabels" -}}
app.kubernetes.io/name: {{ include "aralez.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Return a podAffinity/podAntiAffinity definition
*/}}
{{- define "affinities.pods" -}}
{{- if eq .Values.podAntiAffinityPreset "soft" }}
{{- include "affinities.pods.soft" . -}}
{{- else if eq .Values.podAntiAffinityPreset "hard" }}
{{- include "affinities.pods.hard" . -}}
{{- end -}}
{{- end -}}
{{/*
Return a soft podAntiAffinity definition
*/}}
{{- define "affinities.pods.soft" -}}
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchLabels: {{- (include "aralez.selectorLabels" .) | nindent 12 }}
namespaces:
- {{ .Release.Namespace | quote }}
topologyKey: kubernetes.io/hostname
weight: 1
{{- end -}}
{{/*
Return a hard podAntiAffinity definition
*/}}
{{- define "affinities.pods.hard" -}}
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels: {{- (include "aralez.selectorLabels" .) | nindent 10 }}
namespaces:
- {{ .Release.Namespace | quote }}
topologyKey: kubernetes.io/hostname
{{- end -}}
{{/*
Create the name of the service account to use
*/}}
{{- define "aralez.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "aralez.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
+21
View File
@@ -0,0 +1,21 @@
{{- $ca := genCA .Chart.Name 3650 }}
{{- $server := genSignedCert "server" (list "127.0.0.1") nil 3650 $ca }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "aralez.fullname" . }}-tls
labels:
{{- include "aralez.labels" . | nindent 4 }}
type: kubernetes.io/tls
data:
{{- $secretName := printf "%s-tls" (include "aralez.fullname" .) }}
{{- $secretData := (lookup "v1" "Secret" .Release.Namespace $secretName).data }}
{{- if and $secretData (hasKey $secretData "ca.crt") }}
ca.crt: {{ get $secretData "ca.crt" }}
tls.crt: {{ get $secretData "tls.crt" }}
tls.key: {{ get $secretData "tls.key" }}
{{- else }}
ca.crt: {{ $ca.Cert | b64enc }}
tls.crt: {{ $server.Cert | b64enc }}
tls.key: {{ $server.Key | b64enc }}
{{- end }}
+28
View File
@@ -0,0 +1,28 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
data:
aralez.yaml: |
daemon: false
upstreams_conf: /etc/aralez/upstreams.yaml
proxy_configs: /etc/aralez-proxy
proxy_address_http: "0.0.0.0:{{ .Values.hostPort.ports.http }}"
proxy_address_tls: "0.0.0.0:{{ .Values.hostPort.ports.https }}"
config_api_enabled: false
config_address: "0.0.0.0:3000"
upgrade_sock: /tmp/aralez.sock
pid_file: /tmp/aralez.pid
log_level: debug
hc_method: HEAD
hc_interval: 5
upstreams.yaml: |
provider: "kubernetes"
sticky_sessions: 172000
kubernetes: {}
{{- with .Values.upstreams }}
{{- toYaml . | nindent 4 }}
{{- end }}
+130
View File
@@ -0,0 +1,130 @@
apiVersion: apps/v1
{{- if .Values.useDaemonSet }}
kind: DaemonSet
{{- else }}
kind: Deployment
{{- end }}
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
spec:
{{- if not (or .Values.autoscaling.enabled .Values.useDaemonSet) }}
replicas: {{ .Values.replicaCount }}
{{- end }}
{{- if not .Values.useDaemonSet }}
strategy:
type: {{ .Values.updateStrategy.type }}
{{- else }}
updateStrategy:
type: {{ .Values.updateStrategy.type }}
{{- end }}
selector:
matchLabels:
{{- include "aralez.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "aralez.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
annotations:
{{- if .Values.upstreams }}
checksum/upstreams: {{ toJson .Values.upstreams | sha256sum }}
{{- end }}
spec:
enableServiceLinks: false
{{- if and .Values.hostNetwork .Values.useDaemonSet }}
hostNetwork: {{ .Values.hostNetwork }}
dnsPolicy: "ClusterFirstWithHostNet"
{{- end }}
{{- if .Values.priorityClassName }}
priorityClassName: {{ .Values.priorityClassName }}
{{- end }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "aralez.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- if .Values.terminationGracePeriodSeconds }}
terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["aralez"]
args:
- --conf=/etc/aralez/aralez.yaml
{{- range $value := .Values.args }}
- {{ $value }}
{{- end }}
env:
{{- with .Values.env }}
{{- . | toYaml | nindent 12 }}
{{- end }}
ports:
{{- if .Values.hostPort.ports.http }}
- name: http
containerPort: {{ .Values.hostPort.ports.http }}
protocol: TCP
{{- end }}
{{- if .Values.hostPort.ports.https }}
- name: https
containerPort: {{ .Values.hostPort.ports.https }}
protocol: TCP
{{- end }}
# readinessProbe:
# httpGet:
# path: /status
# port: 3000
# initialDelaySeconds: 30
# timeoutSeconds: 5
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: config
mountPath: /etc/aralez
- name: tlscerts
mountPath: /etc/aralez-proxy/certificates
{{- with .Values.volumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
- name: config
configMap:
name: {{ include "aralez.fullname" . }}
- name: tlscerts
secret:
secretName: {{ include "aralez.fullname" . }}-tls
defaultMode: 256
items:
- key: tls.crt
path: tls.crt
- key: tls.key
path: tls.key
{{- with .Values.volumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
affinity:
{{- include "affinities.pods" . | nindent 8 }}
{{- with .Values.affinity }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
+24
View File
@@ -0,0 +1,24 @@
{{- if and .Values.autoscaling.enabled (not .Values.useDaemonSet) }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "aralez.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- end }}
+10
View File
@@ -0,0 +1,10 @@
{{- if .Values.ingressClassResource.enabled -}}
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
labels:
{{- include "aralez.labels" . | nindent 4 }}
name: {{ .Values.ingressClass }}
spec:
controller: {{ .Values.ingressClassResource.controllerValue }}
{{- end }}
+41
View File
@@ -0,0 +1,41 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if eq .Values.service.type "NodePort" }}
externalTrafficPolicy: Local
{{- end }}
{{- with .Values.service.ipFamilies }}
ipFamilies:
{{- toYaml . | nindent 4 }}
{{- end }}
ipFamilyPolicy: {{ if eq (len .Values.service.ipFamilies) 2 }}PreferDualStack{{ else }}SingleStack{{ end }}
ports:
{{- if .Values.hostPort.ports.http }}
- port: 80
targetPort: http
protocol: TCP
name: http
{{- if .Values.hostPort.enabled }}
nodePort: {{ .Values.hostPort.ports.http }}
{{- end }}
{{- end }}
{{- if .Values.hostPort.ports.https }}
- port: 443
targetPort: https
protocol: TCP
name: https
{{- if .Values.hostPort.enabled }}
nodePort: {{ .Values.hostPort.ports.https }}
{{- end }}
{{- end }}
selector:
{{- include "aralez.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,52 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "aralez.serviceAccountName" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: true
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "ingressclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses/status"]
verbs: ["get", "update", "patch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["nodes", "services", "endpoints"]
verbs: ["get", "list", "watch"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "aralez.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "aralez.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
+164
View File
@@ -0,0 +1,164 @@
# Default values for aralez.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: ghcr.io/sadoyan/aralez
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
# -- Environment variables
env: []
# -- Aralez arguments
args: []
# -- Aralez upstreams file body
# refs: https://aralez.rs/docs/kubernetes/
upstreams:
rate_limit: 500000
x4xx_limit: 100000
to_https: true
server_headers:
- "X-Forwarded-Proto:https"
- "X-Forwarded-Port:443"
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
# -- Annotations for pod.
# ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
podAnnotations: {}
# cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
# -- Extra labels for pod.
# ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
podLabels: {}
# -- Pod Security Context.
# ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod
podSecurityContext:
runAsNonRoot: true
runAsUser: 9999
runAsGroup: 9999
fsGroup: 9999
fsGroupChangePolicy: "OnRootMismatch"
# -- Container Security Context.
# ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod
securityContext:
seccompProfile:
type: RuntimeDefault
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
# -- Pod host network
# Use it only for performance reason
hostNetwork: false
# -- Service node port
# refs: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport
hostPort:
enabled: false
ports:
http: 8080
https: 8443
ingressClass: aralez
ingressClassResource:
enabled: true
controllerValue: aralez.proxy/ingress-controller
# -- Bitwarden service parameters
# ref: https://kubernetes.io/docs/user-guide/services/
service:
# -- service type
type: ClusterIP
annotations: {}
# -- IP families for service
# possible values: IPv4, IPv6
# ref: https://kubernetes.io/docs/concepts/services-networking/dual-stack/
ipFamilies: [IPv4]
# -- Resource requests and limits.
# ref: https://kubernetes.io/docs/user-guide/compute-resources/
resources:
limits:
cpu: 1
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
# -- Horizontal and Vertical pod autoscaler.
# ref: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 80
# -- Use a daemonset instead of a deployment
useDaemonSet: false
# -- The time in seconds that is allowed for a Aralez instance to gracefully shutdown
# ref: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks
terminationGracePeriodSeconds: 120
## updateStrategy for PostgreSQL StatefulSet and its slaves StatefulSets
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
# -- Priority Class Name
# ref: https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass
priorityClassName: ""
# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false
# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
# -- Node labels for pod assignment.
# ref: https://kubernetes.io/docs/user-guide/node-selection/
nodeSelector: {}
# -- Tolerations for pod assignment.
# ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
tolerations: []
# -- Pod Anti Affinity
# soft/hard
podAntiAffinityPreset: soft
# -- Affinity for pod assignment.
# ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity
affinity: {}
@@ -1,9 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: aralez-tls
type: kubernetes.io/tls
data:
tls.crt: <base64-cert>
tls.key: <base64-key>
+8
View File
@@ -0,0 +1,8 @@
# 1. Define the IngressClass so Kubernetes knows "aralez" is a valid controller
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: aralez
namespace: upstreams
spec:
controller: aralez.proxy/ingress-controller
+76
View File
@@ -0,0 +1,76 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: aralez
namespace: aralez
labels:
app.kubernetes.io/name: aralez
app.kubernetes.io/component: ingress-controller
spec:
replicas: 1
selector:
matchLabels:
app: aralez
template:
metadata:
labels:
app: aralez
spec:
serviceAccountName: aralez-sa
containers:
- name: aralez
#image: sadoyan/aralez:latest
image: nginx:latest
imagePullPolicy: IfNotPresent
env:
- name: ARALEZ_SERVICE_NAME
value: "aralez-service"
- name: ARALEZ_SERVICE_NAMESPACE
value: "aralez"
- name: ARALEZ_EXTERNAL_IP # Only for bare metal deployment, remove this in Cloud.
value: "192.168.10.10" # Only for bare metal deployment, remove this in Cloud.
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
readinessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "1"
memory: "512Mi"
volumeMounts:
- name: main-config
mountPath: /etc/aralez/main.yaml
subPath: main.yaml
readOnly: true
- name: upstreams-config
mountPath: /etc/aralez/upstreams.yml
subPath: upstreams.yml
readOnly: true
- name: tls-certs
mountPath: /etc/aralez/certificates
readOnly: true
volumes:
- name: main-config
configMap:
name: aralez-main-config
- name: upstreams-config
configMap:
name: aralez-upstreams-config
- name: tls-certs
secret:
secretName: aralez-tls
@@ -1,43 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: aralez
spec:
replicas: 1
selector:
matchLabels:
app: aralez
template:
metadata:
labels:
app: aralez
spec:
serviceAccountName: aralez-sa
containers:
- name: aralez
image: sadoyan/aralez:latest
ports:
- containerPort: 80
- containerPort: 443
volumeMounts:
- name: main-config
mountPath: /etc/aralez/main.yaml
subPath: main.yaml
readOnly: true
- name: upstreams-config
mountPath: /etc/aralez/upstreams.yml
subPath: upstreams.yml
readOnly: true
- name: tls-certs
mountPath: /etc/aralez/certificates
readOnly: true
volumes:
- name: main-config
configMap:
name: aralez-main-config
- name: upstreams-config
configMap:
name: aralez-upstreams-config
- name: tls-certs
secret:
secretName: aralez-tls
@@ -1,14 +0,0 @@
# The file under watch and hot reload, changes are applied immediately, no need to restart or reload.
provider: "kubernetes" # "file" "consul" "kubernetes"
sticky_sessions: 172000
to_https: false
rate_limit: 500000
x4xx_limit: 100000
server_headers:
- "X-Forwarded-Proto:https"
- "X-Forwarded-Port:443"
client_headers:
- "X-Global-Client:Yooooooo"
kubernetes:
servers:
- "127.0.0.1:6443" # Gets KUBERNETES_SERVICE_HOST : KUBERNETES_SERVICE_PORT_HTTPS env variables.
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: aralez-service
namespace: aralez
spec:
type: NodePort
selector:
app: aralez
ports:
- name: http
port: 80
targetPort: 80
- name: https
port: 443
targetPort: 443
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: aralez-tls
namespace: aralez
type: kubernetes.io/tls
data:
tls1.crt: "<FIRST_BASE64_CRT_HERE>"
tls1.key: "<FIRST_BASE64_KEY_HERE>"
tls2.crt: "<SECOND_BASE64_CRT_HERE>"
tls3.key: "<BASE64_BASE64_KEY_HERE>"
+73
View File
@@ -0,0 +1,73 @@
# Define your Ingress resource targeting Aralez
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-service-v2
namespace: upstreams
annotations:
# Custom annotations parsed by Aralez
aralez.rs/rate_limit: "100"
aralez.rs/x4xx_limit: "20"
aralez.rs/client_headers: '["X-Proxy-From:Aralezzzzzzzzzzz", "X-Hopar-From:Hopaaaaaaaaaaaar"]'
aralez.rs/server_headers: '["X-Proxy-Version:Aralez", "X-Po-From:Kung Fu Panda"]'
spec:
ingressClassName: aralez # Matches the IngressClass metadata.name above
rules:
- host: api-service-v2.blabla.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service-v2
port:
number: 80
---
# Define your Ingress resource targeting Aralez
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wss-service-v2
namespace: upstreams
annotations:
# Custom annotations parsed by Aralez
aralez.rs/rate_limit: "50"
aralez.rs/x4xx_limit: "10"
aralez.rs/client_headers: '["X-Hors-Arev:Naturi", "X-Grand-Master:Oogway"]'
aralez.rs/server_headers: '["X-Mors-Arev:Hastat", "X-Shifu-Master:Shifu"]'
spec:
ingressClassName: aralez # Matches the IngressClass metadata.name above
rules:
- host: wss-service-v2.blabla.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: svc-nginx
port:
number: 80
---
# Define your Ingress resource targeting Aralez
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: joj-service-v2
namespace: upstreams
spec:
ingressClassName: aralez # Matches the IngressClass metadata.name above
rules:
- host: joj-service-v2.blabla.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: svc-nginx
port:
number: 80
@@ -2,6 +2,7 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: svc-nginx
namespace: upstreams
labels:
app: svc-nginx
spec:
-24
View File
@@ -1,24 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wss-service-v2
namespace: default
annotations:
# These custom annotations are parsed by Aralez
aralez.rs/rate_limit: "50"
aralez.rs/x4xx_limit: "10"
aralez.rs/client_headers: '["X-Some-Client:Some Custom Header", "X-Example-Client:An Example Header"]'
aralez.rs/server_headers: '["X-Some-Server:Some Custom Header", "X-Example-Server:An Example Header"]'
spec:
ingressClassName: aralez # Matches the IngressClass metadata
rules:
- host: wss-service-v2.blabla.com
http:
paths:
- path: /ws
pathType: Prefix
backend:
service:
name: svc-nginx
port:
number: 80
@@ -2,6 +2,7 @@ apiVersion: v1
kind: Service
metadata:
name: svc-nginx
namespace: upstreams
spec:
selector:
app: svc-nginx
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: api-service-v2
namespace: upstreams
spec:
selector:
app: svc-nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
+40
View File
@@ -0,0 +1,40 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: aralez-sa
namespace: aralez
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: aralez-role
rules:
# Watch Ingresses and IngressClasses across all namespaces
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "ingressclasses"]
verbs: ["get", "list", "watch"]
# Update Ingress status (for setting load balancer IPs)
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses/status"]
verbs: ["update", "patch"]
# Watch Services and Endpoints across all namespaces
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get", "list", "watch"]
# Watch EndpointSlices across all namespaces
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: aralez-ingress
subjects:
- kind: ServiceAccount
name: aralez-sa
namespace: aralez # <--- MUST match where Aralez is deployed
roleRef:
kind: ClusterRole
name: aralez-role
apiGroup: rbac.authorization.k8s.io
+16
View File
@@ -0,0 +1,16 @@
provider: "kubernetes"
to_https: false
sticky_sessions: 86000
rate_limit: 100
x4xx_limit: 100
headers:
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "Strict-Transport-Security:max-age=31536000; includeSubDomains; preload"
kubernetes:
services:
- hostname: "svc-nginx"
upstream: "svc-nginx"
path: "/"
tokenpath: "/var/run/secrets/kubernetes.io/serviceaccount/token"
+1
View File
@@ -16,6 +16,7 @@ file_server_folder: /opt/aralez/public # Optional, local folder to serve
file_server_address: 0.0.0.0:3002 # Optional, Local address for file server. Can set as upstream for public access.
log_level: info # info, warn, error, debug, trace, off
log_file: /tmp/aralez.log, 20, 5, compress # Optional, the location of log file, file size in megabytes, number of log files to keep, compress files .valueas are separated by comma.
log_pattern: "{d(%Y-%m-%d %H:%M:%S)} {h({l})} {t} - {m} {n}" # Optional log pattern, defaults to "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}"
access_log: error # all, error, (Off if commented)
hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE
hc_interval: 2 #Interval for health checks in seconds
+16
View File
@@ -0,0 +1,16 @@
use crate::auth::types::AuthValidator;
use pingora_proxy::Session;
use subtle::ConstantTimeEq;
pub struct ApiKeyAuth<'a>(pub(crate) &'a str);
#[async_trait::async_trait]
impl AuthValidator for ApiKeyAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(header) = session.get_header("x-api-key") {
if let Ok(h) = header.to_str() {
return h.as_bytes().ct_eq(self.0.as_bytes()).into();
}
}
false
}
}
+24
View File
@@ -0,0 +1,24 @@
use crate::auth::types::AuthValidator;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use pingora_proxy::Session;
use subtle::ConstantTimeEq;
pub struct BasicAuth<'a>(pub(crate) &'a str);
#[async_trait::async_trait]
impl AuthValidator for BasicAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(header) = session.get_header("authorization") {
if let Ok(h) = header.to_str() {
if let Some((_, val)) = h.split_once(' ') {
if let Ok(decoded) = STANDARD.decode(val) {
if decoded.as_slice().ct_eq(self.0.as_bytes()).into() {
return true;
}
}
}
}
}
false
}
}
+6 -121
View File
@@ -1,30 +1,14 @@
use crate::utils::jwt::{check_jwt, JWT_TOKEN};
use crate::utils::structs::InnerAuth;
pub(crate) use crate::utils::tools::split_host_port;
use crate::auth::types::AuthValidator;
use crate::utils::tools::split_host_port;
use axum::http::StatusCode;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use pingora::http::RequestHeader;
use pingora_core::connectors::http::Connector;
use pingora_core::upstreams::peer::HttpPeer;
use pingora_http::ResponseHeader;
use pingora_core::prelude::HttpPeer;
use pingora_http::{RequestHeader, ResponseHeader};
use pingora_proxy::Session;
use std::collections::HashMap;
use std::sync::LazyLock;
use subtle::ConstantTimeEq;
use urlencoding::decode;
#[async_trait::async_trait]
trait AuthValidator {
async fn validate(&self, session: &mut Session) -> bool;
}
struct BasicAuth<'a>(&'a str);
struct ApiKeyAuth<'a>(&'a str);
struct JwtAuth();
struct ForwardAuth<'a>(&'a str);
pub static AUTH_CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
static AUTH_CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
pub struct ForwardAuth<'a>(pub(crate) &'a str);
#[async_trait::async_trait]
impl AuthValidator for ForwardAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
@@ -145,102 +129,3 @@ impl AuthValidator for ForwardAuth<'_> {
}
}
}
#[async_trait::async_trait]
impl AuthValidator for BasicAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(header) = session.get_header("authorization") {
if let Ok(h) = header.to_str() {
if let Some((_, val)) = h.split_once(' ') {
if let Ok(decoded) = STANDARD.decode(val) {
if decoded.as_slice().ct_eq(self.0.as_bytes()).into() {
return true;
}
}
}
}
}
false
}
}
#[async_trait::async_trait]
impl AuthValidator for ApiKeyAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(header) = session.get_header("x-api-key") {
if let Ok(h) = header.to_str() {
return h.as_bytes().ct_eq(self.0.as_bytes()).into();
}
}
false
}
}
#[async_trait::async_trait]
impl AuthValidator for JwtAuth {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(jwtsecret) = JWT_TOKEN.clone() {
if let Some(tok) = get_query_param(session, "araleztoken") {
return check_jwt(tok.as_str(), jwtsecret.as_ref());
}
if let Some(auth_header) = session.get_header("authorization") {
if let Ok(header_str) = auth_header.to_str() {
if let Some((scheme, token)) = header_str.split_once(' ') {
if scheme.eq_ignore_ascii_case("bearer") {
return check_jwt(token, jwtsecret.as_ref());
}
}
}
}
}
false
}
}
pub async fn authenticate(auth: &InnerAuth, session: &mut Session) -> bool {
match &*auth.auth_type {
"basic" => BasicAuth(&*auth.auth_cred).validate(session).await,
"apikey" => ApiKeyAuth(&*auth.auth_cred).validate(session).await,
"jwt" => JwtAuth().validate(session).await,
"forward" => ForwardAuth(&*auth.auth_cred).validate(session).await,
_ => {
log::warn!("Unsupported authentication mechanism : {}", &*auth.auth_type);
false
}
}
}
pub fn get_query_param(session: &mut Session, key: &str) -> Option<String> {
let query = session.req_header().uri.query()?;
let params: HashMap<_, _> = query
.split('&')
.filter_map(|pair| {
let mut parts = pair.splitn(2, '=');
let k = parts.next()?;
let v = parts.next().unwrap_or(""); // Some params might have no value
Some((k, v))
})
.collect();
params.get(key).and_then(|v| decode(v).ok()).map(|s| s.to_string())
}
// #[allow(clippy::needless_return)]
// pub fn split_host_port(addr: &str, tls: bool) -> Option<(&str, u16, bool, &str)> {
// match addr.split_once(':') {
// Some((h, p)) => match p.parse::<u16>() {
// Ok(port) => return Some((h, port, tls, h)),
// Err(_) => {
// log::warn!("ForwardAuth: invalid port in {}", addr);
// return None;
// }
// },
// None => {
// if tls {
// return Some((addr, 443u16, tls, addr));
// } else {
// return Some((addr, 80u16, tls, addr));
// }
// }
// };
// }
+44 -3
View File
@@ -1,13 +1,18 @@
use crate::auth::types::AuthValidator;
use ahash::AHasher;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use jsonwebtoken::{Algorithm, DecodingKey, Validation};
use moka::sync::Cache;
use moka::Expiry;
use pingora_proxy::Session;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant, SystemTime};
use urlencoding::decode;
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
@@ -41,6 +46,42 @@ impl Expiry<u64, u64> for JwtExpiry {
}
}
}
pub struct JwtAuth();
#[async_trait::async_trait]
impl AuthValidator for JwtAuth {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(jwtsecret) = JWT_TOKEN.clone() {
if let Some(tok) = get_query_param(session, "araleztoken") {
return check_jwt(tok.as_str(), jwtsecret.as_ref());
}
if let Some(auth_header) = session.get_header("authorization") {
if let Ok(header_str) = auth_header.to_str() {
if let Some((scheme, token)) = header_str.split_once(' ') {
if scheme.eq_ignore_ascii_case("bearer") {
return check_jwt(token, jwtsecret.as_ref());
}
}
}
}
}
false
}
}
fn get_query_param(session: &mut Session, key: &str) -> Option<String> {
let query = session.req_header().uri.query()?;
let params: HashMap<_, _> = query
.split('&')
.filter_map(|pair| {
let mut parts = pair.splitn(2, '=');
let k = parts.next()?;
let v = parts.next().unwrap_or(""); // Some params might have no value
Some((k, v))
})
.collect();
params.get(key).and_then(|v| decode(v).ok()).map(|s| s.to_string())
}
pub fn check_jwt(token: &str, secret: &str) -> bool {
let key = hash_token(token, secret);
@@ -57,7 +98,7 @@ pub fn check_jwt(token: &str, secret: &str) -> bool {
Err(_) => return false,
}
match decode::<Claims>(token, &DecodingKey::from_secret(secret.as_ref()), &JWT_VALIDATION) {
match jsonwebtoken::decode::<Claims>(token, &DecodingKey::from_secret(secret.as_ref()), &JWT_VALIDATION) {
Ok(data) => {
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
if data.claims.exp > now {
+7
View File
@@ -0,0 +1,7 @@
pub mod apikey;
pub mod basic;
pub mod forward;
pub mod jwt;
pub mod types;
pub use types::*;
+23
View File
@@ -0,0 +1,23 @@
use crate::auth::apikey::ApiKeyAuth;
use crate::auth::basic::BasicAuth;
use crate::auth::forward::ForwardAuth;
use crate::auth::jwt::JwtAuth;
use crate::utils::types::InnerAuth;
use pingora_proxy::Session;
#[async_trait::async_trait]
pub trait AuthValidator {
async fn validate(&self, session: &mut Session) -> bool;
}
pub async fn authenticate(auth: &InnerAuth, session: &mut Session) -> bool {
match &*auth.auth_type {
"basic" => BasicAuth(&*auth.auth_cred).validate(session).await,
"apikey" => ApiKeyAuth(&*auth.auth_cred).validate(session).await,
"jwt" => JwtAuth().validate(session).await,
"forward" => ForwardAuth(&*auth.auth_cred).validate(session).await,
_ => {
log::warn!("Unsupported authentication mechanism : {}", &*auth.auth_type);
false
}
}
}
+20 -11
View File
@@ -1,22 +1,28 @@
use crate::core::proxyhttp::LB;
use crate::ingress::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider, KubernetesProvider};
use crate::logging::core::{init_access_log, init_access_logging, log_builder};
use crate::tls::acme::order::refresh_order;
use crate::utils::discovery::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider, KubernetesProvider};
use crate::utils::metrics::calc_cache_metrics;
use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::Configuration;
use crate::utils::tools::*;
use crate::utils::types::Configuration;
use crate::utils::*;
use crate::web::logging::init_logging;
use crate::web::proxyhttp::LB;
use async_trait::async_trait;
use dashmap::DashMap;
use log::{error, info};
use pingora_core::server::ShutdownWatch;
use pingora_core::services::background::BackgroundService;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::mpsc;
#[async_trait]
impl BackgroundService for LB {
async fn start(&self, mut shutdown: ShutdownWatch) {
log_builder(&self.config, &self.config.log_file).await;
let al = &self.config.access_log.clone().unwrap_or("none".to_string());
let _ = init_access_log(al.as_str()).await;
info!("Starting background service"); // tx: Sender<Configuration>
let (tx, mut rx) = mpsc::channel::<Configuration>(1);
let tx_api = tx.clone();
@@ -67,23 +73,23 @@ impl BackgroundService for LB {
current_upstreams: self.ump_upst.clone(),
full_upstreams: self.ump_full.clone(),
};
drop(tokio::spawn(async move { api_load.start(tx_api).await }));
tokio::spawn(async move { api_load.start(tx_api).await });
let uu = self.ump_upst.clone();
let ff = self.ump_full.clone();
let im = self.ump_byid.clone();
let (hc_method, hc_interval) = (self.config.hc_method.clone(), self.config.hc_interval);
drop(tokio::spawn(async move {
healthcheck::hc2(uu, ff, im, (&*hc_method.to_string(), hc_interval.to_string().parse().unwrap())).await
}));
drop(tokio::spawn(async move { refresh_order(certdir, confdir).await }));
init_logging(self.config.access_log.clone());
tokio::spawn(async move { healthcheck::hc2(uu, ff, im, (&*hc_method.to_string(), hc_interval.to_string().parse().unwrap())).await });
tokio::spawn(async move { calc_cache_metrics().await });
tokio::spawn(async move { refresh_order(certdir, confdir).await });
init_access_logging(self.config.access_log.clone()).await;
loop {
tokio::select! {
_ = shutdown.changed() => {
break;
}
val = rx.recv() => {
let start = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
if let Some(ss) = val {
clone_dashmap_into(&ss.upstreams, &self.ump_full);
clone_dashmap_into(&ss.upstreams, &self.ump_upst);
@@ -130,6 +136,9 @@ impl BackgroundService for LB {
}
}
}
let end = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let startup = end - start;
info!("Loading upstreams took: {}ms", startup.as_millis());
}
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
use crate::utils::structs::InnerMap;
use crate::web::proxyhttp::LB;
use crate::core::proxyhttp::LB;
use crate::utils::types::InnerMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
-2
View File
@@ -1,7 +1,5 @@
pub mod acme;
pub mod bgservice;
pub mod gethosts;
pub mod logging;
pub mod proxyhttp;
pub mod start;
pub mod webserver;
+23 -14
View File
@@ -1,9 +1,9 @@
use crate::utils::auth::authenticate;
use crate::auth::authenticate;
use crate::core::gethosts::{GetHost, GetHostsReturHeaders};
use crate::logging::core::access_log;
use crate::utils::lazylock::{CACHE_LOCK, CACHE_TTL, EVICTION, LOCALHOST, MEM_CACHE, RATE_LIMITER, REQUESTS_4XX, REVERSE_STORE};
use crate::utils::metrics::*;
use crate::utils::structs::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use crate::web::gethosts::{GetHost, GetHostsReturHeaders};
use crate::web::logging::access_log;
use crate::utils::types::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use arc_swap::ArcSwap;
use async_trait::async_trait;
use axum::body::Bytes;
@@ -46,6 +46,9 @@ pub struct Context {
x4xx_limit: Option<u32>,
}
thread_local! {
static KEY_BUF: RefCell<String> = RefCell::new(String::with_capacity(256));
}
#[async_trait]
impl ProxyHttp for LB {
type CTX = Context;
@@ -61,9 +64,22 @@ impl ProxyHttp for LB {
}
}
fn cache_key_callback(&self, session: &Session, _ctx: &mut Self::CTX) -> Result<CacheKey> {
Ok(CacheKey::new(_ctx.hostname.clone().unwrap_or_default().as_ref(), session.req_header().uri.to_string(), ""))
fn cache_key_callback(&self, session: &Session, ctx: &mut Self::CTX) -> Result<CacheKey> {
let host = ctx.hostname.as_deref().unwrap_or("default");
let path = session.req_header().uri.path();
let primary_key = KEY_BUF.with(|buf| {
let mut b = buf.borrow_mut();
b.clear();
b.push_str(host);
b.push_str(path);
b.clone()
});
let secondary_key = session.req_header().uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/");
Ok(CacheKey::new(primary_key, secondary_key))
}
fn request_cache_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<()> {
if self.cache_enabled && session.req_header().method == Method::GET {
if let Some(eviction) = EVICTION.get() {
@@ -339,14 +355,7 @@ impl ProxyHttp for LB {
}
}
}
// match session.cache.phase() {
// CachePhase::Hit => info!("Cache Status: HIT (Served from Memory)"),
// CachePhase::Miss => info!("Cache Status: MISS (Fetched from Upstream)"),
// CachePhase::Expired => info!("Cache Status: EXPIRED (Revalidating)"),
// _ => {}
// }
access_log(response_code, &self.request_summary(session, ctx), session);
access_log(response_code, &self.request_summary(session, ctx), session).await;
}
}
+8 -8
View File
@@ -1,12 +1,11 @@
// use rustls::crypto::ring::default_provider;
use crate::core::proxyhttp::LB;
use crate::tls::grades;
use crate::tls::load;
use crate::tls::load::CertificateConfig;
use crate::utils::folder_watch;
use crate::utils::lazylock::{CACHE_TTL, EVICTION};
use crate::utils::structs::Extraparams;
use crate::utils::tools::*;
use crate::web::logging::init_access_log;
use crate::web::proxyhttp::LB;
use crate::utils::types::Extraparams;
use arc_swap::ArcSwap;
use dashmap::DashMap;
use log::info;
@@ -18,6 +17,7 @@ use pingora_core::prelude::{background_service, Opt};
use pingora_core::protocols::TcpKeepalive;
use pingora_core::server::Server;
use privdrop::reexports::libc::SIGQUIT;
use rustls::crypto::ring::default_provider;
use sd_notify::NotifyState;
use signal_hook::{
consts::{SIGINT, SIGTERM},
@@ -29,7 +29,7 @@ use std::time::Duration;
use std::{fs, thread};
pub fn run() {
// default_provider().install_default().expect("Failed to install rustls crypto provider");
default_provider().install_default().expect("Failed to install RusTLS crypto provider");
let parameters = Opt::parse_args();
let file = parameters.conf.clone().unwrap();
let maincfg = crate::utils::parceyaml::parce_main_config(file.as_str());
@@ -70,8 +70,8 @@ pub fn run() {
extraparams: ec_config,
cache_enabled: cache_enabled,
};
let al = cfg.access_log.clone().unwrap_or("none".to_string());
init_access_log(al.as_str());
// let al = cfg.access_log.clone().unwrap_or("none".to_string());
// init_access_log(al.as_str());
let grade = cfg.proxy_tls_grade.clone().unwrap_or("medium".to_string());
info!("TLS grade set to: [ {} ]", grade);
@@ -114,7 +114,7 @@ pub fn run() {
fs::create_dir_all(certs_path.clone()).unwrap();
}
thread::spawn(move || {
watch_folder(certs_path, tx).unwrap();
folder_watch(certs_path, tx).unwrap();
});
let certificate_configs = rx.recv().unwrap();
let first_set = load::Certificates::new(&certificate_configs, grade.as_str()).unwrap_or_else(|| panic!("Unable to load initial certificate info"));
+9 -13
View File
@@ -1,9 +1,9 @@
use crate::utils::discovery::APIUpstreamProvider;
use crate::utils::jwt::Claims;
use crate::auth::jwt::Claims;
use crate::ingress::APIUpstreamProvider;
use crate::tls::acme::http01::{acme_create, acme_order, http01_challenge};
use crate::utils::metrics::{get_memory_usage, get_open_files, MEMORY_USAGE, OPEN_FILES};
use crate::utils::structs::{Config, Configuration, UpstreamsDashMap};
use crate::utils::tools::{upstreams_liveness_json, upstreams_to_json};
use crate::web::acme::{acme_create, acme_order, http01_challenge};
use crate::utils::types::{Config, Configuration, UpstreamsDashMap};
use axum::body::Body;
use axum::extract::{Query, State};
use axum::http::{Response, StatusCode};
@@ -30,10 +30,10 @@ struct OutToken {
}
#[derive(Clone)]
pub(crate) struct AppState {
pub struct AppState {
master_key: Option<String>,
pub(crate) cert_creds: String,
pub(crate) certs_dir: String,
pub cert_creds: String,
pub certs_dir: String,
upstreams_file: String,
config_sender: mpsc::Sender<Configuration>,
config_api_enabled: bool,
@@ -78,10 +78,6 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: mpsc::Sende
Err(e) => error!("File server exited: {}", e),
}
}))
// let a = config.file_server_address.to_owned();
// let f = config.file_server_folder.to_owned();
// drop(tokio::spawn(async move { run_file_server(a, f).await }));
}
let listener = port_is_available("Config API", &config.address).await;
@@ -114,9 +110,9 @@ async fn conf(State(st): State<AppState>, Query(params): Query<HashMap<String, S
match parsed {
Ok(_) => {
if let Some(_) = params.get("save") {
drop(tokio::spawn(async move { apply_config(content.as_str(), st, true).await }));
tokio::spawn(async move { apply_config(content.as_str(), st, true).await });
} else {
drop(tokio::spawn(async move { apply_config(content.as_str(), st, false).await }));
tokio::spawn(async move { apply_config(content.as_str(), st, false).await });
}
Response::builder().status(StatusCode::OK).body(Body::from("Accepted! Applying in background\n")).unwrap()
}
@@ -1,7 +1,8 @@
use crate::ingress::kuberconsul;
use crate::ingress::kuberconsul::{list_to_upstreams, ServiceDiscovery};
use crate::utils::httpclient;
use crate::utils::kuberconsul::{list_to_upstreams, ServiceDiscovery};
use crate::utils::parceyaml::build_headers;
use crate::utils::structs::{Configuration, GlobalServiceMapping, UpstreamsDashMap};
use crate::utils::types::{Configuration, GlobalServiceMapping, UpstreamsDashMap};
use async_trait::async_trait;
use dashmap::DashMap;
use pingora::prelude::sleep;
@@ -81,7 +82,7 @@ impl ServiceDiscovery for ConsulDiscovery {
}
}
if let Some(lt) = crate::utils::kuberconsul::clone_compare(&upstreams, &config).await {
if let Some(lt) = kuberconsul::clone_compare(&upstreams, &config).await {
let _ = toreturn.send(lt).await;
}
@@ -1,9 +1,9 @@
use crate::utils::consul::ConsulDiscovery;
use crate::utils::filewatch;
use crate::utils::kuberconsul::ServiceDiscovery;
use crate::utils::kubernetes::KubernetesDiscovery;
use crate::utils::structs::{Configuration, UpstreamsDashMap};
use crate::web::webserver;
use crate::core::webserver;
use crate::ingress::consul::ConsulDiscovery;
use crate::ingress::kuberconsul::ServiceDiscovery;
use crate::ingress::kubernetes::KubernetesDiscovery;
use crate::utils::types::{Configuration, UpstreamsDashMap};
use crate::utils::watch;
use async_trait::async_trait;
use std::sync::Arc;
use tokio::sync::mpsc::Sender;
@@ -48,7 +48,7 @@ impl Discovery for APIUpstreamProvider {
#[async_trait]
impl Discovery for FromFileProvider {
async fn start(&self, tx: Sender<Configuration>) {
tokio::spawn(filewatch::start(self.path.clone(), tx));
tokio::spawn(watch::file_watch(self.path.clone(), tx));
}
}
@@ -1,5 +1,5 @@
use crate::utils::structs::{Configuration, GlobalServiceMapping, InnerMap, UpstreamsDashMap};
use crate::utils::tools::{clone_dashmap_into, compare_dashmaps, print_upstreams};
use crate::utils::types::{Configuration, GlobalServiceMapping, InnerMap, UpstreamsDashMap};
use async_trait::async_trait;
use dashmap::DashMap;
use std::sync::atomic::AtomicUsize;
@@ -1,41 +1,55 @@
use crate::utils::httpclient;
use crate::utils::kubewatcher::{start_ingress_watcher_with_config, AralezRouteUpdate};
use crate::utils::parceyaml::build_headers;
use crate::utils::structs::{Configuration, GlobalServiceMapping, UpstreamsDashMap};
use crate::utils::types::{Configuration, GlobalServiceMapping, UpstreamsDashMap};
use async_trait::async_trait;
use dashmap::DashMap;
use log::error;
use rand::RngExt;
use std::env;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use crate::utils::kuberconsul::{clone_compare, list_to_upstreams, ServiceDiscovery};
use crate::ingress::kuberconsul;
use crate::ingress::kuberconsul::{clone_compare, list_to_upstreams, ServiceDiscovery};
use crate::ingress::kubewatcher::{start_ingress_watcher_with_config, AralezRouteUpdate};
use rustls::crypto::ring::default_provider;
use rustls::crypto::CryptoProvider;
use tokio::sync::mpsc::Sender;
#[derive(Debug, serde::Deserialize)]
pub struct KubeEndpoints {
pub subsets: Option<Vec<KubeSubset>>,
#[serde(rename_all = "camelCase")]
pub struct KubeEndpointSliceList {
#[serde(default)]
pub items: Vec<KubeEndpointSlice>,
}
#[derive(Debug, serde::Deserialize)]
pub struct KubeSubset {
pub addresses: Option<Vec<KubeAddress>>,
#[serde(rename_all = "camelCase")]
pub struct KubeEndpointSlice {
pub ports: Option<Vec<KubePort>>,
#[serde(default)]
pub endpoints: Vec<KubeEndpoint>,
}
#[derive(Debug, serde::Deserialize)]
pub struct KubeAddress {
pub ip: String,
#[serde(rename_all = "camelCase")]
pub struct KubeEndpoint {
#[serde(default)]
pub addresses: Vec<String>,
pub conditions: Option<KubeConditions>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubeConditions {
pub ready: Option<bool>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubePort {
pub port: u16,
pub port: Option<u16>,
}
pub struct KubernetesDiscovery;
#[async_trait]
@@ -54,8 +68,7 @@ impl ServiceDiscovery for KubernetesDiscovery {
let num = if end > 0 { rand::rng().random_range(0..end) } else { 0 };
let server = servers.get(num).unwrap().to_string();
let path = kuber.tokenpath.unwrap_or_else(|| "/var/run/secrets/kubernetes.io/serviceaccount/token".to_string());
let namespace = get_current_namespace().unwrap_or_else(|| "default".to_string());
let token = crate::utils::kuberconsul::read_token(path.as_str()).await;
let token = kuberconsul::read_token(path.as_str()).await;
if CryptoProvider::get_default().is_none() {
default_provider().install_default().expect("Failed to install rustls crypto provider");
@@ -86,7 +99,6 @@ impl ServiceDiscovery for KubernetesDiscovery {
to_https: None,
redirect_to: None,
authorization: None,
// sticky_sessions: update.sticky_sessions,
rate_limit: update.rate_limit,
x4xx_limit: update.x4xx_limit,
client_headers: None,
@@ -129,7 +141,12 @@ impl ServiceDiscovery for KubernetesDiscovery {
sheader_list.insert(Arc::from(path_key), server_headers_list);
config.server_headers.insert(host_key.clone(), sheader_list);
}
let url = format!("https://{}/api/v1/namespaces/{}/endpoints/{}", server, namespace, update.service_name);
let url = format!(
"https://{}/apis/discovery.k8s.io/v1/namespaces/{}/endpointslices?labelSelector=kubernetes.io%2Fservice-name%3D{}",
server, update.namespace, update.service_name
);
let list = httpclient::for_kuber(&url, &token, &service).await;
if list.is_none() {
@@ -145,13 +162,3 @@ impl ServiceDiscovery for KubernetesDiscovery {
}
}
}
fn get_current_namespace() -> Option<String> {
let ns_path = "/var/run/secrets/kubernetes.io/serviceaccount/namespace";
if Path::new(ns_path).exists() {
if let Ok(contents) = fs::read_to_string(ns_path) {
return Some(contents.trim().to_string());
}
}
env::var("POD_NAMESPACE").ok()
}
@@ -1,3 +1,4 @@
use dashmap::DashMap;
use futures::StreamExt;
use hyper::Uri;
use k8s_openapi::api::core::v1::{Node, Service};
@@ -11,17 +12,18 @@ use kube::{
};
use log::{error, info};
use serde_json::json;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
pub struct AralezRouteUpdate {
pub host: String,
pub path: String,
pub namespace: String,
pub service_name: String,
pub is_deleted: bool,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
pub sticky_sessions: Option<u64>,
pub client_headers: Option<Vec<String>>,
pub server_headers: Option<Vec<String>>,
}
@@ -30,11 +32,14 @@ pub struct AralezRouteUpdate {
struct AralezAnnotation {
rate_limit: Option<isize>,
x4xx_limit: Option<u32>,
sticky_sessions: Option<u64>,
pub client_headers: Option<Vec<String>>,
pub server_headers: Option<Vec<String>>,
namespace: String,
client_headers: Option<Vec<String>>,
server_headers: Option<Vec<String>>,
}
lazy_static::lazy_static! {
static ref PATCHED_INGRESSES: Arc<DashMap<String, String>> = Arc::new(DashMap::new());
}
pub async fn start_ingress_watcher_with_config(
api_server: &str,
token: &str,
@@ -82,53 +87,59 @@ async fn process_ingress_change(client: &Client, ing: &Ingress, target_class: &s
if !is_deleted {
if let (Some(name), Some(ns)) = (ing.metadata.name.as_deref(), ing.metadata.namespace.as_deref()) {
let svc_name = std::env::var("ARALEZ_SERVICE_NAME").unwrap_or_else(|_| "aralez-service".to_string());
let external_ip = match discover_controller_ip(&client, ns, &svc_name).await {
Some(ip) => ip,
None => discover_node_ip(&client)
.await
.unwrap_or_else(|| std::env::var("ARALEZ_EXTERNAL_IP").unwrap_or_else(|_| "127.0.0.1".to_string())),
};
let client_clone = client.clone();
let name_clone = name.to_string();
let ns_clone = ns.to_string();
let status = ing.status.clone();
tokio::spawn(async move {
patch_ingress_status_address(&client_clone, &ns_clone, &name_clone, status.as_ref(), &external_ip).await;
});
let key = format!("{}/{}", ns, name);
let external_ip = resolve_external_ip(client).await;
let status_already_correct = ing
.status
.as_ref()
.and_then(|s| s.load_balancer.as_ref())
.and_then(|lb| lb.ingress.as_ref())
.map(|list| list.iter().any(|i| i.ip.as_deref() == Some(&external_ip)))
.unwrap_or(false);
if status_already_correct {
PATCHED_INGRESSES.insert(key, external_ip);
} else {
PATCHED_INGRESSES.insert(key.clone(), external_ip.clone());
let client_clone = client.clone();
let name_clone = name.to_string();
let ns_clone = ns.to_string();
let key_clone = key.clone();
tokio::spawn(async move {
if let Err(e) = patch_ingress_status_address(&client_clone, &ns_clone, &name_clone, &external_ip).await {
error!("Failed to patch ingress status address {}/{}: {}", &ns_clone, &name_clone, e);
PATCHED_INGRESSES.remove(&key_clone);
}
});
}
}
} else {
if let (Some(name), Some(ns)) = (ing.metadata.name.as_deref(), ing.metadata.namespace.as_deref()) {
PATCHED_INGRESSES.remove(&format!("{}/{}", ns, name));
}
}
if let Some(rules) = &spec.rules {
for rule in rules {
let host = match &rule.host {
Some(h) => h.clone(),
None => "*".to_string(),
};
let host = rule.host.clone().unwrap_or_else(|| "*".to_string());
if let Some(http) = &rule.http {
for path_entry in &http.paths {
let path = path_entry.path.clone().unwrap_or_else(|| "/".to_string());
if let Some(service) = &path_entry.backend.service {
let service_name = service.name.clone();
let mut update = AralezRouteUpdate {
let annot = parse_ingress_config(ing);
let update = AralezRouteUpdate {
host: host.clone(),
path,
service_name,
is_deleted,
rate_limit: None,
x4xx_limit: None,
sticky_sessions: None,
client_headers: None,
server_headers: None,
rate_limit: annot.rate_limit,
x4xx_limit: annot.x4xx_limit,
client_headers: annot.client_headers,
server_headers: annot.server_headers,
namespace: annot.namespace,
};
if let Some(annot) = parse_ingress_config(ing) {
update.rate_limit = annot.rate_limit;
update.x4xx_limit = annot.x4xx_limit;
update.sticky_sessions = annot.sticky_sessions;
update.client_headers = annot.client_headers;
update.server_headers = annot.server_headers;
}
if let Err(e) = tx.send(update).await {
error!("Failed to send route update to channel: {:?}", e);
}
@@ -160,54 +171,26 @@ pub async fn create_custom_k8s_client(api_server_url: &str, bearer_token: &str,
Ok(client)
}
fn parse_ingress_config(ing: &Ingress) -> Option<AralezAnnotation> {
if let Some(annotations) = &ing.metadata.annotations {
let mut clh: Option<Vec<String>> = None;
let mut srh: Option<Vec<String>> = None;
fn parse_ingress_config(ing: &Ingress) -> AralezAnnotation {
let namespace = ing.metadata.namespace.clone().unwrap_or_else(|| "default".to_string());
let annotations = ing.metadata.annotations.as_ref();
if let Some(ch) = annotations.get("aralez.rs/client_headers") {
let data: Vec<String> = serde_json::from_str(ch).unwrap_or_default();
clh = Some(data);
}
if let Some(sh) = annotations.get("aralez.rs/server_headers") {
let data: Vec<String> = serde_json::from_str(sh).unwrap_or_default();
srh = Some(data);
};
let client_headers = annotations.and_then(|a| a.get("aralez.rs/client_headers")).and_then(|ch| serde_json::from_str(ch).ok());
let annots = AralezAnnotation {
rate_limit: annotations.get("aralez.rs/rate_limit").and_then(|v| v.parse().ok()),
x4xx_limit: annotations.get("aralez.rs/x4xx_limit").and_then(|v| v.parse().ok()),
sticky_sessions: annotations.get("aralez.rs/sticky_sessions").and_then(|v| v.parse().ok()),
client_headers: clh,
server_headers: srh,
};
return Some(annots);
let server_headers = annotations.and_then(|a| a.get("aralez.rs/server_headers")).and_then(|sh| serde_json::from_str(sh).ok());
AralezAnnotation {
rate_limit: annotations.and_then(|a| a.get("aralez.rs/rate_limit")).and_then(|v| v.parse().ok()),
x4xx_limit: annotations.and_then(|a| a.get("aralez.rs/x4xx_limit")).and_then(|v| v.parse().ok()),
client_headers,
server_headers,
namespace,
}
None
}
pub async fn patch_ingress_status_address(
client: &Client,
namespace: &str,
name: &str,
current_status: Option<&k8s_openapi::api::networking::v1::IngressStatus>,
external_ip: &str,
) {
if let Some(status) = current_status {
if let Some(lb) = &status.load_balancer {
if let Some(ing_list) = &lb.ingress {
if ing_list.iter().any(|i| i.ip.as_deref() == Some(external_ip)) {
return;
}
}
}
}
pub async fn patch_ingress_status_address(client: &Client, namespace: &str, name: &str, external_ip: &str) -> Result<(), kube::Error> {
let ingresses: Api<Ingress> = Api::namespaced(client.clone(), namespace);
let patch = json!({
"apiVersion": "networking.k8s.io/v1",
"kind": "Ingress",
"status": {
"loadBalancer": {
"ingress": [
@@ -218,12 +201,17 @@ pub async fn patch_ingress_status_address(
}
}
});
let params = PatchParams::default();
let params = PatchParams::apply("aralez-ingress-controller").force();
match ingresses.patch_status(name, &params, &Patch::Apply(patch)).await {
Ok(_) => info!("Patched Ingress {}/{} ADDRESS to {}", namespace, name, external_ip),
Err(e) => error!("Failed to patch status for Ingress {}/{}: {:?}", namespace, name, e),
match ingresses.patch_status(name, &params, &Patch::Strategic(patch)).await {
Ok(_) => {
info!("Successfully patched Ingress {}/{} ADDRESS to {}", namespace, name, external_ip);
Ok(())
}
Err(e) => {
error!("Failed to patch status for Ingress {}/{}: {:?}", namespace, name, e);
Err(e)
}
}
}
@@ -269,3 +257,23 @@ pub async fn discover_controller_ip(client: &Client, namespace: &str, service_na
}
None
}
pub async fn resolve_external_ip(client: &Client) -> String {
if let Ok(ip) = std::env::var("ARALEZ_EXTERNAL_IP") {
let trimmed = ip.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
}
let svc_name = std::env::var("ARALEZ_SERVICE_NAME").unwrap_or_else(|_| "aralez-service".to_string());
let svc_ns = std::env::var("ARALEZ_SERVICE_NAMESPACE").unwrap_or_else(|_| "aralez".to_string());
if let Some(ip) = discover_controller_ip(client, &svc_ns, &svc_name).await {
return ip;
}
if let Some(ip) = discover_node_ip(client).await {
return ip;
}
"127.0.0.1".to_string()
}
+7
View File
@@ -0,0 +1,7 @@
pub mod consul;
pub mod discovery;
pub mod kuberconsul;
pub mod kubernetes;
pub mod kubewatcher;
pub use discovery::*;
+6
View File
@@ -0,0 +1,6 @@
pub mod auth;
pub mod core;
pub mod ingress;
pub mod logging;
pub mod tls;
pub mod utils;
+336
View File
@@ -0,0 +1,336 @@
use crate::logging::types::start_logging_backend;
use crate::utils::metrics::LOGGING_ERRORS;
use crate::utils::types::AppConfig;
use anyhow::Result;
use log::{error, info, log, Level, LevelFilter, Record};
use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
use log4rs::append::rolling_file::policy::compound::CompoundPolicy;
use log4rs::append::rolling_file::RollingFileAppender;
use log4rs::append::Append;
use log4rs::config::Logger;
use log4rs::{
append::console::ConsoleAppender,
config::{Appender, Config as Log4rsConfig, Root},
encode::pattern::PatternEncoder,
};
use pingora_cache::CachePhase;
use pingora_http::Version;
use pingora_proxy::Session;
use serde::Serialize;
use std::cell::RefCell;
use std::fmt::Write;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::OnceLock;
use tokio::sync::mpsc;
#[derive(Debug)]
pub struct LogMessage {
pub response_code: u16,
pub summary: String,
pub client_ip: IpAddr,
pub version: Version,
pub user_agent: String,
pub cache_status: CachePhase,
}
#[derive(Debug, Serialize, Clone)]
pub struct StructuredSystemLog {
pub target: &'static str,
pub level: Level,
pub message: String,
}
thread_local! {
static LOG_BUF: RefCell<String> = RefCell::new(String::with_capacity(512));
}
static ACCESS_LOG_SENDER: OnceLock<mpsc::Sender<LogMessage>> = OnceLock::new();
pub(crate) static SYSTEM_LOG_SENDER: OnceLock<mpsc::Sender<StructuredSystemLog>> = OnceLock::new();
static ACCESS_LOG: OnceLock<LogLevel> = OnceLock::new();
const LOG_BUFFER: usize = 16384;
static IS_STRUCTURED: OnceLock<bool> = OnceLock::new();
static LOG_BACKEND: OnceLock<&'static str> = OnceLock::new();
pub fn set_backend(key: String) -> Result<(), &'static str> {
let static_key: &'static str = Box::leak(key.into_boxed_str());
LOG_BACKEND.set(static_key).map_err(|_| "LOG_BACKEND was already initialized!")
}
pub fn get_backend() -> &'static str {
LOG_BACKEND.get().copied().unwrap_or("system")
}
#[derive(Debug)]
pub struct StructuredChannelAppender {
sender: mpsc::Sender<StructuredSystemLog>,
}
impl StructuredChannelAppender {
pub fn new(sender: mpsc::Sender<StructuredSystemLog>) -> Self {
Self { sender }
}
}
impl Append for StructuredChannelAppender {
fn append(&self, record: &Record) -> Result<()> {
let message = LOG_BUF.with(|buf| {
let mut b = buf.borrow_mut();
b.clear();
let _ = write!(&mut *b, "{}", record.args());
b.clone()
});
let target: &'static str = unsafe { std::mem::transmute(record.target()) };
let msg = StructuredSystemLog {
target,
level: record.level(),
message,
};
let _ = self.sender.try_send(msg);
Ok(())
}
fn flush(&self) {}
}
pub async fn log_builder(conf: &AppConfig, location: &Option<String>) {
let log_level = match conf.log_level.as_str() {
"info" => LevelFilter::Info,
"error" => LevelFilter::Error,
"warn" => LevelFilter::Warn,
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
"off" => LevelFilter::Off,
_ => {
println!("Error reading log level, defaulting to: INFO");
LevelFilter::Info
}
};
let mut pat: String = "{d(%Y-%m-%d %H:%M:%S)} {l} - {m} {n}".to_string();
if let Some(ptrn) = conf.log_pattern.clone() {
pat = ptrn;
}
let pattern = pat.as_str();
if let Some(backend) = conf.log_structured.as_deref() {
if let Some(backend_name) = backend.split_whitespace().next() {
let _ = set_backend(backend_name.to_string());
init_structured_log().await;
}
let syslog_appender: Option<Box<dyn Append>> = SYSTEM_LOG_SENDER
.get()
.cloned()
.map(|sender| Box::new(StructuredChannelAppender::new(sender)) as Box<dyn Append>);
let stdout = ConsoleAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build();
let mut config_builder = Log4rsConfig::builder().appender(Appender::builder().build("stdout", Box::new(stdout)));
if let Some(appender) = syslog_appender {
config_builder = config_builder
.appender(Appender::builder().build("syslog_appender", appender))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("pingora_proxy", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("pingora_core", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("pingora_pool", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("pingora_cache", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("hyper_util", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("aralez", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("h2", log_level));
} else {
config_builder = config_builder
.logger(Logger::builder().build("pingora_core", LevelFilter::Off))
.logger(Logger::builder().build("pingora_pool", LevelFilter::Off))
.logger(Logger::builder().build("pingora_cache", LevelFilter::Off))
.logger(Logger::builder().build("hyper_util", LevelFilter::Off))
.logger(Logger::builder().build("aralez", LevelFilter::Off))
.logger(Logger::builder().build("h2", LevelFilter::Off));
}
let config = config_builder.build(Root::builder().appender("stdout").build(log_level)).unwrap();
if let Err(e) = log4rs::init_config(config) {
eprintln!("Warning: log4rs logger already initialized: {:?}", e);
}
info!("Enabling structured logging with backend : {}", backend);
let _ = IS_STRUCTURED.set(true);
return;
}
if let Some(location) = location {
let parts: Vec<&str> = location.split(',').map(|s| s.trim()).collect();
let path = match parts.get(0).filter(|s| !s.is_empty()) {
Some(p) => p,
None => {
error!("Invalid log location string provided; falling back to stdout");
return;
}
};
let compress = parts.get(3).unwrap_or(&"No");
let size_mb: u64 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(100);
let keep: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(5);
let pattern_str = match compress {
&"compress" => format!("{}.{{}}.gz", path),
_ => format!("{}.{{}}", path),
};
let roller = FixedWindowRoller::builder().build(pattern_str.as_str(), keep).unwrap();
let trigger = SizeTrigger::new(size_mb * 1024 * 1024);
let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller));
let file = match RollingFileAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build(path, Box::new(policy)) {
Ok(f) => f,
Err(e) => {
error!("Failed to create log file at {}: {}. Falling back to stdout.", path, e);
return;
}
};
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("file", Box::new(file)))
.logger(Logger::builder().build("pingora_proxy", LevelFilter::Off))
.build(Root::builder().appender("file").build(log_level))
.unwrap();
if let Err(e) = log4rs::init_config(config) {
eprintln!("Warning: log4rs logger already initialized: {:?}", e);
}
info!("Logging to: {}, Max file size: {}mb, Files to keep: {}, compression: {} ", path, size_mb, keep, compress);
} else {
let stdout = ConsoleAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build();
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.logger(Logger::builder().build("pingora_proxy", LevelFilter::Off))
.build(Root::builder().appender("stdout").build(log_level))
.unwrap();
if let Err(e) = log4rs::init_config(config) {
eprintln!("Warning: log4rs logger already initialized: {:?}", e);
}
info!("No files are configured, logging to stdout");
}
}
pub async fn init_access_log(level_str: &str) {
let level = LogLevel::from_str(level_str);
let _ = ACCESS_LOG.set(level);
}
#[derive(Debug)]
pub enum LogLevel {
Access,
Error,
None,
}
impl LogLevel {
pub fn from_str(s: &str) -> Self {
match s {
"all" => LogLevel::Access,
"error" => LogLevel::Error,
_ => LogLevel::None,
}
}
}
#[derive(Debug)]
pub enum MatchStatus {
Ok2xx,
Er4xx,
Er5xx,
}
impl MatchStatus {
#[inline]
pub fn from_code(code: u16) -> Self {
match code {
100..=399 => Self::Ok2xx,
400..=499 => Self::Er4xx,
_ => Self::Er5xx,
}
}
}
pub async fn access_log(response_code: u16, summary: &str, session: &Session) {
let level = ACCESS_LOG.get().unwrap_or(&LogLevel::None);
let status = MatchStatus::from_code(response_code);
let should_log = match level {
LogLevel::Access => true,
LogLevel::None => false,
LogLevel::Error => matches!(status, MatchStatus::Er5xx),
};
if !should_log {
return;
}
let ip = session
.client_addr()
.and_then(|addr| addr.as_inet())
.map(|addr| addr.ip())
.unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST));
let user_agent = session.req_header().headers.get("user-agent").and_then(|v| v.to_str().ok()).unwrap_or("-");
let msg = LogMessage {
response_code,
summary: summary.to_owned(),
client_ip: ip,
version: session.req_header().version,
user_agent: user_agent.to_owned(),
cache_status: session.cache.phase(),
};
if IS_STRUCTURED.get().is_some() {
write_access_log(&msg).await;
return;
}
if let Some(sender) = ACCESS_LOG_SENDER.get() {
if let Err(_) = sender.try_send(msg) {
LOGGING_ERRORS.inc();
}
}
}
pub async fn init_access_logging(enabled: Option<String>) {
if enabled.is_some() {
LOGGING_ERRORS.set(0);
info!("Enabling {:?} log, with buffer of {} messages", ACCESS_LOG.get().unwrap_or(&LogLevel::None), LOG_BUFFER);
let (ltx, lrx) = mpsc::channel(LOG_BUFFER);
let _ = ACCESS_LOG_SENDER.set(ltx);
tokio::spawn(async move { access_log_receiver(lrx).await });
}
}
pub async fn init_structured_log() {
let (tx, rx) = mpsc::channel::<StructuredSystemLog>(LOG_BUFFER);
let _ = SYSTEM_LOG_SENDER.set(tx);
let backend = get_backend();
start_logging_backend(backend, rx);
}
pub async fn access_log_receiver(mut receiver: mpsc::Receiver<LogMessage>) {
while let Some(msg) = receiver.recv().await {
write_access_log(&msg).await;
}
}
async fn write_access_log(msg: &LogMessage) {
let level = match MatchStatus::from_code(msg.response_code) {
MatchStatus::Ok2xx => Level::Info,
MatchStatus::Er4xx => Level::Warn,
MatchStatus::Er5xx => Level::Error,
};
log!(
level,
"{}, {}, {}, client: {}, version: {:?}, useragent: {}",
msg.response_code,
msg.cache_status.as_str(),
msg.summary,
msg.client_ip,
msg.version,
msg.user_agent,
);
}
+187
View File
@@ -0,0 +1,187 @@
use crate::logging::core::StructuredSystemLog;
use crate::logging::types::{LogBackendPlugin, WriteLog};
use crate::utils::tools::get_hostname;
use async_trait::async_trait;
use bytes::{BufMut, BytesMut};
use elasticsearch::auth::Credentials;
use elasticsearch::http::transport::Transport;
use elasticsearch::{BulkParts, Elasticsearch};
use humantime::format_rfc3339;
use serde_json::json;
use std::env;
use std::sync::LazyLock;
use std::time::{Duration, SystemTime};
use tokio::sync::mpsc;
use tokio::sync::OnceCell;
static CLIENT: OnceCell<Elasticsearch> = OnceCell::const_new();
#[derive(Debug)]
struct ElasticHost {
url: &'static str,
}
struct ElasticSearchConfig {
pub hosts: &'static [ElasticHost],
pub user: &'static str,
pub password: &'static str,
pub hostname: &'static str,
pub index_name: &'static str,
pub flush_duration: u64,
pub batch_len: usize,
pub buffer_size: usize,
}
static ELASTIC: LazyLock<ElasticSearchConfig> = LazyLock::new(ElasticSearchConfig::load_from_env);
impl ElasticSearchConfig {
pub fn load_from_env() -> Self {
let raw_hosts = env::var("LOG_ELASTIC_HOSTS").unwrap_or_else(|_| "http://127.0.0.1:9200".to_string());
let mut hosts_struct = Vec::new();
for entry in raw_hosts.split(',') {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
let (scheme, rest) = if let Some(stripped) = entry.strip_prefix("https://") {
("https", stripped)
} else if let Some(stripped) = entry.strip_prefix("http://") {
("http", stripped)
} else {
("http", entry)
};
let (address, port) = match rest.split_once(':') {
Some((host_part, port_part)) => {
let port = port_part.parse::<u16>().unwrap_or(9200);
(host_part, port)
}
None => (rest, 9200),
};
let full_url = format!("{}://{}:{}", scheme, address, port);
hosts_struct.push(ElasticHost {
url: Box::leak(full_url.into_boxed_str()),
});
}
let user = env::var("LOG_ELASTIC_USER").unwrap_or_else(|_| "elastic".to_string());
let password = env::var("LOG_ELASTIC_PASSWORD").unwrap_or_else(|_| "elastic".to_string());
let index_name = env::var("LOG_ELASTIC_INDEX").unwrap_or_else(|_| "aralez-logs".to_string());
let s_flush_duration = env::var("LOG_ELASTIC_FLUSH_DURATION").unwrap_or_else(|_| "2".to_string());
let flush_duration = s_flush_duration.parse::<u64>().unwrap_or(2);
let s_batch_len = env::var("LOG_ELASTIC_BATCH_LEN").unwrap_or_else(|_| "200".to_string());
let batch_len = s_batch_len.parse::<usize>().unwrap_or(200);
let buffer_size: usize = 100 * 512;
Self {
hosts: Box::leak(hosts_struct.into_boxed_slice()),
user: Box::leak(user.into_boxed_str()),
password: Box::leak(password.into_boxed_str()),
flush_duration,
batch_len,
buffer_size,
hostname: Box::leak(get_hostname().into_boxed_str()),
index_name: Box::leak(index_name.into_boxed_str()),
}
}
}
async fn make_es_pool() -> Elasticsearch {
let valid_urls: Vec<&str> = ELASTIC.hosts.iter().map(|target| target.url).collect();
if valid_urls.is_empty() {
log::error!("No valid Elasticsearch host URLs found in configuration");
return Elasticsearch::default();
}
let transport = match Transport::sniffing_node_list(valid_urls, Duration::from_secs(180)) {
Ok(t) => t,
Err(e) => {
log::error!("Failed to create sniffing transport: {}", e);
return Elasticsearch::default();
}
};
let credentials = Credentials::Basic(ELASTIC.user.to_string(), ELASTIC.password.to_string());
transport.set_auth(credentials);
let client = Elasticsearch::new(transport);
if let Ok(res) = client.ping().send().await {
if res.status_code().is_success() {
log::info!("Elasticsearch sniffing pool initialized successfully");
return client;
}
}
log::warn!("Elasticsearch pool initialized, but cluster ping returned non-200");
client
}
struct ElasticSearch;
#[async_trait]
impl WriteLog for ElasticSearch {
async fn run(&self, mut rx: mpsc::Receiver<StructuredSystemLog>) {
let mut batch = Vec::with_capacity(ELASTIC.batch_len);
let mut timer = tokio::time::interval(Duration::from_secs(ELASTIC.flush_duration));
let mut buffer = BytesMut::with_capacity(ELASTIC.buffer_size);
let index_header = format!("{{\"index\":{{\"_index\":\"{}\"}}}}\n", ELASTIC.index_name).into_bytes();
loop {
tokio::select! {
Some(msg) = rx.recv() => {
batch.push(msg);
if batch.len() >= ELASTIC.batch_len {
flush_to_es(&mut batch, &mut buffer, &index_header, ELASTIC.hostname).await;
}
}
_ = timer.tick() => {
if !batch.is_empty() {
flush_to_es(&mut batch, &mut buffer, &index_header, ELASTIC.hostname).await;
}
}
}
}
}
}
async fn flush_to_es(batch: &mut Vec<StructuredSystemLog>, buffer: &mut BytesMut, index_header: &[u8], hostname: &str) {
buffer.clear();
for msg in batch.drain(..) {
buffer.extend_from_slice(index_header);
let logstash_payload = json!({
"@timestamp": format_rfc3339(SystemTime::now()).to_string(),
"@version": "1",
"log": {
"level": msg.level,
"logger": msg.target,
},
"message": msg.message,
"host": {
"hostname": hostname,
}
});
if serde_json::to_writer((buffer).writer(), &logstash_payload).is_ok() {
buffer.extend_from_slice(b"\n");
}
}
if buffer.is_empty() {
return;
}
let payload = buffer.split().freeze();
let client = CLIENT.get_or_init(make_es_pool).await;
let _ = client.bulk(BulkParts::None).body(vec![payload]).send().await;
}
inventory::submit! {
LogBackendPlugin {
name: "elasticsearch",
factory: || Box::new(ElasticSearch),
}
}
+22
View File
@@ -0,0 +1,22 @@
use crate::logging::core::StructuredSystemLog;
use crate::logging::types::{LogBackendPlugin, WriteLog};
use async_trait::async_trait;
use tokio::sync::mpsc;
pub struct Example;
#[async_trait]
impl WriteLog for Example {
async fn run(&self, mut rx: mpsc::Receiver<StructuredSystemLog>) {
while let Some(msg) = rx.recv().await {
println!("Sending log Example : {:?}", msg);
}
}
}
inventory::submit! {
LogBackendPlugin {
name: "example",
factory: || Box::new(Example),
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod core;
pub mod elasticsearch;
pub mod example;
pub mod types;
+36
View File
@@ -0,0 +1,36 @@
use crate::logging::core::StructuredSystemLog;
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::LazyLock;
use tokio::sync::mpsc;
#[async_trait]
pub trait WriteLog: Send + Sync {
async fn run(&self, rx: mpsc::Receiver<StructuredSystemLog>);
}
pub struct LogBackendPlugin {
pub name: &'static str,
pub factory: fn() -> Box<dyn WriteLog>,
}
inventory::collect!(LogBackendPlugin);
static BACKENDS: LazyLock<HashMap<&'static str, Box<dyn WriteLog>>> = LazyLock::new(|| {
let mut map = HashMap::new();
for plugin in inventory::iter::<LogBackendPlugin> {
map.insert(plugin.name, (plugin.factory)());
}
map
});
pub fn start_logging_backend(backend: &str, rx: mpsc::Receiver<StructuredSystemLog>) {
if let Some(logger) = BACKENDS.get(backend) {
let logger_ref = logger.as_ref();
tokio::spawn(async move {
logger_ref.run(rx).await;
});
} else {
log::warn!("Unsupported logging mechanism: {}", backend);
}
}
+2 -4
View File
@@ -1,6 +1,4 @@
mod tls;
mod utils;
mod web;
use aralez::core;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
@@ -12,5 +10,5 @@ fn main() {
std::process::exit(0);
}
web::start::run();
core::start::run();
}
-2
View File
@@ -1,2 +0,0 @@
pub mod account;
pub mod order;
+3 -2
View File
@@ -1,3 +1,4 @@
use crate::core::webserver;
use crate::tls::acme::{account, order};
use crate::utils::lazylock::CHALLENGES;
use axum::body::Body;
@@ -6,7 +7,7 @@ use axum::http::{Response, StatusCode};
use axum::response::IntoResponse;
#[allow(clippy::needless_return)]
pub async fn acme_create(State(state): State<crate::web::webserver::AppState>) -> impl IntoResponse {
pub async fn acme_create(State(state): State<webserver::AppState>) -> impl IntoResponse {
match account::load_or_create(state.cert_creds.as_str()).await {
Ok(txt) => {
return Response::builder()
@@ -24,7 +25,7 @@ pub async fn acme_create(State(state): State<crate::web::webserver::AppState>) -
};
}
#[allow(clippy::needless_return)]
pub async fn acme_order(State(state): State<crate::web::webserver::AppState>, axum::extract::Path(domain): axum::extract::Path<String>) -> impl IntoResponse {
pub async fn acme_order(State(state): State<webserver::AppState>, axum::extract::Path(domain): axum::extract::Path<String>) -> impl IntoResponse {
let domain_clean = domain.trim_matches('/');
match order::order(domain_clean, state.cert_creds.as_str(), state.certs_dir).await {
Ok(txt) => {
+6
View File
@@ -0,0 +1,6 @@
pub mod account;
pub mod http01;
pub mod order;
pub use account::*;
pub use order::*;
+1
View File
@@ -1,3 +1,4 @@
pub mod acme;
pub mod grades;
pub mod load;
pub use load::*;
+6 -6
View File
@@ -23,9 +23,9 @@ unsafe impl GlobalAlloc for CountingAllocator {
// Uncomment following lines and comment allocator in main.rs
// #[global_allocator]
// pub static A: CountingAllocator = CountingAllocator;
#[allow(dead_code)]
fn for_example() {
let before = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
let after = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
println!("Allocations : {}", after - before);
}
// #[allow(dead_code)]
// fn for_example() {
// let before = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
// let after = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
// println!("Allocations : {}", after - before);
// }
+82 -1
View File
@@ -9,7 +9,7 @@ use std::time::Duration;
pub static HC_CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
pub async fn httpclient(method: &str, tls: bool, host: &str, path: &str, address: &str, port: u16, url: String) -> (bool, bool) {
pub async fn httpclient(method: &str, tls: bool, host: &str, path: &str, address: &str, port: u16, url: &str, payload: Bytes) -> (bool, bool) {
let method = match method {
"HEAD" => "HEAD",
"GET" => "GET",
@@ -24,6 +24,86 @@ pub async fn httpclient(method: &str, tls: bool, host: &str, path: &str, address
peer.options.verify_hostname = false;
}
let mut is_h2 = false;
peer.options.total_connection_timeout = Option::from(Duration::from_secs(5));
peer.options.alpn = ALPN::H2H1;
let (mut http_session, _) = match HC_CONNECTOR.get_http_session(&peer).await {
Ok(s) => s,
Err(e) => {
if let Some(msg) = e.context {
log::warn!("{}, type: {}", msg.as_str(), e.etype.as_str());
} else {
log::warn!("Fail to connect to addr: {}, tls {}, host {}", address, tls, host);
}
return (false, false);
}
};
match http_session.as_http2() {
Some(_) => {
peer.options.alpn = ALPN::H2;
is_h2 = true;
}
None => {
if ping_grpc(url).await {
return (true, true);
}
}
}
let mut req = match RequestHeader::build(method, path.as_bytes(), None) {
Ok(r) => r,
Err(e) => {
log::warn!("Failed to build request: {}", e);
return (false, false);
}
};
req.insert_header("Host", host).ok();
let body_bytes = if method == "POST" {
req.insert_header("Content-Type", "application/json").ok();
req.insert_header("Content-Length", payload.len().to_string()).ok();
payload
} else {
Bytes::new()
};
if let Err(e) = http_session.write_request_header(Box::new(req)).await {
log::warn!("Write header failed: {}", e);
return (false, false);
}
if let Err(e) = http_session.write_request_body(body_bytes, true).await {
log::warn!("Write body failed: {}", e);
return (false, false);
}
let status = match http_session.read_response_header().await {
Ok(_) => http_session.response_header().map(|r| r.status.as_u16()).unwrap_or(500),
Err(e) => {
if !is_h2 && ping_grpc(url).await {
return (true, true);
}
log::warn!("Health Check read failed ({}) : {} - {}", if is_h2 { "H2" } else { "H1" }, host, e);
return (false, false);
}
};
HC_CONNECTOR.release_http_session(http_session, &peer, None).await;
((200..500).contains(&status), is_h2)
}
/*
pub async fn httpclient(method: &str, tls: bool, host: &str, path: &str, address: &str, port: u16, url: String) -> (bool, bool) {
let method = match method {
"HEAD" => "HEAD",
"GET" => "GET",
"POST" => "POST",
_ => "GET",
};
let mut peer = HttpPeer::new((address, port), tls, host.to_string());
if tls {
peer.options.verify_cert = false;
peer.options.verify_hostname = false;
}
let mut is_h2 = false;
peer.options.total_connection_timeout = Option::from(Duration::from_secs(5));
peer.options.alpn = ALPN::H2H1;
@@ -93,3 +173,4 @@ pub async fn httpclient(method: &str, tls: bool, host: &str, path: &str, address
HC_CONNECTOR.release_http_session(http_session, &peer, None).await;
((200..500).contains(&status), is_h2)
}
*/
+4 -2
View File
@@ -1,7 +1,8 @@
use crate::utils::hcclient::httpclient;
use crate::utils::lazylock::REVERSE_STORE;
use crate::utils::structs::{InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use crate::utils::tools::*;
use crate::utils::types::{InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use bytes::Bytes;
use dashmap::DashMap;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
@@ -9,6 +10,7 @@ use std::time::Duration;
use tokio::net::TcpStream;
use tokio::time::{interval, timeout};
use tokio_native_tls::native_tls::TlsConnector;
const EMPTY_PAYLOAD: Bytes = Bytes::from_static(b"");
pub async fn hc2(upslist: Arc<UpstreamsDashMap>, fullist: Arc<UpstreamsDashMap>, idlist: Arc<UpstreamsIdMap>, params: (&str, u64)) {
let mut period = interval(Duration::from_secs(params.1));
@@ -66,7 +68,7 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str) -> UpstreamsD
} else {
format!("http://{}:{}{}", upstream.address, upstream.port, path)
};
let resp = httpclient(method, tls, host, path, upstream.address.as_ref(), upstream.port, link).await;
let resp = httpclient(method, tls, host, path, upstream.address.as_ref(), upstream.port, link.as_str(), EMPTY_PAYLOAD).await;
if resp.0 {
if resp.1 {
scheme.is_http2 = resp.1;
+28 -19
View File
@@ -1,7 +1,7 @@
use crate::utils::consul::ConsulService;
use crate::utils::kuberconsul::match_path;
use crate::utils::kubernetes::KubeEndpoints;
use crate::utils::structs::{GlobalServiceMapping, InnerMap};
use crate::ingress::consul::ConsulService;
use crate::ingress::kuberconsul::match_path;
use crate::ingress::kubernetes::KubeEndpointSliceList;
use crate::utils::types::{GlobalServiceMapping, InnerMap};
use ahash::HashMap;
use dashmap::DashMap;
use pingora_core::connectors::http::Connector;
@@ -36,7 +36,9 @@ pub struct ConsulServicesInternal {
#[serde(rename = "aralez.to_https")]
pub to_https: Option<bool>,
}
pub type ConsulServices = HashMap<String, ConsulServicesInternal>;
pub async fn for_consul_list(url: &str, token: Option<String>) -> Option<ConsulServices> {
if let Some(data) = getfromapi(url, token, "consul").await {
let yo = parse_services(data);
@@ -75,6 +77,7 @@ fn parse_services(json: Vec<u8>) -> Result<ConsulServices, serde_json::Error> {
})
.collect())
}
pub async fn for_consul(url: &str, token: Option<String>, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> {
if let Some(data) = getfromapi(url, token, "consul").await {
let endpoints: Vec<ConsulService> = serde_json::from_slice(&data).ok()?;
@@ -103,18 +106,29 @@ pub async fn for_consul(url: &str, token: Option<String>, conf: &GlobalServiceMa
pub async fn for_kuber(url: &str, token: &str, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> {
if let Some(data) = getfromapi(url, Some(token.to_string()), "kubernetes").await {
let endpoints: KubeEndpoints = serde_json::from_slice(&data).ok()?;
let slice_list: KubeEndpointSliceList = serde_json::from_slice(&data).ok()?;
let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new();
let mut inner_vec = Vec::new();
if let Some(subsets) = endpoints.subsets {
for subset in subsets {
if let (Some(addresses), Some(ports)) = (subset.addresses, subset.ports) {
for addr in addresses {
for port in &ports {
for slice in slice_list.items {
let ports = match &slice.ports {
Some(p) if !p.is_empty() => p,
_ => continue,
};
for ep in &slice.endpoints {
let is_ready = ep.conditions.as_ref().and_then(|c| c.ready).unwrap_or(true);
if !is_ready {
continue;
}
for addr in &ep.addresses {
for port in ports {
if let Some(port_num) = port.port {
let to_add = Arc::from(InnerMap {
address: Arc::from(addr.ip.as_str()),
port: port.port,
address: Arc::from(addr.as_str()),
port: port_num,
is_ssl: false,
is_http2: false,
to_https: conf.to_https.unwrap_or(false),
@@ -130,6 +144,7 @@ pub async fn for_kuber(url: &str, token: &str, conf: &GlobalServiceMapping) -> O
}
}
}
if !inner_vec.is_empty() {
match_path(conf, &upstreams, inner_vec);
return Some(upstreams);
@@ -137,6 +152,7 @@ pub async fn for_kuber(url: &str, token: &str, conf: &GlobalServiceMapping) -> O
}
None
}
pub async fn getfromapi(url: &str, token: Option<String>, provider: &str) -> Option<Vec<u8>> {
let (host, port, path, is_tls) = parse_url(&url).ok()?;
@@ -226,10 +242,3 @@ fn parse_url(url: &str) -> Result<(&str, u16, &str, bool), &'static str> {
Ok((host, port, uri, is_https))
}
// fn base64_decode(encoded: &str) -> Option<String> {
// let decoded_bytes = general_purpose::STANDARD.decode(encoded).unwrap_or_default();
// let decoded: Result<HashMap<String, String>, serde_json::Error> = serde_json::from_slice(&decoded_bytes);
// println!("Decoded: {:?}", decoded);
// None
// }
+76 -16
View File
@@ -47,28 +47,76 @@ pub static REQUESTS_BY_UPSTREAM: LazyLock<IntCounterVec> =
pub static REQUESTS_BY_VERSION: LazyLock<IntCounterVec> =
LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_version_total", "Number of requests by HTTP versions", &["version"]).unwrap());
pub static HTTP_11_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["HTTP/1.1"]));
pub static HTTP_20_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["HTTP/2.0"]));
pub static HTTP_30_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["HTTP/3.0"]));
pub static HTTP_10_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["HTTP/1.0"]));
pub static HTTP_UNK_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["Unknown"]));
pub static METHOD_GET: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["GET"]));
pub static METHOD_POST: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["POST"]));
pub static METHOD_PUT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["PUT"]));
pub static METHOD_DELETE: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["DELETE"]));
pub static METHOD_HEAD: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["HEAD"]));
pub static METHOD_OPTIONS: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["OPTIONS"]));
pub static METHOD_PATCH: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["PATCH"]));
pub static METHOD_OTHER: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["OTHER"]));
pub static STATUS_100: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["100"]));
pub static STATUS_101: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["101"]));
pub static STATUS_200: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["200"]));
pub static STATUS_204: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["204"]));
pub static STATUS_301: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["301"]));
pub static STATUS_302: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["302"]));
pub static STATUS_304: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["304"]));
pub static STATUS_400: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["400"]));
pub static STATUS_403: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["403"]));
pub static STATUS_404: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["404"]));
pub static STATUS_500: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["500"]));
pub static STATUS_501: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["501"]));
pub static STATUS_502: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["502"]));
pub static STATUS_503: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["503"]));
pub fn calc_metrics(metric_types: &MetricTypes) {
REQUEST_COUNT.inc();
let version_str = match metric_types.version {
Version::HTTP_11 => "HTTP/1.1",
Version::HTTP_2 => "HTTP/2.0",
Version::HTTP_3 => "HTTP/3.0",
Version::HTTP_10 => "HTTP/1.0",
_ => "Unknown",
match metric_types.version {
Version::HTTP_11 => HTTP_11_COUNT.inc(),
Version::HTTP_2 => HTTP_20_COUNT.inc(),
Version::HTTP_3 => HTTP_30_COUNT.inc(),
Version::HTTP_10 => HTTP_10_COUNT.inc(),
_ => HTTP_UNK_COUNT.inc(),
};
REQUESTS_BY_VERSION.with_label_values(&[version_str]).inc();
RESPONSE_CODES.with_label_values(&[metric_types.code.unwrap_or(StatusCode::GONE).as_str()]).inc();
REQUESTS_BY_METHOD.with_label_values(&[metric_types.method.as_str()]).inc();
match metric_types.method.as_str() {
"GET" => METHOD_GET.inc(),
"POST" => METHOD_POST.inc(),
"PUT" => METHOD_PUT.inc(),
"DELETE" => METHOD_DELETE.inc(),
"HEAD" => METHOD_HEAD.inc(),
"OPTIONS" => METHOD_OPTIONS.inc(),
"PATCH" => METHOD_PATCH.inc(),
_ => METHOD_OTHER.inc(),
}
let code = metric_types.code.unwrap_or(StatusCode::GONE);
match code.as_u16() {
100 => STATUS_100.inc(),
101 => STATUS_101.inc(),
200 => STATUS_200.inc(),
204 => STATUS_204.inc(),
301 => STATUS_301.inc(),
302 => STATUS_302.inc(),
304 => STATUS_304.inc(),
400 => STATUS_400.inc(),
403 => STATUS_403.inc(),
404 => STATUS_404.inc(),
500 => STATUS_500.inc(),
501 => STATUS_501.inc(),
502 => STATUS_502.inc(),
503 => STATUS_503.inc(),
_ => RESPONSE_CODES.with_label_values(&[code.as_str()]).inc(), // Fallback for rare status codes
}
REQUESTS_BY_UPSTREAM.with_label_values(&[metric_types.upstream.as_ref()]).inc();
RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64());
if let Some(eviction) = EVICTION.get() {
CACHE_SIZE_BYTES.set(eviction.total_size() as i64);
CACHE_ITEMS.set(eviction.total_items() as i64);
CACHE_EVICTED_BYTES.set(eviction.evicted_size() as i64);
CACHE_EVICTED_ITEMS.set(eviction.evicted_items() as i64);
}
}
pub(crate) fn get_memory_usage() -> usize {
@@ -87,3 +135,15 @@ pub(crate) fn get_memory_usage() -> usize {
pub fn get_open_files() -> usize {
std::fs::read_dir("/proc/self/fd").map(|dir| dir.count()).unwrap_or(0)
}
pub async fn calc_cache_metrics() {
loop {
if let Some(eviction) = EVICTION.get() {
CACHE_SIZE_BYTES.set(eviction.total_size() as i64);
CACHE_ITEMS.set(eviction.total_items() as i64);
CACHE_EVICTED_BYTES.set(eviction.evicted_size() as i64);
CACHE_EVICTED_ITEMS.set(eviction.evicted_items() as i64);
}
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
}
+4 -9
View File
@@ -1,19 +1,14 @@
pub mod auth;
pub mod consul;
pub mod discovery;
mod filewatch;
pub mod fordebug;
pub mod grpc;
pub mod hcclient;
pub mod healthcheck;
pub mod httpclient;
pub mod jwt;
pub mod kuberconsul;
pub mod kubernetes;
pub mod kubewatcher;
// pub mod ingress;
pub mod lazylock;
pub mod metrics;
pub mod parceyaml;
pub mod state;
pub mod structs;
pub mod tools;
pub mod types;
pub(crate) mod watch;
pub use watch::*;
+4 -6
View File
@@ -1,9 +1,8 @@
use crate::utils::healthcheck;
use crate::utils::lazylock::REVERSE_STORE;
use crate::utils::state::{is_first_run, mark_not_first_run};
use crate::utils::structs::*;
use crate::utils::tools::{clone_dashmap, clone_dashmap_into, print_upstreams};
use crate::web::logging::log_builder;
use crate::utils::types::*;
use dashmap::DashMap;
use log::{error, info, warn};
use std::collections::HashMap;
@@ -91,7 +90,7 @@ pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>,
}
info!("Reading upstreams from {}", d);
data // [2606:4700:2ff9::1]:443
data
}
"content" => {
info!("Reading upstreams from API post body");
@@ -259,13 +258,12 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
}
}
pub fn parce_main_config(path: &str) -> AppConfig {
let data = fs::read_to_string(path).unwrap();
let data = fs::read_to_string(path).expect("Failed to read main config file");
let mut cfo: AppConfig = noyalib::from_str(&data).expect("Failed to parse main config file");
if let Ok(jwt_key) = env::var("JWT_KEY") {
cfo.master_key = Some(jwt_key);
};
log_builder(&cfo, &cfo.log_file);
// log_builder(&cfo, &cfo.log_file);
cfo.hc_method = cfo.hc_method.to_uppercase();
if let Some((ip, port_str)) = cfo.config_address.rsplit_once(':') {
if let Ok(port) = port_str.parse::<u16>() {
+18 -51
View File
@@ -1,13 +1,11 @@
use crate::tls::load;
use crate::tls::load::CertificateConfig;
use crate::utils::structs::{Extraparams, InnerMapForJson, UpstreamSnapshotForJson, UpstreamsDashMap, UpstreamsIdMap};
use crate::utils::types::{Extraparams, InnerMapForJson, UpstreamSnapshotForJson, UpstreamsDashMap, UpstreamsIdMap};
use dashmap::DashMap;
use log::{error, info};
use notify::{event::ModifyKind, Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use privdrop::PrivDrop;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::any::type_name;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::fs::OpenOptions;
@@ -16,12 +14,11 @@ use std::net::SocketAddr;
use std::net::TcpListener;
use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::OpenOptionsExt;
use std::process::Command;
use std::str::FromStr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{channel, Sender};
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::{fs, process, thread, time};
use std::{env, fs, process, thread, time};
pub fn print_upstreams(upstreams: &UpstreamsDashMap, extraparams: &Extraparams) {
let mut out = String::new();
@@ -47,24 +44,6 @@ pub fn print_upstreams(upstreams: &UpstreamsDashMap, extraparams: &Extraparams)
}
info!("\n{}", out.trim_end());
}
#[allow(dead_code)]
pub fn typeoff<T>(_: T) {
let to = type_name::<T>();
println!("{:?}", to);
}
#[allow(dead_code)]
pub fn string_to_bool(val: Option<&str>) -> Option<bool> {
match val {
Some(v) => match v {
"yes" => Some(true),
"true" => Some(true),
_ => Some(false),
},
None => Some(false),
}
}
pub fn clone_dashmap(original: &UpstreamsDashMap) -> UpstreamsDashMap {
let new_map: UpstreamsDashMap = DashMap::new();
@@ -209,33 +188,6 @@ pub fn listdir(dir: String) -> Vec<load::CertificateConfig> {
certificate_configs
}
pub fn watch_folder(path: String, sender: Sender<Vec<CertificateConfig>>) -> notify::Result<()> {
let (tx, rx) = channel();
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
watcher.watch(path.as_ref(), RecursiveMode::Recursive)?;
info!("Watching for certificates in : {}", path);
let certificate_configs = listdir(path.clone());
sender.send(certificate_configs)?;
let mut start = Instant::now();
loop {
match rx.recv_timeout(Duration::from_secs(1)) {
Ok(Ok(event)) => match &event.kind {
EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(_) | EventKind::Remove(_) => {
if start.elapsed() > Duration::from_secs(1) {
start = Instant::now();
let certificate_configs = listdir(path.clone());
sender.send(certificate_configs)?;
info!("Certificate changed: {:?}, {:?}", event.kind, event.paths);
}
}
_ => {}
},
Ok(Err(e)) => error!("Watch error: {:?}", e),
Err(_) => {}
}
}
}
pub fn drop_priv(user: String, group: String, http_addr: String, tls_addr: Option<String>) {
thread::sleep(time::Duration::from_millis(10));
loop {
@@ -397,3 +349,18 @@ pub fn split_host_port(addr: &str, tls: bool) -> Option<(&str, u16, bool, &str)>
}
};
}
pub fn get_hostname() -> String {
if let Ok(host) = env::var("HOSTNAME").or_else(|_| env::var("COMPUTERNAME")) {
if !host.trim().is_empty() {
return host;
}
}
Command::new("hostname")
.output()
.ok()
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "unknown".to_string())
}
@@ -131,6 +131,8 @@ pub struct AppConfig {
pub runuser: Option<String>,
pub rungroup: Option<String>,
pub log_file: Option<String>,
pub log_pattern: Option<String>,
pub log_structured: Option<String>,
pub tcp_keepalive_idle: Option<u64>,
pub tcp_keepalive_interval: Option<u64>,
pub tcp_keepalive_count: Option<usize>,
+32 -3
View File
@@ -1,15 +1,18 @@
use crate::tls::CertificateConfig;
use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::Configuration;
use log::error;
use crate::utils::tools::listdir;
use crate::utils::types::Configuration;
use log::{error, info};
use notify::event::ModifyKind;
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use pingora::prelude::sleep;
use std::path::Path;
use std::sync::mpsc::channel;
use std::time::{Duration, Instant};
use tokio::sync::mpsc::Sender;
use tokio::task;
pub async fn start(fp: String, toreturn: Sender<Configuration>) {
pub async fn file_watch(fp: String, toreturn: Sender<Configuration>) {
sleep(Duration::from_millis(50)).await; // For having nice logs :-)
let file_path = fp.as_str();
let parent_dir = Path::new(file_path).parent().unwrap();
@@ -50,3 +53,29 @@ pub async fn start(fp: String, toreturn: Sender<Configuration>) {
}
}
}
pub fn folder_watch(path: String, sender: std::sync::mpsc::Sender<Vec<CertificateConfig>>) -> notify::Result<()> {
let (tx, rx) = channel();
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
watcher.watch(path.as_ref(), RecursiveMode::Recursive)?;
info!("Watching for certificates in : {}", path);
let certificate_configs = listdir(path.clone());
sender.send(certificate_configs)?;
let mut start = Instant::now();
loop {
match rx.recv_timeout(Duration::from_secs(1)) {
Ok(Ok(event)) => match &event.kind {
EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(_) | EventKind::Remove(_) => {
if start.elapsed() > Duration::from_secs(1) {
start = Instant::now();
let certificate_configs = listdir(path.clone());
sender.send(certificate_configs)?;
info!("Certificate changed: {:?}, {:?}", event.kind, event.paths);
}
}
_ => {}
},
Ok(Err(e)) => error!("Watch error: {:?}", e),
Err(_) => {}
}
}
}
-176
View File
@@ -1,176 +0,0 @@
use crate::utils::metrics::LOGGING_ERRORS;
use crate::utils::structs::AppConfig;
use log::{info, LevelFilter};
use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
use log4rs::append::rolling_file::policy::compound::CompoundPolicy;
use log4rs::append::rolling_file::RollingFileAppender;
use log4rs::{
append::console::ConsoleAppender,
config::{Appender, Config as Log4rsConfig, Root},
encode::pattern::PatternEncoder,
};
use pingora_cache::CachePhase;
use pingora_http::Version;
use pingora_proxy::Session;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::OnceLock;
use tokio::sync::mpsc;
#[derive(Debug)]
pub struct LogMessage {
pub response_code: u16,
pub summary: String,
pub client_ip: IpAddr,
pub version: Version,
pub user_agent: String,
pub cache_status: CachePhase,
}
static LOG_SENDER: OnceLock<mpsc::Sender<LogMessage>> = OnceLock::new();
static ACCESS_LOG: OnceLock<LogLevel> = OnceLock::new();
const LOG_BUFFER: usize = 16384;
pub fn log_builder(conf: &AppConfig, location: &Option<String>) {
let log_level = match conf.log_level.as_str() {
"info" => LevelFilter::Info,
"error" => LevelFilter::Error,
"warn" => LevelFilter::Warn,
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
"off" => LevelFilter::Off,
_ => {
println!("Error reading log level, defaulting to: INFO");
LevelFilter::Info
}
};
let pattern = "{d(%Y-%m-%d %H:%M:%S)} {l} {t} - {m}\n";
if let Some(location) = location {
let parts: Vec<&str> = location.splitn(4, ',').map(|s| s.trim()).collect();
let path = parts.get(0).expect("Syntax error, could not get path for log files");
let compress = parts.get(3).unwrap_or(&"No");
let size_mb: u64 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(100);
let keep: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(5);
let pattern_str = match compress {
&"compress" => format!("{}.{{}}.gz", path),
_ => format!("{}.{{}}", path),
};
let roller = FixedWindowRoller::builder().build(pattern_str.as_str(), keep).unwrap();
let trigger = SizeTrigger::new(size_mb * 1024 * 1024);
let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller));
let file = RollingFileAppender::builder()
.encoder(Box::new(PatternEncoder::new(pattern)))
.build(path, Box::new(policy))
.unwrap();
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("file", Box::new(file)))
.build(Root::builder().appender("file").build(log_level))
.unwrap();
log4rs::init_config(config).unwrap();
info!("Logging to: {}, Max file size: {}mb, Files to keep: {}, compression: {} ", path, size_mb, keep, compress);
} else {
let stdout = ConsoleAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build();
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.build(Root::builder().appender("stdout").build(log_level))
.unwrap();
log4rs::init_config(config).unwrap();
info!("No files are configured, logging to stdout");
}
}
pub fn init_access_log(level_str: &str) {
let level = LogLevel::from_str(level_str);
let _ = ACCESS_LOG.set(level);
}
#[derive(Debug)]
pub enum LogLevel {
Access,
Error,
None,
}
impl LogLevel {
pub fn from_str(s: &str) -> Self {
match s {
"all" => LogLevel::Access,
"error" => LogLevel::Error,
_ => LogLevel::None,
}
}
}
pub fn access_log(response_code: u16, summary: &str, session: &Session) {
let level = ACCESS_LOG.get().unwrap_or(&LogLevel::None);
let should_log = match level {
LogLevel::Access => true,
LogLevel::None => false,
LogLevel::Error => !(100..=399).contains(&response_code),
};
if !should_log {
return;
}
let ip = session
.client_addr()
.and_then(|addr| addr.as_inet())
.map(|addr| addr.ip())
.unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST));
let user_agent = session.req_header().headers.get("user-agent").and_then(|v| v.to_str().ok()).unwrap_or("-");
// let cache_status = match session.cache.phase() {
// CachePhase::Hit => "Hit",
// CachePhase::Miss => "Miss",
// CachePhase::Expired => "Expired",
// _ => "Disabled",
// };
let log = LogMessage {
response_code,
summary: summary.to_owned(),
client_ip: ip,
version: session.req_header().version,
user_agent: user_agent.to_owned(),
cache_status: session.cache.phase(),
};
if let Some(sender) = LOG_SENDER.get() {
let sender = sender;
if let Err(_) = sender.try_send(log) {
LOGGING_ERRORS.inc();
}
}
}
pub fn init_logging(enabled: Option<String>) {
if let Some(_) = enabled {
LOGGING_ERRORS.set(0);
info!("Enabling {:?} log, with buffer of {} messages", ACCESS_LOG.get().unwrap_or(&LogLevel::None), LOG_BUFFER);
let (ltx, lrx) = mpsc::channel(LOG_BUFFER);
LOG_SENDER.set(ltx).unwrap();
std::thread::spawn(move || log_receiver(lrx));
}
}
pub fn log_receiver(mut receiver: mpsc::Receiver<LogMessage>) {
while let Some(msg) = receiver.blocking_recv() {
info!(
"{}, {}, {}, client: {}, version: {:?}, useragent: {}",
msg.response_code,
msg.cache_status.as_str(),
msg.summary,
msg.client_ip,
msg.version,
msg.user_agent,
);
}
}