From 25c2c391c912cd6de1156b7687e825cfd1481bb6 Mon Sep 17 00:00:00 2001 From: cc14514 Date: Sat, 31 Jan 2026 13:17:21 +0800 Subject: [PATCH 1/2] chore(docker,k8s): extend Docker image build and provide complete Kubernetes example Signed-off-by: cc14514 --- .gitignore | 3 +- Dockerfile | 134 +++++- Makefile | 114 ++++- README-CN.md | 1 + README.md | 1 + docs/images/bfe-k8s.png | Bin 0 -> 52632 bytes .../installation/install_using_docker.md | 56 ++- examples/kubernetes/README.md | 166 +++++++ examples/kubernetes/api-server-configmap.yaml | 86 ++++ examples/kubernetes/api-server-deploy.yaml | 83 ++++ examples/kubernetes/bfe-configmap.yaml | 237 ++++++++++ examples/kubernetes/bfe-deploy.yaml | 95 ++++ examples/kubernetes/kustomization.yaml | 13 + examples/kubernetes/mysql-deploy.yaml | 427 ++++++++++++++++++ examples/kubernetes/namespace.yaml | 6 + .../kubernetes/service-controller-deploy.yaml | 117 +++++ examples/kubernetes/whoami-deploy.yaml | 61 +++ 17 files changed, 1576 insertions(+), 24 deletions(-) create mode 100644 docs/images/bfe-k8s.png create mode 100644 examples/kubernetes/README.md create mode 100644 examples/kubernetes/api-server-configmap.yaml create mode 100644 examples/kubernetes/api-server-deploy.yaml create mode 100644 examples/kubernetes/bfe-configmap.yaml create mode 100644 examples/kubernetes/bfe-deploy.yaml create mode 100644 examples/kubernetes/kustomization.yaml create mode 100644 examples/kubernetes/mysql-deploy.yaml create mode 100644 examples/kubernetes/namespace.yaml create mode 100644 examples/kubernetes/service-controller-deploy.yaml create mode 100644 examples/kubernetes/whoami-deploy.yaml diff --git a/.gitignore b/.gitignore index 70b7e4302..b558346c6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ .svn .tmp .download -output +output/ .*.swp .*.swo /**/y.output @@ -25,7 +25,6 @@ profile.out coverage.txt .idea/* .vscode/* -bfe dist/* conf/wasm_plugin diff --git a/Dockerfile b/Dockerfile index b110cbcbb..29da58dfa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,20 +12,134 @@ # See the License for the specific language governing permissions and # limitations under the License. # -FROM --platform=${BUILDPLATFORM} golang:1.17.5-alpine3.15 AS build +FROM --platform=${BUILDPLATFORM} golang:1.22.2-alpine3.19 AS build ARG TARGETARCH ARG TARGETOS -WORKDIR /bfe +WORKDIR /src COPY . . -RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags "-X main.version=`cat VERSION`" -FROM alpine:3.15 AS run -RUN apk update && apk add --no-cache ca-certificates -COPY --from=build /bfe/bfe /bfe/bin/ -COPY conf /bfe/conf/ +RUN set -ex; \ + mkdir -p /out; \ + CGO_ENABLED=0 \ + GOOS=${TARGETOS:-linux} \ + GOARCH=${TARGETARCH:-$(go env GOARCH)} \ + go build -ldflags "-X main.version=$(cat VERSION)" -o /out/bfe + +FROM alpine:3.19 AS confagent +ARG TARGETARCH +ARG CONF_AGENT_VERSION=0.0.2 + +RUN apk add --no-cache ca-certificates wget tar + +RUN set -ex; \ + CONF_AGENT_VERSION_NO_V="${CONF_AGENT_VERSION#v}"; \ + CONF_AGENT_VERSION_TAG="v${CONF_AGENT_VERSION_NO_V}"; \ + ARCH="${TARGETARCH:-}"; \ + if [ -z "${ARCH}" ]; then \ + ARCH="$(uname -m)"; \ + fi; \ + case "${ARCH}" in \ + amd64|x86_64) CONF_AGENT_ARCH="amd64" ;; \ + arm64|aarch64) CONF_AGENT_ARCH="arm64" ;; \ + *) echo "Unsupported architecture: ${ARCH}"; exit 1 ;; \ + esac; \ + CONF_AGENT_URL="https://github.com/bfenetworks/conf-agent/releases/download/${CONF_AGENT_VERSION_TAG}/conf-agent_${CONF_AGENT_VERSION_NO_V}_linux_${CONF_AGENT_ARCH}.tar.gz"; \ + wget -O /tmp/conf-agent.tar.gz "${CONF_AGENT_URL}"; \ + tar -xzf /tmp/conf-agent.tar.gz -C /tmp; \ + mkdir -p /out; \ + mv /tmp/conf-agent /out/conf-agent; \ + if [ -d /tmp/conf ]; then mv /tmp/conf /out/conf-agent-conf; else mkdir -p /out/conf-agent-conf; fi; \ + chmod +x /out/conf-agent + +FROM alpine:3.19 +ARG VARIANT=prod + +RUN set -ex; \ + apk add --no-cache ca-certificates; \ + if [ "${VARIANT}" = "debug" ]; then \ + apk add --no-cache bash curl wget vim; \ + fi + +RUN mkdir -p /home/work/conf-agent/conf \ + && mkdir -p /home/work/conf-agent/log \ + && mkdir -p /home/work/bfe/bin \ + && mkdir -p /home/work/bfe/conf \ + && mkdir -p /home/work/bfe/log + +COPY --from=confagent /out/conf-agent /home/work/conf-agent/conf-agent +COPY --from=confagent /out/conf-agent-conf /home/work/conf-agent/conf +COPY --from=build /out/bfe /home/work/bfe/bin/bfe +COPY --from=build /src/conf /home/work/bfe/conf/ + +# COPY deploy/docker/entrypoint.sh /home/work/entrypoint.sh +# Generate entrypoint.sh inside the image to avoid external file dependency +RUN set -ex; \ + cat > /home/work/entrypoint.sh <<'EOF' +#!/bin/sh +set -eu + +# Log function +log() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" +} + +# Start conf-agent in background +start_conf_agent() { + if [ -f "/home/work/conf-agent/conf-agent" ]; then + log "Starting conf-agent..." + cd /home/work/conf-agent + nohup ./conf-agent -c ./conf > /home/work/conf-agent/log/stdout.log 2>&1 & + CONF_AGENT_PID=$! + log "conf-agent started, PID: $CONF_AGENT_PID" + cd /home/work + else + log "Warning: conf-agent binary not found, skipping startup" + fi +} + +# Start bfe in foreground +start_bfe() { + log "Starting bfe..." + cd /home/work/bfe/bin + exec ./bfe -c ../conf/ -l ../log/ -s +} + +# Signal handler +handle_signal() { + log "Received termination signal, shutting down..." + + # Terminate conf-agent + if [ -n "$CONF_AGENT_PID" ]; then + log "Stopping conf-agent (PID: $CONF_AGENT_PID)..." + kill -TERM "$CONF_AGENT_PID" 2>/dev/null || true + wait "$CONF_AGENT_PID" 2>/dev/null || true + fi + + exit 0 +} + +# Register signal handlers +trap 'handle_signal' TERM INT + +# Main process +log "========================================" +log "BFE Container Startup Script" +log "========================================" + +# 1. Start conf-agent if exists +start_conf_agent + +# Wait for conf-agent initialization +sleep 2 + +# 2. Start bfe in foreground +start_bfe +EOF + +RUN chmod +x /home/work/entrypoint.sh + EXPOSE 8080 8443 8421 -WORKDIR /bfe/bin -ENTRYPOINT ["./bfe"] -CMD ["-c", "../conf/", "-l", "../log"] +WORKDIR /home/work +ENTRYPOINT ["/home/work/entrypoint.sh"] diff --git a/Makefile b/Makefile index 4c7ec07b4..983b3bae1 100644 --- a/Makefile +++ b/Makefile @@ -138,12 +138,120 @@ license-check: license-fix: $(LICENSEEYE) header fix -# make docker + +# Docker image build targets +BFE_IMAGE_NAME ?= bfe +# conf-agent version used in Docker image build. +# Default: 0.0.2 +# Override example: make docker CONF_AGENT_VERSION=0.0.2 +CONF_AGENT_VERSION ?= 0.0.2 +NO_CACHE ?= false + +# Optional cleanup controls +# - CLEAN_DANGLING=true will remove dangling images (":") after build. +# - CLEAN_BUILDKIT_CACHE=true will prune build cache (can slow down next builds). +CLEAN_DANGLING ?= false +CLEAN_BUILDKIT_CACHE ?= false + +# Optional buildx (multi-arch) settings +PLATFORMS ?= linux/amd64,linux/arm64 +BUILDER_NAME ?= bfe-builder + +# buildx helpers +# - make docker (local build) does NOT require buildx. +# - make docker-push (multi-arch push) requires buildx and will auto-init a builder. +buildx-check: + @docker buildx version >/dev/null 2>&1 || ( \ + echo "Error: docker buildx is not available."; \ + echo "- If you use Docker Desktop: update/enable BuildKit/buildx."; \ + echo "- If you use docker-ce: install the buildx plugin."; \ + exit 1; \ + ) + +buildx-init: buildx-check + @docker buildx inspect $(BUILDER_NAME) >/dev/null 2>&1 || docker buildx create --name $(BUILDER_NAME) --driver docker-container --use + @docker buildx use $(BUILDER_NAME) + @docker buildx inspect --bootstrap >/dev/null 2>&1 || true + +# make docker: Build BFE docker images (prod + debug) docker: + @echo "Building BFE docker images (prod + debug)..." + @NORM_BFE_VERSION=$$(echo "$(BFE_VERSION)" | sed 's/^v*/v/'); \ + NORM_CONF_VERSION=$$(echo "$(CONF_AGENT_VERSION)" | sed 's/^v*/v/'); \ + echo "BFE version: $$NORM_BFE_VERSION"; \ + echo "conf-agent version: $$NORM_CONF_VERSION"; \ + echo "Step 1/2: build prod image"; \ docker build \ - -t bfe:$(BFE_VERSION) \ + $$(if [ "$(NO_CACHE)" = "true" ]; then echo "--no-cache"; fi) \ + --build-arg VARIANT=prod \ + --build-arg CONF_AGENT_VERSION=$$NORM_CONF_VERSION \ + -t $(BFE_IMAGE_NAME):$$NORM_BFE_VERSION \ + -t $(BFE_IMAGE_NAME):latest \ + -f Dockerfile \ + .; \ + echo "Step 2/2: build debug image"; \ + docker build \ + $$(if [ "$(NO_CACHE)" = "true" ]; then echo "--no-cache"; fi) \ + --build-arg VARIANT=debug \ + --build-arg CONF_AGENT_VERSION=$$NORM_CONF_VERSION \ + -t $(BFE_IMAGE_NAME):$$NORM_BFE_VERSION-debug \ -f Dockerfile \ . + @$(MAKE) docker-prune + +# docker-prune: optional post-build cleanup (safe-by-default) +docker-prune: + @if [ "$(CLEAN_DANGLING)" = "true" ]; then \ + echo "Pruning dangling images ()..."; \ + docker image prune -f; \ + fi + @if [ "$(CLEAN_BUILDKIT_CACHE)" = "true" ]; then \ + echo "Pruning build cache (BuildKit)..."; \ + docker builder prune -f; \ + fi + +# make docker-push: Build & push multi-arch images using buildx (REGISTRY is required) +# Usage: make docker-push REGISTRY=ghcr.io/your-org +docker-push: + @if [ -z "$(REGISTRY)" ]; then \ + echo "Error: REGISTRY is required"; \ + echo "Usage: make docker-push REGISTRY=ghcr.io/your-org"; \ + exit 1; \ + fi + @echo "Building and pushing multi-arch images via buildx..." + @echo "Platforms: $(PLATFORMS)" + @$(MAKE) buildx-init + @NORM_BFE_VERSION=$$(echo "$(BFE_VERSION)" | sed 's/^v*/v/'); \ + NORM_CONF_VERSION=$$(echo "$(CONF_AGENT_VERSION)" | sed 's/^v*/v/'); \ + NO_CACHE_OPT=$$(if [ "$(NO_CACHE)" = "true" ]; then echo "--no-cache"; fi); \ + echo "BFE version: $$NORM_BFE_VERSION"; \ + echo "conf-agent version: $$NORM_CONF_VERSION"; \ + echo "Step 1/2: build+push prod (multi-arch)"; \ + docker buildx build \ + --platform $(PLATFORMS) \ + $$NO_CACHE_OPT \ + --build-arg VARIANT=prod \ + --build-arg CONF_AGENT_VERSION=$$NORM_CONF_VERSION \ + -t $(REGISTRY)/$(BFE_IMAGE_NAME):$$NORM_BFE_VERSION \ + -t $(REGISTRY)/$(BFE_IMAGE_NAME):latest \ + -f Dockerfile \ + --push \ + .; \ + echo "Step 2/2: build+push debug (multi-arch)"; \ + docker buildx build \ + --platform $(PLATFORMS) \ + $$NO_CACHE_OPT \ + --build-arg VARIANT=debug \ + --build-arg CONF_AGENT_VERSION=$$NORM_CONF_VERSION \ + -t $(REGISTRY)/$(BFE_IMAGE_NAME):$$NORM_BFE_VERSION-debug \ + -f Dockerfile \ + --push \ + .; \ + echo "Pushed multi-arch:"; \ + echo " - $(REGISTRY)/$(BFE_IMAGE_NAME):$$NORM_BFE_VERSION"; \ + echo " - $(REGISTRY)/$(BFE_IMAGE_NAME):$$NORM_BFE_VERSION-debug"; \ + echo " - $(REGISTRY)/$(BFE_IMAGE_NAME):latest (prod)" + @$(MAKE) docker-prune # make clean clean: @@ -153,4 +261,4 @@ clean: rm -rf $(GOPATH)/pkg/linux_amd64 # avoid filename conflict and speed up build -.PHONY: all prepare compile test package clean build +.PHONY: all prepare compile test package clean build docker docker-push docker-prune buildx-check buildx-init diff --git a/README-CN.md b/README-CN.md index cecb0f823..be83630a8 100644 --- a/README-CN.md +++ b/README-CN.md @@ -50,6 +50,7 @@ BFE的架构说明见[概览](docs/zh_cn/introduction/overview.md)文档 - 数据平面:BFE核心转发引擎的[编译及运行](docs/zh_cn/installation/install_from_source.md) - 控制平面:请参考控制平面的[部署说明](https://github.com/bfenetworks/api-server/blob/develop/docs/zh_cn/deploy.md) +- Kubernetes 部署示例(kustomize):[examples/kubernetes/README.md](examples/kubernetes/README.md) ## 运行测试 diff --git a/README.md b/README.md index abeb155d6..e177f348f 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Besides, we also implement [BFE Ingress Controller](https://github.com/bfenetwor - Data plane: BFE Server [build and run](docs/en_us/installation/install_from_source.md) - Control plane: English document coming soon. [Chinese version](https://github.com/bfenetworks/api-server/blob/develop/docs/zh_cn/deploy.md) +- Kubernetes example (kustomize): [examples/kubernetes/README.md](examples/kubernetes/README.md) ## Running the tests diff --git a/docs/images/bfe-k8s.png b/docs/images/bfe-k8s.png new file mode 100644 index 0000000000000000000000000000000000000000..fb26b861d1e917c30f98de040c35e19dbb6e1099 GIT binary patch literal 52632 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

ZJC#%GRA7cEr*dl&P=RkQ85dO$?eHL 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 LoYjA>i;Mh!lsf=) literal 0 HcmV?d00001 diff --git a/docs/zh_cn/installation/install_using_docker.md b/docs/zh_cn/installation/install_using_docker.md index edf43b3fd..75ba4e530 100644 --- a/docs/zh_cn/installation/install_using_docker.md +++ b/docs/zh_cn/installation/install_using_docker.md @@ -1,22 +1,60 @@ -# docker安装 +# Docker 安装 -## 安装 && 运行 +本章介绍如何通过 Docker 运行 BFE。 -- 基于示例配置运行BFE: +## 方式一:直接运行已有镜像 + +如果你已经有可用镜像(例如 `bfenetworks/bfe`,或你自己构建并推送到私有仓库的镜像),可以直接运行: ```bash -docker run -p 8080:8080 -p 8443:8443 -p 8421:8421 bfenetworks/bfe +docker run --rm \ + -p 8080:8080 -p 8443:8443 -p 8421:8421 \ + ``` -你可以访问 http://127.0.0.1:8080/ 因为没有匹配的配置,将得到 status code 500 -你可以访问 http://127.0.0.1:8421/ 查看监控信息 +你可以访问: +- http://127.0.0.1:8080/ (如果配置未命中,可能返回 500) +- http://127.0.0.1:8421/monitor (监控信息) + +## 方式二:从源码构建镜像(推荐) -- 自定义配置文件路径 +在仓库根目录执行: ```bash -// 事先准备好你自己的配置放到 (可以参考 配置 章节) /Users/BFE/conf +# 一次构建 prod + debug 两个镜像 +make docker + +# 可选:指定 conf-agent 版本(默认 0.0.2) +make docker CONF_AGENT_VERSION=0.0.2 +``` + +构建后的镜像标签(以 VERSION=1.8.0 为例): +- `bfe:v1.8.0`(prod) +- `bfe:v1.8.0-debug`(debug) +- `bfe:latest`(始终指向 prod) -docker run -p 8080:8080 -p 8443:8443 -p 8421:8421 -v /Users/BFE/Desktop/log:/bfe/log -v /Users/BFE/Desktop/conf:/bfe/conf bfenetworks/bfe +## 自定义配置(挂载本地目录) + +镜像内目录约定: +- BFE 配置目录:`/home/work/bfe/conf` +- BFE 日志目录:`/home/work/bfe/log` +- conf-agent 配置目录:`/home/work/conf-agent/conf` +- conf-agent 日志目录:`/home/work/conf-agent/log` + +示例:挂载你本地准备好的配置与日志目录(按需修改路径): + +```bash +# 事先准备好你自己的配置: +# - /Users/BFE/Desktop/conf/ (BFE 配置目录) +# - /Users/BFE/Desktop/conf-agent/ (conf-agent 配置目录,里面放 conf-agent.toml) +# - /Users/BFE/Desktop/log/ (BFE 日志目录) + +docker run --rm \ + -p 8080:8080 -p 8443:8443 -p 8421:8421 \ + -v /Users/BFE/Desktop/conf:/home/work/bfe/conf \ + -v /Users/BFE/Desktop/log:/home/work/bfe/log \ + -v /Users/BFE/Desktop/conf-agent:/home/work/conf-agent/conf \ + bfe:latest ``` ## 下一步 diff --git a/examples/kubernetes/README.md b/examples/kubernetes/README.md new file mode 100644 index 000000000..675884aa4 --- /dev/null +++ b/examples/kubernetes/README.md @@ -0,0 +1,166 @@ +# BFE Kubernetes 部署示例 + +## 概述 + +![BFE Kubernetes](../../docs/images/bfe-k8s.png) + +本示例在 `bfe-system` 命名空间中演示了若干关键组件及其交互: +- 数据面(bfe 与 conf-agent)负责流量转发与接入控制; +- 控制面(api-server 与 dashboard)负责策略下发与管理; +- 服务发现(service-controller)负责发现并同步后端服务; +- 示例服务 whoami 用于验证路由; +- 组件间通过 Kubernetes Service/DNS 相互通信,如: + - api-server.bfe-system.svc.cluster.local + - mysql.bfe-system.svc.cluster.local + +注意: +- api-server 与 MySQL 提供持久化与控制数据; + - 示例中的 MySQL 会随清单重建并重新初始化 + - 不能直接用于生产环境 + + +主要文件概览: + +| **文件名** | **说明** | +|---|---| +| `namespace.yaml` | 命名空间定义(bfe-system) | +| `kustomization.yaml` | kustomize 资源汇总与启用/禁用项 | +| `bfe-configmap.yaml` | bfe 配置(bfe.conf、conf-agent.toml 等) | +| `bfe-deploy.yaml` | bfe 数据面 Deployment 清单 | +| `api-server-configmap.yaml` | API Server 配置(DB 连接、鉴权示例) | +| `api-server-deploy.yaml` | API Server Deployment 清单 | +| `mysql-deploy.yaml` | MySQL Deployment(示例数据库与存储配置) | +| `service-controller-deploy.yaml` | 服务发现控制器 Deployment 清单 | +| `whoami-deploy.yaml` | 示例测试服务 whoami 的 Deployment 清单 | + +## 前提条件 + +- 版本约束:kubectl 必须支持 -k 参数 + - 建议 kubectl >= 1.20 或任意能执行 `kubectl apply -k .` 的版本。 + +- 集群访问权限:kubectl 能访问目标集群并有权限创建 Namespace、Deployment、Service、ConfigMap、Secret 等资源。 + +- 镜像可拉取:若使用外部镜像仓库,确保集群节点能拉取示例镜像,或在 `*-deploy.yaml` 中替换为可访问的镜像地址。 + +- 可选工具:若 kubectl 无内置 kustomize,请安装 kustomize 或使用带 kustomize 的 kubectl 版本。 + + +## 部署 + +- 部署 bfe 服务(数据面、控制面、服务发现) + +```bash +cd examples/kubernetes +kubectl apply -k . +``` + +- 部署测试服务(验证 bfe 服务启动成功后) + +```bash +cd examples/kubernetes +kubectl apply -f whoami-deploy.yaml +``` + +## 验证 + +- 检查命名空间、Pod 与服务状态: + +```bash +[root@iTM ~]# kubectl get ns bfe-system +NAME STATUS AGE +bfe-system Active 29h + +[root@iTM ~]# kubectl -n bfe-system get pods +NAME READY STATUS RESTARTS AGE +api-server-655fdffbf-hwvgw 1/1 Running 0 29h +bfe-85f4d45ddf-4xwdz 1/1 Running 0 29h +bfe-85f4d45ddf-srnxd 1/1 Running 0 29h +bfe-service-controller-6867d57767-92b5m 1/1 Running 0 29h +mysql-d768d5d4d-fj4j5 1/1 Running 0 29h + +[root@iTM ~]# kubectl -n bfe-system get service +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +api-server NodePort 10.105.122.39 8183:30083/TCP,8284:30786/TCP 29h +bfe NodePort 10.108.55.8 8080:30080/TCP,8443:30443/TCP,8421:30421/TCP 29h +mysql ClusterIP None 3306/TCP 29h +[root@iTM ~]# +``` + +- 登陆 dashboard: + - 浏览器访问 http://{NodeIP}:30083 + - 默认账号/密码:admin/admin + +## 清理 + +- 清理测试服务 + +```bash +cd examples/kubernetes +kubectl delete -f whoami-deploy.yaml +``` + +- 清理 bfe 服务(数据面、控制面、服务发现) + +```bash +cd examples/kubernetes +kubectl delete -k . +``` + +- 按需删除单个资源: + +```bash +# bfe +kubectl -n bfe-system delete -f whoami-deploy.yaml +kubectl -n bfe-system delete -f service-controller-deploy.yaml +kubectl -n bfe-system delete -f api-server-deploy.yaml +kubectl -n bfe-system delete -f api-server-configmap.yaml +kubectl -n bfe-system delete -f mysql-deploy.yaml +kubectl -n bfe-system delete -f bfe-deploy.yaml +kubectl -n bfe-system delete -f bfe-configmap.yaml + +# whoami +kubectl delete -f whoami-deploy.yaml +``` + +## 重启 + +示例中 MySQL Pod 每次重启时会重新初始化并丢失已有数据, +若希望在仅更新其它 Pod(例如修改配置后 `kubectl apply -k .`)时保留已有数据库, +可在 `examples/kubernetes/kustomization.yaml` 中临时注释或移除 `mysql-deploy.yaml`。 +然后再执行 `kubectl apply -k .` 进行重启。 + +## 关键配置 + +### 数据面 bfe + +- 镜像:bfe-deploy.yaml 中 spec.template.spec.containers[].image,请替换为可信仓库地址并使用带标签的镜像。 + +- 配置挂载:bfe-configmap.yaml 包含 bfe.conf 与 conf-agent.toml,确认 volumeMounts 的容器路径与配置文件中引用路径一致。 + +- 监控端口:示例暴露 8421,用于健康与监控接口,可通过 Service 或 kubectl port-forward 验证。 + +- 服务端口:示例暴露 8080,NodePort 30080,用于对外提供服务。 + +### 控制面 api-server 与 mysql + +- 数据库连接:api-server-configmap.yaml 中 DB_HOST / DB_PORT / DB_USER / DB_PASSWORD(示例为明文)。 + - 在生产环境请改为 Kubernetes Secret。 + +- 鉴权 Token:示例 Token 已经预置在 `api-server-configmap.yaml` 与 `service-controller-deploy.yaml` 中。 + - 生产环境必须使用 Secret、短期或动态凭证。 + - 生产环境需要预先在控制面 [dashboard](https://github.com/bfenetworks/dashboard/blob/develop/README.md) 中创建 Token。 + +- MySQL 存储:mysql-deploy.yaml 为方便一键部署快速搭建,使用了 emptyDir 卷,重启 pod 后数据会丢失,不适用于生产环境。 + - 生产必须使用 PV/PVC、指定 StorageClass,并配置备份策略。 + + +- 更多请参见:[dashboard](https://github.com/bfenetworks/dashboard/blob/develop/README.md) + +### 服务发现 service-controller 与 whoami + +- 发现规则:service-controller-deploy.yaml 中 args 或 env 定义发现策略、标签选择器或 API Server 地址,按需调整以匹配你的服务标签/注解。 + +- whoami 端口:whoami-deploy.yaml 中 spec.template.spec.containers[].ports 必须与对应 Service 的端口一致。 + +- 更多请参见:[service-controller](https://github.com/bfenetworks/service-controller/blob/main/README.md) + diff --git a/examples/kubernetes/api-server-configmap.yaml b/examples/kubernetes/api-server-configmap.yaml new file mode 100644 index 000000000..030a26473 --- /dev/null +++ b/examples/kubernetes/api-server-configmap.yaml @@ -0,0 +1,86 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: api-server-conf +data: + api_server.toml: |- + # --------------------------------- + # API Server Config + [Server] + # server port + ServerPort = 8183 + # server graceful exit timeout + GracefulTimeOutInMs = 5000 + # monitor port, don't start monitor server if less than 0 + MonitorPort = 8284 + + # --------------------------------- + # Logger Config + # access log config + [Loggers.access] + LogName = "access" + LogLevel = "INFO" + RotateWhen = "MIDNIGHT" + BackupCount = 1 + Format = "[%D %T] [%L] [%S] %M" + StdOut = false + + # sql log, you can skip this config if RunTime.RecordSQL is false + [Loggers.sql] + LogName = "sql" + LogLevel = "INFO" + RotateWhen = "MIDNIGHT" + BackupCount = 1 + Format = "[%D %T] %M" + StdOut = false + + # --------------------------------- + # Database Config + # see https://github.com/go-sql-driver/mysql/blob/master/dsn.go#L37 + [Databases.bfe_db] + DBName = "open_bfe" + # MySQL service in the same namespace + Addr = "mysql.bfe-system.svc.cluster.local:3306" + Net = "tcp" + User = "root" + Passwd = "bfe123456" + MultiStatements = true + MaxAllowedPacket = 67108864 + ParseTime = true + AllowNativePasswords = true + + Driver = "mysql" + MaxOpenConns = 500 + MaxIdleConns = 100 + ConnMaxIdleTimeInMs = 500000 + ConnMaxLifetimeInMs = 5000000 + + + # --------------------------------- + # Dependence Config + [Depends] + # NavTreeFile path + NavTreeFile = "${conf_dir}/nav_tree.toml" + # i18n conf dir path + I18nDir = "${conf_dir}/i18n" + # dashboard icon + UIIcon = "https://raw.githubusercontent.com/bfenetworks/bfe/develop/docs/images/logo/icon/color/bfe-icon-color.svg" + # dashboard logo + UILogo = "https://raw.githubusercontent.com/bfenetworks/bfe/develop/docs/images/logo/horizontal/color/bfe-horizontal-color.png" + + + # --------------------------------- + # Runtime Config + [RunTime] + # you can use "Skip {role_name} as authorization header to access api server if open this optional + # eg: Headers[Authorization] = "Skip System" + # don't open it on production environment + SkipTokenValidate = false + # sql will be record to log file when this option be opend + RecordSQL = false + # how long use must login again + SessionExpireInDay = 10 + # static file path, when dynamic router not be matched, static file will be return if found + StaticFilePath = "./static" + # debug info will be add to response when this option be opend + Debug = false diff --git a/examples/kubernetes/api-server-deploy.yaml b/examples/kubernetes/api-server-deploy.yaml new file mode 100644 index 000000000..21e21722e --- /dev/null +++ b/examples/kubernetes/api-server-deploy.yaml @@ -0,0 +1,83 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: api-server +spec: + replicas: 1 + selector: + matchLabels: + app: api-server + template: + metadata: + labels: + app: api-server + spec: + securityContext: + runAsUser: 10001 + runAsGroup: 10001 + fsGroup: 10001 + initContainers: + # Wait for MySQL init job to complete + - name: wait-for-db-init + image: busybox:1.36 + command: + - sh + - -c + - | + echo "Waiting for database initialization..." + until nc -z mysql.bfe-system.svc.cluster.local 3306; do + echo "MySQL is not ready, waiting..." + sleep 5 + done + echo "MySQL is ready, waiting for init job to complete..." + sleep 15 + echo "Database should be initialized now!" + containers: + - name: api-server + # Update this image to your registry. + image: ghcr.io/bfenetworks/api-server:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8183 + - name: monitor + containerPort: 8284 + volumeMounts: + - name: conf + mountPath: /home/work/api-server/conf/api_server.toml + subPath: api_server.toml + readOnly: true + readinessProbe: + httpGet: + path: /monitor + port: 8284 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /monitor + port: 8284 + initialDelaySeconds: 15 + periodSeconds: 20 + volumes: + - name: conf + configMap: + name: api-server-conf + +--- +apiVersion: v1 +kind: Service +metadata: + name: api-server +spec: + selector: + app: api-server + ports: + - name: http + port: 8183 + targetPort: 8183 + nodePort: 30083 + - name: monitor + port: 8284 + targetPort: 8284 + type: NodePort diff --git a/examples/kubernetes/bfe-configmap.yaml b/examples/kubernetes/bfe-configmap.yaml new file mode 100644 index 000000000..3f2b4d575 --- /dev/null +++ b/examples/kubernetes/bfe-configmap.yaml @@ -0,0 +1,237 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: bfe-config +data: + bfe.conf: | + [Server] + # listen port for http request + HttpPort = 8080 + # listen port for https request + HttpsPort = 8443 + # listen port for monitor request + MonitorPort = 8421 + # if false, disable monitor server + MonitorEnabled = true + + # max number of CPUs to use (0 to use all CPUs) + MaxCpus = 0 + + # type of layer-4 load balancer (PROXY/NONE), default NONE + Layer4LoadBalancer = "" + + # tls handshake timeout, in seconds + TlsHandshakeTimeout = 30 + + # read timeout, in seconds + ClientReadTimeout = 60 + + # write timeout, in seconds + ClientWriteTimeout = 60 + + # if false, client connection is shutdown disregard of http headers + KeepAliveEnabled = true + + # timeout for graceful shutdown (maximum 300 sec) + GracefulShutdownTimeout = 10 + + # max header length in bytes in request + MaxHeaderBytes = 1048576 + + # max URI(in header) length in bytes in request + MaxHeaderUriBytes = 8192 + + # server_data_conf related confs + HostRuleConf = server_data_conf/host_rule.data + VipRuleConf = server_data_conf/vip_rule.data + RouteRuleConf = server_data_conf/route_rule.data + ClusterConf = server_data_conf/cluster_conf.data + NameConf = server_data_conf/name_conf.data + + # gslb related confs + ClusterTableConf = cluster_conf/cluster_table.data + GslbConf = cluster_conf/gslb.data + + Modules = mod_trust_clientip + # Modules = mod_tcp_keepalive + Modules = mod_block + Modules = mod_header + Modules = mod_rewrite + Modules = mod_redirect + Modules = mod_logid + Modules = mod_tag + Modules = mod_trace + #Modules = mod_userid + #Modules = mod_key_log + Modules = mod_access + Modules = mod_prison + #Modules = mod_auth_request + # Modules = mod_cors + Modules = mod_wasm + + Modules = mod_unified_waf + Modules = mod_ai_token_auth + Modules = mod_body_process + + # interval for get diff of proxy-state + MonitorInterval = 20 + + DebugServHttp = false + DebugBfeRoute = false + DebugBal = false + DebugHealthCheck = false + + [HttpsBasic] + # cert conf for https + ServerCertConf = tls_conf/server_cert_conf.data + + # tls rule for https + TlsRuleConf = tls_conf/tls_rule_conf.data + + # supported cipherSuites preference settings + + # ciphersuites implemented in golang + # TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 + # TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + # TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + # TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + # TLS_ECDHE_RSA_WITH_RC4_128_SHA + # TLS_ECDHE_ECDSA_WITH_RC4_128_SHA + # TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA + # TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA + # TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA + # TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA + # TLS_RSA_WITH_RC4_128_SHA + # TLS_RSA_WITH_AES_128_CBC_SHA + # TLS_RSA_WITH_AES_256_CBC_SHA + # TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA + # TLS_RSA_WITH_3DES_EDE_CBC_SHA + # + # Note: + # -. Do not use cipher suite with 3DES which is insecure now + # and with poor performance + # + # -. Equivalent cipher suites (cipher suites with same priority in server side): + # CipherSuites=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256|TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 + # CipherSuites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256|TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + # + CipherSuites=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256|TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 + CipherSuites=TLS_ECDHE_RSA_WITH_RC4_128_SHA + CipherSuites=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA + CipherSuites=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA + CipherSuites=TLS_RSA_WITH_RC4_128_SHA + CipherSuites=TLS_RSA_WITH_AES_128_CBC_SHA + CipherSuites=TLS_RSA_WITH_AES_256_CBC_SHA + + # supported curve preference settings + # + # curves implemented in golang: + # CurveP256 + # CurveP384 + # CurveP521 + # + # Note: + # - Do not use CurveP384/CurveP521 which is with poor performance + # + CurvePreferences=CurveP256 + + # support Sslv2 ClientHello for compatible with ancient + # TLS capable clients (mozilla 5, java 5/6, openssl 0.9.8 etc) + EnableSslv2ClientHello = true + + # client ca certificates base directory + # Note: filename suffix for ca certificate file should be ".crt", eg. example_ca_bundle.crt + ClientCABaseDir = tls_conf/client_ca + + # client certificate crl base directory + # Note: filename suffix for crl file should be ".crl", eg. example_ca_bundle.crl + ClientCRLBaseDir = tls_conf/client_crl + + [SessionCache] + # disable tls session cache or not + SessionCacheDisabled = true + + # tcp address of redis server + Servers = "example.redis.cluster" + + # prefix for cache key + KeyPrefix = "bfe" + + # connection params (ms) + ConnectTimeout = 50 + ReadTimeout = 50 + WriteTimeout = 50 + + # max idle connections in connection pool + MaxIdle = 20 + + # expire time for tls session state (second) + SessionExpire = 3600 + + [SessionTicket] + # disable tls session ticket or not + SessionTicketsDisabled = true + # session ticket key + SessionTicketKeyFile = tls_conf/session_ticket_key.data + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: conf-agent-config +data: + conf-agent.toml: | + [Logger] + LogDir = "./log/" + LogName = "conf_agent" + LogLevel = "INFO" + RotateWhen = "MIDNIGHT" + BackupCount = 2 + Format = "[%D %T] [%L] [%S] %M" + StdOut = false + + [Basic] + BFECluster = "BFE-demo.szyf" + BFEConfDir = "/home/work/bfe/conf" + BFEMonitorPort = 8421 + BFEReloadTimeoutMs = 1500 + + ReloadIntervalMs = 5000 + + ConfServer = "http://api-server.bfe-system.svc.cluster.local:8183" + # Token 获取参考 https://github.com/bfenetworks/dashboard/blob/develop/docs/zh-cn/user-guide/system-view/user-management.md#token%E7%AE%A1%E7%90%86 + ConfTaskHeaders = {"Authorization" = "Token c5QFb-MrlKktJbOid2M3"} + ConfTaskTimeoutMs = 1500 + + ExtraFileServer = "http://api-server.bfe-system.svc.cluster.local:8183/inner-api/v1/configs/extra_files/" + # Token 获取参考 https://github.com/bfenetworks/dashboard/blob/develop/docs/zh-cn/user-guide/system-view/user-management.md#token%E7%AE%A1%E7%90%86 + ExtraFileTaskHeaders = {"Authorization" = "Token c5QFb-MrlKktJbOid2M3"} + ExtraFileTaskTimeoutMs = 1500 + + + # reloader for server_data_config files, detail see https://www.bfe-networks.net/en_us/configuration/server_data_conf/host_rule.data/ + [Reloaders.server_data_conf] + CopyFiles = ["cluster_conf.data", "host_rule.data", "name_conf.data", "route_rule.data", "vip_rule.data"] + [[Reloaders.server_data_conf.MultiKeyFileTasks]] + ConfAPI = "/inner-api/v1/configs/tls_conf/server_data_conf" + Key2ConfFile = {"HostTable" = "host_rule.data", "RouteTable" = "route_rule.data", "ClusterConf" = "cluster_conf.data"} + + # # reloader for cluster_conf files, detail see https://www.bfe-networks.net/en_us/configuration/cluster_conf/gslb.data/ + [Reloaders.cluster_conf] + BFEReloadAPI = "/reload/gslb_data_conf" + CopyFiles = ["cluster_table.data", "gslb.data"] + [[Reloaders.cluster_conf.NormalFileTasks]] + ConfAPI = "/inner-api/v1/configs/gslb_data/cluster_table" + ConfFileName = "cluster_table.data" + [[Reloaders.cluster_conf.NormalFileTasks]] + ConfAPI = "/inner-api/v1/configs/gslb_data/gslb" + ConfFileName = "gslb.data" + + # # reloader for tls_conf files, detail see https://www.bfe-networks.net/en_us/configuration/tls_conf/server_cert_conf.data/ + #[Reloaders.tls_conf] + #CopyFiles = ["client_ca", "client_crl", "server_cert_conf.data", "session_ticket_key.data", "tls_rule_conf.data"] + #[[Reloaders.tls_conf.ExtraFileTasks]] + #ConfAPI = "/inner-api/v1/configs/protocol/server_cert_conf" + #ConfFileName = "server_cert_conf.data" + #ExtraFileJSONPaths = ["$.Config.CertConf.*.ServerCertFile", "$.Config.CertConf.*.ServerKeyFile"] + diff --git a/examples/kubernetes/bfe-deploy.yaml b/examples/kubernetes/bfe-deploy.yaml new file mode 100644 index 000000000..65f81b0b7 --- /dev/null +++ b/examples/kubernetes/bfe-deploy.yaml @@ -0,0 +1,95 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bfe + namespace: bfe-system + labels: + app: bfe +spec: + replicas: 2 + selector: + matchLabels: + app: bfe + template: + metadata: + labels: + app: bfe + spec: + containers: + - name: bfe + image: ghcr.io/bfenetworks/bfe:v1.8.0-debug # 替换为实际镜像 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + protocol: TCP + - name: https + containerPort: 8443 + protocol: TCP + - name: monitor + containerPort: 8421 + protocol: TCP + volumeMounts: + - name: bfe-conf + mountPath: /home/work/bfe/conf/bfe.conf + subPath: bfe.conf + readOnly: false + - name: conf-agent-conf + mountPath: /home/work/conf-agent/conf/conf-agent.toml + subPath: conf-agent.toml + readOnly: false + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 1000m + memory: 512Mi + livenessProbe: + httpGet: + path: /monitor + port: 8421 + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /monitor + port: 8421 + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: bfe-conf + configMap: + name: bfe-config + - name: conf-agent-conf + configMap: + name: conf-agent-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: bfe + namespace: bfe-system + labels: + app: bfe +spec: + selector: + app: bfe + ports: + - name: http + port: 8080 + targetPort: 8080 + protocol: TCP + nodePort: 30080 + - name: https + port: 8443 + targetPort: 8443 + protocol: TCP + nodePort: 30443 + - name: monitor + port: 8421 + targetPort: 8421 + protocol: TCP + nodePort: 30421 + type: NodePort diff --git a/examples/kubernetes/kustomization.yaml b/examples/kubernetes/kustomization.yaml new file mode 100644 index 000000000..560cbafb7 --- /dev/null +++ b/examples/kubernetes/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: bfe-system + +resources: + - namespace.yaml + - service-controller-deploy.yaml + - api-server-configmap.yaml + - api-server-deploy.yaml + - bfe-configmap.yaml + - bfe-deploy.yaml + - mysql-deploy.yaml diff --git a/examples/kubernetes/mysql-deploy.yaml b/examples/kubernetes/mysql-deploy.yaml new file mode 100644 index 000000000..a01449c1f --- /dev/null +++ b/examples/kubernetes/mysql-deploy.yaml @@ -0,0 +1,427 @@ +# MySQL Database Components for API Server +# This file contains all MySQL-related Kubernetes resources: +# - Secret (credentials) +# - emptyDir (ephemeral storage, for test/demo only) +# - Service (network) +# - Deployment (MySQL server) +# - ConfigMap (db_ddl.sql initialization script) +# - Job (database initialization) + +--- +apiVersion: v1 +kind: Secret +metadata: + name: mysql-secret +type: Opaque +stringData: + mysql-root-password: "bfe123456" + mysql-database: "open_bfe" + + +--- +apiVersion: v1 +kind: Service +metadata: + name: mysql +spec: + selector: + app: mysql + ports: + - name: mysql + port: 3306 + targetPort: 3306 + clusterIP: None + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql +spec: + replicas: 1 + selector: + matchLabels: + app: mysql + strategy: + type: Recreate + template: + metadata: + labels: + app: mysql + spec: + containers: + - name: mysql + image: mysql:8.0 + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-secret + key: mysql-root-password + - name: MYSQL_DATABASE + valueFrom: + secretKeyRef: + name: mysql-secret + key: mysql-database + ports: + - name: mysql + containerPort: 3306 + volumeMounts: + - name: mysql-data + mountPath: /var/lib/mysql + livenessProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + - -uroot + - -p$(MYSQL_ROOT_PASSWORD) + initialDelaySeconds: 60 + timeoutSeconds: 5 + failureThreshold: 5 + periodSeconds: 10 + readinessProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + - -uroot + - -p$(MYSQL_ROOT_PASSWORD) + initialDelaySeconds: 60 + timeoutSeconds: 5 + failureThreshold: 5 + periodSeconds: 10 + volumes: + - name: mysql-data + emptyDir: {} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: mysql-init +data: + db_ddl.sql: | + DROP DATABASE IF EXISTS `open_bfe`; + CREATE DATABASE open_bfe; + + USE open_bfe; + + -- create bfe_clusters + DROP TABLE IF EXISTS `bfe_clusters`; + CREATE TABLE `bfe_clusters` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `pool_name` varchar(255) NOT NULL DEFAULT '', + `capacity` bigint(20) NOT NULL, + `enabled` tinyint(1) NOT NULL DEFAULT '1', + `gtc_enabled` tinyint(1) NOT NULL DEFAULT '1', + `gtc_manual_enabled` tinyint(1) NOT NULL DEFAULT '1', + `exempt_traffic_check` tinyint(1) NOT NULL DEFAULT '0', + `created_at` datetime NOT NULL, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `name_uni` (`name`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + -- create products + DROP TABLE IF EXISTS `products`; + CREATE TABLE `products` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL , + `mail_list` varchar(4096) NOT NULL , + `contact_person` varchar(4096) NOT NULL , + `sms_list` varchar(4096) NOT NULL DEFAULT "no sms" , + `description` varchar(1024) NOT NULL DEFAULT "no desc" , + `created_at` datetime NOT NULL , + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP , + PRIMARY KEY (`id`), + UNIQUE KEY `name_uni` (`name`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + -- create domains + DROP TABLE IF EXISTS `domains`; + CREATE TABLE `domains` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `product_id` bigint(20) NOT NULL, + `type` int(11) NOT NULL, + `using_advanced_redirect` tinyint(1) NOT NULL DEFAULT 0, + `using_advanced_hsts` tinyint(1) NOT NULL DEFAULT 0, + `created_at` datetime NOT NULL, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `name_uni` (`name`), + INDEX `product_id` (`product_id`), + INDEX `type` (`type`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + -- create clusters + DROP TABLE IF EXISTS `clusters`; + CREATE TABLE `clusters` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `description` varchar(1024) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT "no desc", + `product_id` bigint(20) NOT NULL, + `max_idle_conn_per_host` smallint(6) NOT NULL DEFAULT '2', + `timeout_conn_serv` int(11) NOT NULL DEFAULT '50000', + `timeout_response_header` int(11) NOT NULL DEFAULT '50000', + `timeout_readbody_client` int(11) NOT NULL DEFAULT '30000', + `timeout_read_client_again` int(11) NOT NULL DEFAULT '30000', + `timeout_write_client` int(11) NOT NULL DEFAULT '60000', + `healthcheck_schem` varchar(16) NOT NULL DEFAULT 'http', + `healthcheck_interval` int(11) NOT NULL DEFAULT '1000', + `healthcheck_failnum` int(11) NOT NULL DEFAULT '10', + `healthcheck_host` varchar(255) NOT NULL, + `healthcheck_uri` varchar(255) NOT NULL, + `healthcheck_statuscode` int(11) NOT NULL DEFAULT '200', + `clientip_carry` tinyint(4) NOT NULL DEFAULT '0', + `port_carry` tinyint(1) NOT NULL DEFAULT '0', + `max_retry_in_cluster` tinyint(4) NOT NULL DEFAULT '3', + `max_retry_cross_cluster` tinyint(4) NOT NULL DEFAULT '0', + `ready` tinyint(1) NOT NULL DEFAULT '1', + `hash_strategy` int NOT NULL DEFAULT '0', + `cookie_key` varchar(255) NOT NULL DEFAULT 'BAIDUID', + `hash_header` varchar(255) NOT NULL DEFAULT 'Cookie:BAIDUID', + `session_sticky` tinyint(1) NOT NULL DEFAULT '0', + `req_write_buffer_size` int(11) NOT NULL DEFAULT '512', + `req_flush_interval` int(11) NOT NULL DEFAULT '0', + `res_flush_interval` int(11) NOT NULL DEFAULT '20', + `cancel_on_client_close` tinyint(1) NOT NULL DEFAULT '0', + `failure_status` tinyint(1) NOT NULL DEFAULT '0', + `max_conns_per_host` int(11) NOT NULL DEFAULT '0', + `created_at` datetime NOT NULL, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `name_index` (`name`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + -- create lb_matrices + DROP TABLE IF EXISTS `lb_matrices`; + CREATE TABLE `lb_matrices` ( + `cluster_id` bigint(20) NOT NULL AUTO_INCREMENT, + `lb_matrix` varchar(8192) NOT NULL, + `product_id` bigint(20) NOT NULL, + `created_at` datetime NOT NULL, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`cluster_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + -- create sub_clusters + DROP TABLE IF EXISTS `sub_clusters`; + CREATE TABLE `sub_clusters` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `cluster_id` bigint(20) NOT NULL, + `product_id` bigint(20) NOT NULL, + `description` varchar(1024) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT "no desc", + `bns_name_id` bigint(20) NOT NULL, + `enabled` tinyint(1) NOT NULL DEFAULT '1', + `created_at` datetime NOT NULL, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `name_product_index` (`name`, `product_id`), + INDEX `cluster_id` (`cluster_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + -- create pools + DROP TABLE IF EXISTS `pools`; + CREATE TABLE `pools` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `product_id` bigint(20) NOT NULL DEFAULT 0, + `ready` boolean NOT NULL DEFAULT 1, + `instance_detail` mediumtext, + `type` tinyint(4) NOT NULL DEFAULT 1, + `tag` tinyint(4) NOT NULL DEFAULT 0, + `created_at` datetime NOT NULL, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `name_uni` (`name`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + -- create route_basic_rules + DROP TABLE IF EXISTS `route_basic_rules`; + CREATE TABLE `route_basic_rules` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `description` varchar(1024) NOT NULL DEFAULT '', + `product_id` bigint(20) NOT NULL, + `host_names` text NOT NULL, + `paths` text NOT NULL, + `cluster_id` bigint(20) NOT NULL, + `created_at` datetime NOT NULL, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `product_id` (`product_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + -- create route_advance_rules + DROP TABLE IF EXISTS `route_advance_rules`; + CREATE TABLE `route_advance_rules` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `description` varchar(1024) NOT NULL DEFAULT '', + `product_id` bigint(20) NOT NULL, + `expression` varchar(4096) binary NOT NULL, + `cluster_id` bigint(20) NOT NULL, + `created_at` datetime NOT NULL, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `product_id` (`product_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + -- create forward_cases + DROP TABLE IF EXISTS `route_cases`; + CREATE TABLE `route_cases` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `description` varchar(1024) NOT NULL DEFAULT '', + `product_id` bigint(20) NOT NULL, + `url` varchar(4096) NOT NULL, + `method` varchar(255) NOT NULL DEFAULT "", + `protocol` varchar(255) NOT NULL DEFAULT "", + `header` varchar(4096) NOT NULL, + `body` varchar(4096) NOT NULL DEFAULT "", + `expect_cluster` varchar(255) NOT NULL, + `created_at` datetime NOT NULL, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `product_id` (`product_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + -- create certificates + DROP TABLE IF EXISTS `certificates`; + CREATE TABLE `certificates` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `cert_name` varchar(255) NOT NULL, + `description` varchar(1024) NOT NULL DEFAULT 'no desc', + `is_default` tinyint(1) NOT NULL DEFAULT '0', + `expired_date` varchar(255) NOT NULL, + `cert_file_name` varchar(255) NOT NULL, + `cert_file_path` varchar(255) NOT NULL, + `key_file_name` varchar(255) NOT NULL, + `key_file_path` varchar(255) NOT NULL, + + `created_at` datetime NOT NULL, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`cert_name`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + -- create extra_files + DROP TABLE IF EXISTS `extra_files`; + CREATE TABLE `extra_files` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `product_id` bigint(20) NOT NULL DEFAULT 0, + `description` varchar(1024) NOT NULL DEFAULT '', + `md5` varchar(64) NOT NULL, + `content` mediumtext, + `created_at` datetime NOT NULL , + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP , + PRIMARY KEY (`id`), + UNIQUE KEY `name_product` (`name`, `product_id`), + INDEX `product_id` (`product_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + DROP TABLE IF EXISTS `config_versions`; + CREATE TABLE `config_versions` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `data_sign` varchar(255) NOT NULL, + `version` varchar(255) NOT NULL, + `created_at` datetime NOT NULL, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + + -- create users + DROP TABLE IF EXISTS `users`; + CREATE TABLE `users` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `type` tinyint(1) NOT NULL DEFAULT '0', + `password` varchar(255) NOT NULL DEFAULT '', + `ticket` varchar(20) NOT NULL DEFAULT '', + `ticket_created_at` datetime NOT NULL DEFAULT '0000-01-01 00:00:00', + `scopes` varchar(2048) NOT NULL DEFAULT '', + `created_at` datetime NOT NULL, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `name_uni` (`name`, `type`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + -- create user_products + DROP TABLE IF EXISTS `user_products`; + CREATE TABLE `user_products` ( + `user_id` bigint(20) NOT NULL, + `product_id` bigint(20) NOT NULL, + `created_at` datetime NOT NULL, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`user_id`, `product_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + + insert into users (id, name, password, scopes, created_at) values(1, 'admin', 'admin', 'System', now()); + insert into users (id, name, type, password, ticket, ticket_created_at, scopes, created_at) values (2, 'demo', 1, '', 'eT5QWkLhQmp6lO4NWxAc', now(), 'Product', now()); + insert into users (id, name, type, password, ticket, ticket_created_at, scopes, created_at) values (3, 'conf-agent', 1, '', 'c5QFb-MrlKktJbOid2M3', now(), 'System', now()); + + insert into products (id, name, `description`, mail_list, contact_person, created_at) values (1, 'BFE', 'Build-in Product, User by System Manager', 'bfe@cncf.com', 'bfe', now()); + insert into products (id, name, `description`, mail_list, contact_person, created_at) values (2, 'demo', 'k8s demo', '', '', now()); + insert into user_products (user_id, product_id, created_at, updated_at) values (2, 2, now(), now()); + +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: mysql-init-job +spec: + ttlSecondsAfterFinished: 100 + template: + metadata: + labels: + app: mysql-init + spec: + restartPolicy: OnFailure + initContainers: + # Wait for MySQL to be ready + - name: wait-for-mysql + image: busybox:1.36 + command: + - sh + - -c + - | + echo "Waiting for MySQL to be ready..." + until nc -z mysql.bfe-system.svc.cluster.local 3306; do + echo "MySQL is not ready yet, sleeping 5s..." + sleep 5 + done + echo "MySQL is ready!" + sleep 10 + containers: + - name: mysql-init + image: mysql:8.0 + env: + - name: MYSQL_PWD + valueFrom: + secretKeyRef: + name: mysql-secret + key: mysql-root-password + command: + - sh + - -c + - | + echo "Starting database initialization..." + mysql -h mysql.bfe-system.svc.cluster.local -uroot < /init-scripts/db_ddl.sql + echo "Database initialization completed!" + volumeMounts: + - name: init-scripts + mountPath: /init-scripts + volumes: + - name: init-scripts + configMap: + name: mysql-init + diff --git a/examples/kubernetes/namespace.yaml b/examples/kubernetes/namespace.yaml new file mode 100644 index 000000000..9b803c334 --- /dev/null +++ b/examples/kubernetes/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: bfe-system + labels: + name: bfe-system diff --git a/examples/kubernetes/service-controller-deploy.yaml b/examples/kubernetes/service-controller-deploy.yaml new file mode 100644 index 000000000..596dc91cf --- /dev/null +++ b/examples/kubernetes/service-controller-deploy.yaml @@ -0,0 +1,117 @@ +# Copyright (c) 2025 The BFE Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: bfe-service-controller + labels: + app.kubernetes.io/name: bfe-service-controller + app.kubernetes.io/instance: bfe-service-controller + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + #namespace as postfix + name: bfe-service-controller-default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: bfe-service-controller + namespace: bfe-system + +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: bfe-service-controller + labels: + app.kubernetes.io/name: bfe-service-controller + app.kubernetes.io/instance: bfe-service-controller +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: bfe-service-controller + app.kubernetes.io/instance: bfe-service-controller + template: + metadata: + labels: + app.kubernetes.io/name: bfe-service-controller + app.kubernetes.io/instance: bfe-service-controller + spec: + serviceAccountName: bfe-service-controller + containers: + - name: bfe-service-controller + imagePullPolicy: Always + image: ghcr.io/bfenetworks/service-controller:x86_64-v1.0.0 + command: [ "/service-controller" ] + args: + - '-bfe-api-addr=http://api-server.bfe-system.svc.cluster.local:8183' + - '-bfe-api-token=Token eT5QWkLhQmp6lO4NWxAc' + - '-k8s-cluster-name=szyf' + - '-namespace=bfe-system' + ports: + - containerPort: 9081 + livenessProbe: + httpGet: + path: /healthz + port: 9081 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 2 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /readyz + port: 9081 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 2 + successThreshold: 1 + failureThreshold: 3 + securityContext: + runAsUser: 1000 # bfeuser defined in Dockerfile + runAsGroup: 1000 # bfegroup defined in Dockerfile + runAsNonRoot: true + privileged: false + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: true + resources: + requests: + cpu: "500m" + memory: "500Mi" + limits: + cpu: "1" + memory: "2Gi" + volumeMounts: + - name: tmp + mountPath: /tmp + - name: var-run + mountPath: /var/run + volumes: + - name: tmp + emptyDir: {} + - name: var-run + emptyDir: {} \ No newline at end of file diff --git a/examples/kubernetes/whoami-deploy.yaml b/examples/kubernetes/whoami-deploy.yaml new file mode 100644 index 000000000..f822f81f0 --- /dev/null +++ b/examples/kubernetes/whoami-deploy.yaml @@ -0,0 +1,61 @@ +# Copyright (c) 2025 The BFE Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: whoami + namespace: bfe-system + labels: + app.kubernetes.io/name: whoami + app.kubernetes.io/instance: whoami +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: whoami + app.kubernetes.io/instance: whoami + template: + metadata: + labels: + app.kubernetes.io/name: whoami + app.kubernetes.io/instance: whoami + spec: + containers: + - name: whoami + imagePullPolicy: IfNotPresent + image: traefik/whoami + ports: + - containerPort: 80 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami + namespace: bfe-system + labels: + bfe-product: demo +spec: + ports: + - name: http + port: 8080 + targetPort: 80 + # - name: http2 #multiple port demo + # port: 8090 + # targetPort: 80 + selector: + app.kubernetes.io/name: whoami + #app.kubernetes.io/instance: whoami From 7058f1355bb7f9dc0ded490ab0e2256290610189 Mon Sep 17 00:00:00 2001 From: cc14514 Date: Sat, 31 Jan 2026 13:54:52 +0800 Subject: [PATCH 2/2] docs(k8s): add Apache-2.0 license headers to example manifests Signed-off-by: cc14514 --- examples/kubernetes/api-server-configmap.yaml | 14 ++++++++++++++ examples/kubernetes/api-server-deploy.yaml | 14 ++++++++++++++ examples/kubernetes/bfe-configmap.yaml | 14 ++++++++++++++ examples/kubernetes/bfe-deploy.yaml | 14 ++++++++++++++ examples/kubernetes/kustomization.yaml | 14 ++++++++++++++ examples/kubernetes/mysql-deploy.yaml | 14 ++++++++++++++ examples/kubernetes/namespace.yaml | 14 ++++++++++++++ 7 files changed, 98 insertions(+) diff --git a/examples/kubernetes/api-server-configmap.yaml b/examples/kubernetes/api-server-configmap.yaml index 030a26473..193df2f8c 100644 --- a/examples/kubernetes/api-server-configmap.yaml +++ b/examples/kubernetes/api-server-configmap.yaml @@ -1,3 +1,17 @@ +# Copyright (c) 2026 The BFE Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + apiVersion: v1 kind: ConfigMap metadata: diff --git a/examples/kubernetes/api-server-deploy.yaml b/examples/kubernetes/api-server-deploy.yaml index 21e21722e..576e6890e 100644 --- a/examples/kubernetes/api-server-deploy.yaml +++ b/examples/kubernetes/api-server-deploy.yaml @@ -1,3 +1,17 @@ +# Copyright (c) 2026 The BFE Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + apiVersion: apps/v1 kind: Deployment metadata: diff --git a/examples/kubernetes/bfe-configmap.yaml b/examples/kubernetes/bfe-configmap.yaml index 3f2b4d575..708d3ac83 100644 --- a/examples/kubernetes/bfe-configmap.yaml +++ b/examples/kubernetes/bfe-configmap.yaml @@ -1,3 +1,17 @@ +# Copyright (c) 2026 The BFE Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + apiVersion: v1 kind: ConfigMap metadata: diff --git a/examples/kubernetes/bfe-deploy.yaml b/examples/kubernetes/bfe-deploy.yaml index 65f81b0b7..52e369b6a 100644 --- a/examples/kubernetes/bfe-deploy.yaml +++ b/examples/kubernetes/bfe-deploy.yaml @@ -1,3 +1,17 @@ +# Copyright (c) 2026 The BFE Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + apiVersion: apps/v1 kind: Deployment metadata: diff --git a/examples/kubernetes/kustomization.yaml b/examples/kubernetes/kustomization.yaml index 560cbafb7..20a9868f4 100644 --- a/examples/kubernetes/kustomization.yaml +++ b/examples/kubernetes/kustomization.yaml @@ -1,3 +1,17 @@ +# Copyright (c) 2026 The BFE Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization diff --git a/examples/kubernetes/mysql-deploy.yaml b/examples/kubernetes/mysql-deploy.yaml index a01449c1f..40064e140 100644 --- a/examples/kubernetes/mysql-deploy.yaml +++ b/examples/kubernetes/mysql-deploy.yaml @@ -1,3 +1,17 @@ +# Copyright (c) 2026 The BFE Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # MySQL Database Components for API Server # This file contains all MySQL-related Kubernetes resources: # - Secret (credentials) diff --git a/examples/kubernetes/namespace.yaml b/examples/kubernetes/namespace.yaml index 9b803c334..b89eed56a 100644 --- a/examples/kubernetes/namespace.yaml +++ b/examples/kubernetes/namespace.yaml @@ -1,3 +1,17 @@ +# Copyright (c) 2026 The BFE Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + apiVersion: v1 kind: Namespace metadata: