mirror of
https://github.com/iflytek/skillhub.git
synced 2026-11-14 09:01:16 +00:00
fix(analytics): preserve deployment identity and Helm secrets
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
ffc9c8341a
commit
1e97905a08
11 changed files with 448 additions and 42 deletions
3
.github/workflows/pr-helm-chart.yml
vendored
3
.github/workflows/pr-helm-chart.yml
vendored
|
|
@ -50,6 +50,9 @@ jobs:
|
|||
- name: Validate configuration contracts
|
||||
run: bash tests/configuration-contracts.sh
|
||||
|
||||
- name: Verify analytics smoke isolation
|
||||
run: bash tests/analytics-smoke-test.sh
|
||||
|
||||
- name: Validate chart metadata
|
||||
run: |
|
||||
CHART_VERSION=$(helm show chart . | grep '^version:' | awk '{print $2}')
|
||||
|
|
|
|||
|
|
@ -108,10 +108,26 @@ Sentinel、实际 MinIO S3 连接,以及由 Kubernetes API 接受的 TLS Ingre
|
|||
### 可选前端动作统计
|
||||
|
||||
默认关闭。`web.analytics.config` 接受 JSON 字符串,`web.analytics.upstream` 接受仅部署可见的
|
||||
HTTP(S) origin。支持 Umami,不需要编辑 JS 或 Nginx;先在平台创建网站并取得 UUID。
|
||||
HTTP(S) origin。支持 Umami 和 DataFinder,不需要编辑 JS 或 Nginx;Umami 先创建网站取得 UUID,
|
||||
DataFinder 先取得应用 app_id 和采集域名。
|
||||
值放在 Web 专用 ConfigMap,只改统计配置时仅 Web 滚动更新、后端 Pod 不替换;旧标签页不会自动更新。字段、授权接口、初始化、隐私及验收见
|
||||
[前端动作统计接入指南](../../docs/frontend-analytics-integration.md)。
|
||||
|
||||
统计专项验收脚本会发送测试事件、查询 `stats` 中的独立数据库,并对指定的已安装 Release
|
||||
执行真实 `helm upgrade --reuse-values`,将统计规则改为空对象;它不是只读检查,也不会自动恢复规则。
|
||||
仅用于隔离验收安装,要求单个 Ready Web/Server Pod、独立 `stats` 平台和提前配置好的 Web port-forward。
|
||||
从仓库根目录运行(先替换网站 UUID、values 和安装名称):
|
||||
|
||||
```sh
|
||||
ANALYTICS_SMOKE_WEB_URL=http://127.0.0.1:18082 \
|
||||
bash charts/skillhub/tests/analytics-smoke.sh "<网站UUID>" values-acceptance.yaml sh1037 skillhub
|
||||
```
|
||||
|
||||
第二个参数必须是该安装使用的配置;脚本复用已有 Release 值,不会隐式加载
|
||||
`tests/test-values.yaml`。显式文件仍可覆盖原值,勿混用其他环境的 values 或重新生成 Secret。
|
||||
脚本成功只证明代理入库及本次统计配置升级时 Web 滚动、Server 未替换;浏览器授权、
|
||||
业务回归和真实生产配置仍须按接入指南验证。验收后用原安装配置再次升级以恢复规则。
|
||||
|
||||
### 高可用模式
|
||||
|
||||
```bash
|
||||
|
|
|
|||
190
charts/skillhub/tests/analytics-smoke-test.sh
Executable file
190
charts/skillhub/tests/analytics-smoke-test.sh
Executable file
|
|
@ -0,0 +1,190 @@
|
|||
#!/usr/bin/env bash
|
||||
# Exercise the installed-release smoke without a cluster or third-party packages.
|
||||
set -euo pipefail
|
||||
TEST_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
python3 - "${ANALYTICS_SMOKE_SCRIPT:-$TEST_DIR/analytics-smoke.sh}" <<'PY'
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
script = Path(sys.argv[1]).resolve()
|
||||
website = '00000000-0000-4000-8000-000000000037'
|
||||
# JSON is valid Helm YAML. The caller file deliberately omits settings supplied
|
||||
# at installation through other files/--set, especially private credentials.
|
||||
original = {
|
||||
'secrets': {'bootstrapAdminPassword': 'synthetic-private-bootstrap',
|
||||
'downloadAnonCookieSecret': 'synthetic-private-cookie-secret'},
|
||||
'postgresql': {'auth': {'password': 'synthetic-private-postgres'}},
|
||||
'redis': {'auth': {'password': 'synthetic-private-redis'}},
|
||||
'server': {'replicaCount': 1, 'image': {'tag': 'installed-server-tag'}},
|
||||
'web': {'basePath': '/registry/', 'analytics': {
|
||||
'upstream': 'http://original-stats.internal:3000', 'config': '{}'}},
|
||||
'existingSecret': '',
|
||||
}
|
||||
caller = {'web': {'analytics': {'upstream': 'http://original-stats.internal:3000'}}}
|
||||
|
||||
# These fakes model Helm value layering and process state, not chart rendering.
|
||||
# They fail on unexpected operations so this test cannot reach a real cluster.
|
||||
fake = r'''#!/usr/bin/env python3
|
||||
import copy, json, os, sys
|
||||
from pathlib import Path
|
||||
root = Path(os.environ['FAKE_STATE'])
|
||||
name = Path(sys.argv[0]).name
|
||||
args = sys.argv[1:]
|
||||
scenario = os.environ['FAKE_SCENARIO']
|
||||
|
||||
def check(condition, message):
|
||||
if not condition:
|
||||
sys.exit(message)
|
||||
|
||||
def record(value):
|
||||
with (root / 'calls').open('a') as f:
|
||||
f.write(value + '\n')
|
||||
|
||||
def merge(base, values):
|
||||
for key, value in values.items():
|
||||
if isinstance(value, dict) and isinstance(base.get(key), dict):
|
||||
merge(base[key], value)
|
||||
else:
|
||||
base[key] = copy.deepcopy(value)
|
||||
|
||||
if name == 'helm':
|
||||
check(args[:2] == ['upgrade', os.environ['FAKE_RELEASE']], 'unexpected Helm operation')
|
||||
check(args[args.index('-n') + 1] == os.environ['FAKE_NAMESPACE'], 'wrong release namespace')
|
||||
check('--wait' in args and args[args.index('--timeout') + 1] == '5m', 'upgrade must wait')
|
||||
original = json.loads((root / 'original.json').read_text())
|
||||
effective = copy.deepcopy(original) if '--reuse-values' in args else {}
|
||||
files = [Path(args[i + 1]) for i, arg in enumerate(args) if arg == '-f']
|
||||
check(Path(os.environ['FAKE_CALLER']) in files, 'caller values were omitted')
|
||||
for path in files:
|
||||
# Reading the known repository fixture models the old bug as well.
|
||||
text = path.read_text()
|
||||
if text.startswith('{'):
|
||||
values = json.loads(text)
|
||||
else:
|
||||
values = {}
|
||||
stack = [(-1, values)]
|
||||
for line in text.splitlines():
|
||||
if not line.strip() or line.lstrip().startswith('#'):
|
||||
continue
|
||||
indent = len(line) - len(line.lstrip())
|
||||
key, value = line.strip().split(':', 1)
|
||||
while stack[-1][0] >= indent:
|
||||
stack.pop()
|
||||
parent = stack[-1][1]
|
||||
value = value.strip()
|
||||
parent[key] = value.strip("'\"") if value else {}
|
||||
if not value:
|
||||
stack.append((indent, parent[key]))
|
||||
merge(effective, values)
|
||||
expected = copy.deepcopy(original)
|
||||
expected['web']['analytics']['config'] = effective.get('web', {}).get('analytics', {}).get('config')
|
||||
if scenario == 'generated-profile':
|
||||
check(effective.get('secrets') == original['secrets'],
|
||||
'generated secret profile must retain empty values for lookup')
|
||||
check(effective == expected, 'upgrade changed unrelated release values or secrets')
|
||||
check(all(p.name != 'test-values.yaml' for p in files), 'repository fixture values injected')
|
||||
config = json.loads(effective['web']['analytics']['config'])
|
||||
check(config == {'enabled': True, 'adapter': 'umami', 'consent': 'granted',
|
||||
'rules': {}, 'adapterConfig': {'websiteId': os.environ['FAKE_WEBSITE']}},
|
||||
'wrong analytics upgrade')
|
||||
(root / 'upgraded').touch()
|
||||
record('upgrade: analytics only; installed values and secrets retained; no test fixture')
|
||||
elif name == 'kubectl':
|
||||
check(args[0] == '-n', 'namespace must be explicit')
|
||||
namespace, command = args[1], args[2:]
|
||||
if command[:2] == ['get', 'pods']:
|
||||
check(namespace == os.environ['FAKE_NAMESPACE'], 'wrong pod namespace')
|
||||
selector = command[command.index('-l') + 1]
|
||||
component = selector.rsplit('=', 1)[-1]
|
||||
check(component in ('server', 'web'), 'unexpected component')
|
||||
upgraded = (root / 'upgraded').exists()
|
||||
changed = upgraded and ((component == 'web' and scenario != 'web-stable') or
|
||||
(component == 'server' and scenario == 'server-roll'))
|
||||
uid = component + ('-after' if changed else '-before')
|
||||
record('uid: ' + uid)
|
||||
print(json.dumps({'items': [{'metadata': {'uid': uid}, 'status': {
|
||||
'conditions': [{'type': 'Ready', 'status': 'True'}]}}]}))
|
||||
elif command[:3] == ['exec', 'deployment/umami-postgres', '--']:
|
||||
check(namespace == 'stats', 'event query must use independent stats namespace')
|
||||
check(os.environ['FAKE_WEBSITE'] in command[-1] and "event_name='skill.card.open'" in command[-1],
|
||||
'wrong event query')
|
||||
print(11 if (root / 'sent').exists() and scenario != 'no-event' else 10)
|
||||
record('event count')
|
||||
elif command[:2] == ['rollout', 'status']:
|
||||
check(command[2] == 'deployment/' + os.environ['FAKE_RELEASE'] + '-skillhub-web', 'wrong rollout')
|
||||
check((root / 'upgraded').exists(), 'rollout before upgrade')
|
||||
record('rollout: web')
|
||||
else:
|
||||
sys.exit('unexpected kubectl operation')
|
||||
elif name == 'curl':
|
||||
check(args[-1].startswith('http://127.0.0.1:18082/') or '--data' in args, 'unexpected Web URL')
|
||||
if '-X' in args:
|
||||
check(args[args.index('-X') + 1] == 'POST', 'expected event POST')
|
||||
payload = json.loads(args[args.index('--data') + 1])['payload']
|
||||
check(payload['website'] == os.environ['FAKE_WEBSITE'] and payload['name'] == 'skill.card.open',
|
||||
'wrong event payload')
|
||||
(root / 'sent').touch()
|
||||
record('HTTP: event POST')
|
||||
else:
|
||||
check(args[-1].endswith('/_skillhub-analytics/script.js'), 'wrong tracker path')
|
||||
record('HTTP: tracker GET')
|
||||
print('200', end='')
|
||||
else:
|
||||
sys.exit('unexpected fake command')
|
||||
'''
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='analytics-smoke-test.') as tmp:
|
||||
root = Path(tmp)
|
||||
bin_dir = root / 'bin'
|
||||
bin_dir.mkdir()
|
||||
shim = bin_dir / 'fake'
|
||||
shim.write_text(fake)
|
||||
shim.chmod(0o755)
|
||||
for name in ('helm', 'kubectl', 'curl'):
|
||||
(bin_dir / name).symlink_to(shim)
|
||||
caller_file = root / 'installed-overrides.json'
|
||||
caller_file.write_text(json.dumps(caller))
|
||||
for scenario, failure in (
|
||||
('generated-profile', None),
|
||||
('success', None),
|
||||
('server-roll', 'Server pod rolled unexpectedly'),
|
||||
('web-stable', 'Web pod did not roll'),
|
||||
('no-event', 'HTTP 200 but event not stored'),
|
||||
):
|
||||
state = root / scenario
|
||||
state.mkdir()
|
||||
installed = copy.deepcopy(original)
|
||||
if scenario == 'generated-profile':
|
||||
# Empty application-secret values preserve the chart's existing-Secret
|
||||
# lookup path. This value-layer model does not perform that lookup.
|
||||
installed['secrets'] = {'allowAutoGenerated': True,
|
||||
'bootstrapAdminPassword': '',
|
||||
'downloadAnonCookieSecret': ''}
|
||||
(state / 'original.json').write_text(json.dumps(installed))
|
||||
env = dict(os.environ, PATH=str(bin_dir) + os.pathsep + os.environ['PATH'],
|
||||
FAKE_STATE=str(state), FAKE_SCENARIO=scenario,
|
||||
FAKE_WEBSITE=website, FAKE_CALLER=str(caller_file),
|
||||
FAKE_RELEASE='original-release', FAKE_NAMESPACE='original-namespace',
|
||||
ANALYTICS_SMOKE_WEB_URL='http://127.0.0.1:18082', TMPDIR=str(state))
|
||||
result = subprocess.run(['bash', str(script), website, str(caller_file),
|
||||
env['FAKE_RELEASE'], env['FAKE_NAMESPACE']],
|
||||
env=env, text=True, capture_output=True)
|
||||
if failure is None:
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
calls = (state / 'calls').read_text().splitlines()
|
||||
assert calls.count('event count') == 2, calls
|
||||
for call in ('uid: server-before', 'uid: web-before', 'uid: web-after', 'rollout: web'):
|
||||
assert call in calls, call
|
||||
assert calls.count('uid: server-before') == 2, calls
|
||||
assert 'server UID unchanged' in result.stdout, result.stdout
|
||||
else:
|
||||
assert result.returncode != 0 and failure in result.stderr, result.stdout + result.stderr
|
||||
assert not list(state.glob('skillhub-analytics-smoke.*')), 'temporary override was not cleaned'
|
||||
print('PASS: ' + scenario)
|
||||
print('Analytics smoke regression tests passed (fake Helm/kubectl/curl; no deployment).')
|
||||
PY
|
||||
|
|
@ -31,12 +31,13 @@ send_code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST "$WEB_URL/_skillhub-
|
|||
after=$(count_events)
|
||||
[[ "$after" -gt "$before" ]] || { echo 'HTTP 200 but event not stored' >&2; exit 1; }
|
||||
echo "Independent stats DB: event count $before -> $after (script=$script_code send=$send_code)"
|
||||
# Change only Web analytics. All other values, including secrets, come from the same files.
|
||||
# Reuse installed values (including secrets); the caller file must reproduce the install.
|
||||
# Never apply chart test fixtures to an existing release.
|
||||
changed="{\"enabled\":true,\"adapter\":\"umami\",\"consent\":\"granted\",\"rules\":{},\"adapterConfig\":{\"websiteId\":\"$WEBSITE_ID\"}}"
|
||||
change_values=$(mktemp "${TMPDIR:-/tmp}/skillhub-analytics-smoke.XXXXXX")
|
||||
trap 'rm -f "$change_values"' EXIT
|
||||
printf "web:\n analytics:\n config: '%s'\n" "$changed" >"$change_values"
|
||||
helm upgrade "$RELEASE" "$CHART_DIR" -n "$NAMESPACE" -f "$CHART_DIR/tests/test-values.yaml" -f "$VALUES_FILE" \
|
||||
helm upgrade "$RELEASE" "$CHART_DIR" -n "$NAMESPACE" --reuse-values -f "$VALUES_FILE" \
|
||||
-f "$change_values" --wait --timeout 5m >/dev/null
|
||||
kubectl -n "$NAMESPACE" rollout status "deployment/$RELEASE-skillhub-web" --timeout=180s >/dev/null
|
||||
server_after=$(pod_uid server)
|
||||
|
|
|
|||
|
|
@ -117,14 +117,18 @@ product 只接受小写字母开头,后接小写字母、数字或下划线,
|
|||
|
||||
## 3. 一键脚本:新建独立 Umami
|
||||
|
||||
无需克隆仓库。从发布分支下载脚本执行;`--ref` 固定脚本和配套文件的同一版本。
|
||||
无需克隆仓库。选择包含本功能的已审核 tag 或提交 SHA,下载同版本脚本和配套文件;
|
||||
下面的 `SKILLHUB_REF` 须替换为实际版本,并在后续所有命令的 shell 中保留。
|
||||
`--ref`(或环境变量 `SKILLHUB_REF`)固定文件来源,`--version` 单独选择 SkillHub 镜像标签;两者需匹配,不能把文件版本当作镜像版本。
|
||||
`runtime.sh` 从 GitHub raw 获取 `compose.release.yml`、`.env.release.example`,在启用时获取
|
||||
`compose.release.analytics.yml`,在部署统计平台时获取 `compose.analytics-umami.yml`。
|
||||
这些文件保存在 `--home` 指定目录。使用 `--aliyun` 时镜像 OSS 根目录须同时发布两个新增 Compose 文件;
|
||||
旧镜像仅可运行不含统计的 `up`。生产环境先锁定版本、备份数据,核对管理端访问控制。
|
||||
旧版 OSS 文件集仅可运行不含统计的 `up`,不能假定 GitHub 的 `--ref` 会固定 OSS 镜像文件版本。生产环境先锁定版本、备份数据,核对管理端访问控制。
|
||||
|
||||
```sh
|
||||
curl -fsSL https://raw.githubusercontent.com/iflytek/skillhub/main/scripts/runtime.sh -o runtime.sh
|
||||
export SKILLHUB_REF="<包含本功能的已审核tag或提交SHA>"
|
||||
export SKILLHUB_VERSION="<与该版本匹配的已发布镜像标签>"
|
||||
curl -fsSL "https://raw.githubusercontent.com/iflytek/skillhub/$SKILLHUB_REF/scripts/runtime.sh" -o runtime.sh
|
||||
sh runtime.sh up --home "$HOME/skillhub-runtime"
|
||||
sh runtime.sh analytics-up --home "$HOME/skillhub-runtime"
|
||||
```
|
||||
|
|
@ -133,23 +137,33 @@ sh runtime.sh analytics-up --home "$HOME/skillhub-runtime"
|
|||
启动独立 Compose 项目 `skillhub-analytics`:Postgres 使用独立数据卷,Umami 管理端默认只绑定
|
||||
`127.0.0.1:8300`。重复执行不重置密钥或数据。远程机器通过 SSH 端口转发管理。
|
||||
如需改端口,编辑 `analytics/.env` 中的 `UMAMI_ADMIN_PORT`,再运行 `analytics-up` 使端口生效。
|
||||
若同机部署多个实例,先在 `.env.release` 中为每个实例指定唯一的
|
||||
`SKILLHUB_ANALYTICS_PROJECT`、`SKILLHUB_ANALYTICS_NETWORK`;已有平台不需要此网络。
|
||||
若同机部署多个实例,首次 `analytics-up` 前在各自 `.env.release` 中指定唯一的
|
||||
`SKILLHUB_ANALYTICS_PROJECT`、`SKILLHUB_ANALYTICS_NETWORK`。首次初始化会将这两个标识
|
||||
写入 `analytics/.env`;此后以独立统计配置中的标识为准,覆盖外层文件和 shell 中的同名值。
|
||||
不要通过修改标识迁移已有数据;它们决定实际 Compose 项目、网络和数据卷归属。已有平台不需要此网络。
|
||||
|
||||
打开 `http://127.0.0.1:8300`,用初始账号 `admin` / `umami` 登录,**立即修改密码**。
|
||||
重新登录,在网站管理中新建网站并复制网站 **UUID**。这一步由管理员完成,脚本不会代建网站。
|
||||
密码或 API token 不得填入 Web 配置。拿到 UUID 后运行:
|
||||
|
||||
```sh
|
||||
sh runtime.sh analytics-enable --home "$HOME/skillhub-runtime" --website-id <网站UUID>
|
||||
sh runtime.sh analytics-enable --home "$HOME/skillhub-runtime" --website-id "<网站UUID>"
|
||||
```
|
||||
|
||||
`analytics-enable` 会立即以 `consent=granted` 开启采集;仅在部署方确认允许后执行。
|
||||
若需要逐用户同意,先按第 2、5 节配置 `denied` 并接入同意组件,不先运行此开启命令。
|
||||
脚本把公开 JSON 和内部 origin `http://umami:3000` 写入 `.env.release`,只重建 Web。
|
||||
Web 和 Umami 通过外部网络互通;SkillHub server/数据库只留在 SkillHub 默认网络。
|
||||
以后 `runtime.sh up` 自动带 Web 网络 overlay,容器重建仍可连通。
|
||||
两个项目各自执行 `up`/`analytics-up`、`down`/`analytics-down`、`pull`/`analytics-pull`。
|
||||
`analytics-down` 仅 stop,数据卷和密钥保留;重新运行 `analytics-up` 即恢复。
|
||||
`down` 不停 Umami,`clean` 只删除 SkillHub 运行目录,不删 Compose 数据卷;清理前备份 `analytics/.env`。
|
||||
`down` 不停 Umami;`clean` 停止 SkillHub 并删除其生成的运行文件,存在 `analytics/` 时保留该目录、
|
||||
凭证和独立项目标识,不停止统计平台、不删数据卷。执行前分别备份业务配置、
|
||||
`analytics/.env` 和统计数据库;`clean` 后重新 `up` 需恢复业务配置,再启用采集。
|
||||
旧版本生成的 `analytics/.env` 若缺少标识,会从原 `.env.release` 迁入;若原文件已经丢失,
|
||||
脚本会在生成默认配置前拒绝运行管理命令,包括重复重试。此时恢复原文件,或核实原项目/网络后补入
|
||||
`analytics/.env`,不要猜测默认名称。可通过 `docker compose ls --all` 确认仍存在的项目,
|
||||
再检查对应 Umami 容器实际加入的网络;不要直接改项目名来尝试连接原数据卷。
|
||||
不要用 `down -v`,除非已备份且明确要删除数据。
|
||||
|
||||
## 3.1 一键脚本:接入已有统计平台
|
||||
|
|
@ -159,7 +173,7 @@ Web 和 Umami 通过外部网络互通;SkillHub server/数据库只留在 Skil
|
|||
|
||||
```sh
|
||||
sh runtime.sh analytics-enable --home "$HOME/skillhub-runtime" \
|
||||
--website-id <网站UUID> --upstream https://stats.example.com
|
||||
--website-id "<网站UUID>" --upstream https://stats.example.com
|
||||
```
|
||||
|
||||
使用已有 DataFinder 时,把平台应用的数字 app_id 和采集域名(非管理域名)填入:
|
||||
|
|
@ -169,12 +183,14 @@ sh runtime.sh analytics-enable --home "$HOME/skillhub-runtime" \
|
|||
--adapter datafinder --app-id 123456 --upstream https://collector.example.com
|
||||
```
|
||||
|
||||
上述 `analytics-enable` 同样立即使用 `consent=granted`,逐用户同意请用第 2、5 节。
|
||||
两种已有平台均不创建本地统计服务或共享网络。关闭采集运行
|
||||
`sh runtime.sh analytics-disable --home "$HOME/skillhub-runtime"`,仅重建 Web,平台仍运行。
|
||||
现有手工 Compose 部署也可按第 2 节直接填写 `.env.release` 并重建 Web。
|
||||
|
||||
## 4. Helm:独立统计部署单元
|
||||
|
||||
以下命令从检出的同一已审核版本仓库根目录运行;Chart、values schema 和 Kubernetes 清单应来自该版本。
|
||||
SkillHub Chart 不依赖 Umami。自建平台可先部署独立的 `deploy/k8s/analytics-umami/umami.yaml`。
|
||||
示例以 `stats` namespace、默认 StorageClass 的 PVC 为基础。先创建独立 namespace 和 Secret:
|
||||
|
||||
|
|
@ -199,7 +215,8 @@ PVC 独立于 SkillHub Release;更新 Umami 镜像或密钥须单独滚动统
|
|||
Postgres 大版本升级先备份数据库并按其升级流程迁移。删除 Deployment 不删除 PVC;
|
||||
删除 namespace/PVC 会删除数据,须先做可恢复备份。
|
||||
|
||||
保留已有生产 values 和 Secret。在 values 中增加以下内容:
|
||||
以下针对已安装的 SkillHub Release。首次安装先按 [Chart README](https://github.com/iflytek/skillhub/blob/main/charts/skillhub/README.md) 完成业务安装,
|
||||
已有平台可跳过上面的自建平台步骤。保留安装时的生产 values 和固定 Secret,在原 values 中增加以下内容:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
|
|
@ -210,11 +227,14 @@ web:
|
|||
|
||||
```sh
|
||||
helm lint --strict charts/skillhub -f values-production.yaml
|
||||
helm upgrade --install skillhub ./charts/skillhub -n skillhub -f values-production.yaml
|
||||
helm upgrade skillhub ./charts/skillhub -n skillhub --reuse-values -f values-production.yaml
|
||||
kubectl -n skillhub rollout status deployment/skillhub-web
|
||||
kubectl -n skillhub logs deployment/skillhub-web --tail=100
|
||||
```
|
||||
|
||||
`helm upgrade` 是实际升级,`--reuse-values` 保留 Release 的已有值;显式传入的文件仍会覆盖旧值,
|
||||
必须使用该安装的配置,不要传入 `charts/skillhub/tests/test-values.yaml` 等测试夹具。
|
||||
保留现有 Secret 引用和固定凭证,不能为开启统计重新生成业务密钥。
|
||||
deployment 名称使用 Chart 默认 release 名 `skillhub`;自定义名称时替换命令中的名称。
|
||||
值写入 Web 专用的 `<release>-web-analytics` ConfigMap,只改统计配置时仅 Web 滚动更新,
|
||||
后端 Pod 不会被替换。
|
||||
|
|
@ -338,11 +358,17 @@ curl -i http://localhost/_skillhub-analytics/api/websites
|
|||
| 有点击但没有统计 | 规则未配置、宿主不支持、超过 Umami 名称 50 字符,或 best-effort 丢失。 |
|
||||
| 上传 413 | 检查整条外层代理链,正常 Web 仍是 100M,统计未降低它。 |
|
||||
|
||||
runtime 升级时先备份 `.env.release`、`analytics/.env` 和数据库,下载目标版本的脚本并更新
|
||||
`SKILLHUB_REF` 和匹配的 `SKILLHUB_VERSION`;`pull` 只拉业务镜像,`analytics-pull` 只拉统计镜像,拉取不替换运行容器。
|
||||
分别运行 `up` 与 `analytics-up` 才应用变化,沿用原凭证和项目标识。不要以删除配置重建作为升级方法。
|
||||
|
||||
升级先比较[manifest](https://github.com/iflytek/skillhub/blob/main/docs/frontend-analytics-manifest.json),已有 ID 含义冻结,新 ID 不自动加入默认规则。
|
||||
消失的配置 ID 不再触发。SDK 固定 Umami 3.4.1;升级需重测真实 payload、stop、并发和
|
||||
根/子路径业务。回滚 Web 镜像要同时使用该版本支持的配置;先关闭统计最稳妥。
|
||||
|
||||
持久关闭:把配置留空或 enabled 改 false,按第 2 节重新创建 Web。新页面应零模块/SDK/请求。
|
||||
持久关闭:runtime 用户运行 `analytics-disable`(第 3.1 节);手工 Compose 用户将配置留空
|
||||
或 enabled 改 false,再按第 2 节重建 Web;Helm 用户将 `web.analytics.config` 留空或
|
||||
enabled 改 false,使用原生产 values 按第 4 节升级。新页面应零模块/SDK/请求。
|
||||
老标签页不自动更新;需让用户关闭/刷新,或在现有同意组件调用 stop。
|
||||
紧急全局阻断可在平台或独立采集路由禁用接收,再处理旧页;不能撤回已发出的请求。
|
||||
|
||||
|
|
@ -353,7 +379,8 @@ sh runtime.sh analytics-down --home "$HOME/skillhub-runtime"
|
|||
```
|
||||
|
||||
Helm 自建平台可单独缩容 `kubectl -n stats scale deployment/umami --replicas=0`,
|
||||
恢复后扩容;SkillHub Release 保持运行。数据库卷保留。删除统计数据需另行备份、确认后只处理 `analytics_postgres_data` 对应的卷,
|
||||
恢复时将副本数改回 1;SkillHub Release 保持运行,PVC 保留。
|
||||
Compose 删除统计数据需另行备份、确认后只处理对应项目的 `analytics_postgres_data` 卷,
|
||||
不要使用整个项目的 `down -v`。
|
||||
|
||||
## 8. 隐私和自定义开发边界
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
# Passive frontend analytics contract
|
||||
|
||||
<!-- #region analytics-body -->
|
||||
For deployment, use the [Chinese configuration guide][analytics-guide].
|
||||
The runtime script can install an independent Umami/PostgreSQL Compose project or
|
||||
connect Web to an existing platform; a separate Kubernetes unit provides Umami for
|
||||
Helm installations. Create a website UUID in Umami before enabling collection.
|
||||
Stopping analytics does not remove its data; refresh old tabs after disabling.
|
||||
Outage events are not replayed, and a well-formed but nonexistent UUID can still fail.
|
||||
For installation, platform initialization, upgrades, shutdown and acceptance, use the
|
||||
[Chinese configuration guide][analytics-guide]. It is the authoritative operator
|
||||
procedure for runtime Compose and Kubernetes/Helm. This document owns the event,
|
||||
module, configuration and privacy contracts; it does not duplicate deployment steps.
|
||||
Docker/Compose and Helm ship optional analytics with Web. No configuration means no
|
||||
controller/adapter chunk, SDK or collection request. The small configuration gate is
|
||||
part of bootstrap; the controller and SDK are not. Business components remain passive.
|
||||
|
|
|
|||
|
|
@ -49,7 +49,11 @@ Nginx 通过独立 include 片段选择已注册适配器的精确端点:Umami
|
|||
|
||||
### 4. Deployment and lifecycle
|
||||
|
||||
现有平台接入:填写 JSON 和内部 upstream,更新 Web。可选自托管组合:在 release Compose 上叠加统计服务与其持久化数据库,使用显式凭证,不新增 Nginx;创建网站/取得 UUID 等一次性平台步骤必须诚实说明。入口和 Helm 均默认关闭。
|
||||
现有平台接入:填写 JSON 和内部 upstream,更新 Web。可选自托管组合:统计服务与其持久化数据库使用独立 Compose 项目,仅 Web 和 Umami 加入共享私网,不新增 Nginx;创建网站/取得 UUID 等一次性平台步骤必须诚实说明。入口和 Helm 均默认关闭。
|
||||
|
||||
独立统计目录中的 `analytics/.env` 同时保存凭据、项目名和网络名,初始化后成为这些标识的权威来源;外层业务配置和继承的 shell 环境不能将统计命令或 Web 接线重定向到其他实例。`clean` 删除业务运行文件但保留独立统计目录。旧配置缺少标识时,只从仍存在的原业务配置迁入;若原配置已丢失,则所有命令在生成默认文件之前明确失败,要求恢复已确认的标识,不猜测默认项目。
|
||||
|
||||
针对已安装 Release 的 Helm 统计验收仅覆盖统计字段,复用现有 Release 值和 Secret;不得隐式叠加固定测试凭据。调用者提供的 values 仍必须与安装配置一致。该脚本会执行真实升级并改变事件规则,只用于有明确边界的隔离验收环境,不能描述为只读检查。
|
||||
|
||||
部署配置变更需重新创建 Web(Compose restart 不重新注入环境变量;Helm 统计值放在 Web 专用 ConfigMap,只滚动 Web)使新页面生效;旧标签页不自动接收新环境参数。动态停止通过当前页面接口生效;紧急全局停止可关闭独立采集路由。文档不得宣称仅修改 env 即刻撤销已在发送的请求。模块使用正常发布构建产物与缓存策略,不把可变配置放入 immutable JS。
|
||||
|
||||
|
|
@ -59,7 +63,7 @@ PR-2 不新增 jq 或 Node 运行时依赖。镜像已有 BusyBox awk 负责有
|
|||
|
||||
bootstrap 仅静态包含小型 gate 和注册参数校验;controller/helper 与 adapter 保持独立懒加载 chunk。在首个 React DOM commit 后的独立任务中启动 gate,不把正常挂载与 SDK 绑定。enabled=true 且 consent=denied 时不加载模块,但代理保留精确端点,供当前页显式 start() 授权后使用。静态 adapter factory 在动态导入返回后检查取消信号,迟到的导入不能重新创建 SDK。
|
||||
|
||||
旧示例目录仅保留指南指针。helper/adapter 与全部断言迁入产品测试树;历史 edge/Compose 只保留为测试 fixture,不是部署路径,不维护第二份采集实现。
|
||||
旧示例目录仅保留指南指针。helper/adapter 与全部断言迁入产品测试树;部署回归直接检查当前生产配置,不保留已删除的历史 edge/Compose 副本。
|
||||
|
||||
### PR-3: second registered adapter
|
||||
|
||||
|
|
|
|||
|
|
@ -150,3 +150,31 @@ Delivered by: PR-1 (scope declaration); PR-2 (completion gate).
|
|||
|
||||
- **WHEN** PR-1 merges independently
|
||||
- **THEN** Issue #989 remains open until PR-2 configuration-only deployment is complete and verified
|
||||
|
||||
### Requirement: Preserve independent analytics deployment identity
|
||||
Delivered by: PR-2.
|
||||
|
||||
The system SHALL persist the analytics project and network alongside its independent credentials and use this identity consistently for analytics lifecycle commands and local Web attachment.
|
||||
|
||||
#### Scenario: Clean and resume a custom analytics deployment
|
||||
|
||||
- **WHEN** an operator initializes a custom analytics project, cleans SkillHub runtime files, and later manages or reconnects analytics
|
||||
- **THEN** the original analytics project, network, credentials and data remain selected, regardless of outer configuration or shell variables
|
||||
- **AND** normal stop operations do not delete analytics volumes
|
||||
|
||||
#### Scenario: Legacy identity cannot be established
|
||||
|
||||
- **WHEN** existing analytics configuration lacks required identity and its original SkillHub configuration is missing
|
||||
- **THEN** runtime commands fail with restoration guidance before generating defaults or invoking Docker, including repeated attempts
|
||||
- **AND** the system does not guess a default project or regenerate analytics credentials
|
||||
|
||||
### Requirement: Keep installed-release verification isolated
|
||||
Delivered by: PR-2.
|
||||
|
||||
The analytics Helm verification script SHALL preserve existing release values and secrets while changing only its intended analytics settings.
|
||||
|
||||
#### Scenario: Verify a release with generated secrets
|
||||
|
||||
- **WHEN** the operator supplies installation values that omit secrets originally generated by the chart
|
||||
- **THEN** verification reuses the existing release values and secret lookup instead of injecting deterministic test credentials
|
||||
- **AND** event storage and Web/server rollout assertions remain enforced
|
||||
|
|
|
|||
|
|
@ -50,3 +50,10 @@
|
|||
- [x] 8.4 Verify isolated Compose from an empty runtime directory, repeat secrets/data, Web recreation/network, analytics stop/restart, existing-platform enable, disable and business routes. Evidence: `skillhub-1037-oneclick/evidence/compose-smoke.log`.
|
||||
- [x] 8.5 Verify real kind: separate stats unit and SkillHub Helm release, Umami DB event increase via Web proxy, analytics-only Helm upgrade rolls Web while server Pod UID remains unchanged. Evidence: `skillhub-1037-oneclick/evidence/kind-smoke.log`.
|
||||
- [ ] 8.6 Independent maintainer review and PR acceptance remain external; do not close Issue #989 here.
|
||||
|
||||
## 9. Maintainer deployment guard fixes
|
||||
- [x] 9.1 Keep installed-release smoke values isolated: reuse release values, remove implicit test credentials, and cover successful and rejected event/rollout paths plus incomplete-fix mutations.
|
||||
- [x] 9.2 Persist independent project/network identity, migrate only from known original configuration, and reject ambiguous legacy state before defaults are generated; cover custom/default multi-instance lifecycle and secret preservation.
|
||||
- [x] 9.3 Clarify file/image version pinning, the three operator paths, independent configuration ownership, cleanup/upgrade boundaries and smoke upgrade side effects; remove duplicate operational descriptions from the contract.
|
||||
- [ ] 9.4 Complete one consolidated independent review and the focused source/deployment validation matrix for these fixes.
|
||||
- [ ] 9.5 Build the affected documentation and verify the rendered operator instructions; keep real-cluster and publication status explicit.
|
||||
|
|
|
|||
|
|
@ -194,12 +194,13 @@ download_file() {
|
|||
set_env_value() {
|
||||
key="$1"
|
||||
value="$2"
|
||||
target_env_file="${3:-$ENV_FILE}"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
if [ ! -f "$target_env_file" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
tmp="$ENV_FILE.tmp"
|
||||
tmp="$target_env_file.tmp"
|
||||
found=false
|
||||
old_umask="$(umask)"
|
||||
umask 077
|
||||
|
|
@ -214,14 +215,14 @@ set_env_value() {
|
|||
printf '%s\n' "$line"
|
||||
;;
|
||||
esac
|
||||
done <"$ENV_FILE"
|
||||
done <"$target_env_file"
|
||||
if [ "$found" = "false" ]; then
|
||||
printf '%s=%s\n' "$key" "$value"
|
||||
fi
|
||||
} >"$tmp"
|
||||
umask "$old_umask"
|
||||
mv "$tmp" "$ENV_FILE"
|
||||
secure_env_file
|
||||
mv "$tmp" "$target_env_file"
|
||||
chmod 600 "$target_env_file"
|
||||
}
|
||||
|
||||
secure_env_file() {
|
||||
|
|
@ -233,7 +234,7 @@ secure_env_file() {
|
|||
get_env_value() {
|
||||
key="$1"
|
||||
default_value="${2:-}"
|
||||
value="$(grep "^$key=" "$ENV_FILE" | tail -n 1 | cut -d= -f2- || true)"
|
||||
value="$(grep "^$key=" "${3:-$ENV_FILE}" | tail -n 1 | cut -d= -f2- || true)"
|
||||
|
||||
if [ -n "$value" ]; then
|
||||
printf '%s' "$value"
|
||||
|
|
@ -341,6 +342,11 @@ SQL
|
|||
}
|
||||
|
||||
prepare_runtime_files() {
|
||||
# Migrate legacy identity before creating a replacement release file. Failed
|
||||
# migration must not leave fresh defaults that a later retry could trust.
|
||||
if [ -f "$ANALYTICS_ENV_FILE" ]; then
|
||||
ensure_analytics_identity
|
||||
fi
|
||||
mkdir -p "$SKILLHUB_HOME"
|
||||
download_file "$SKILLHUB_RAW_BASE/compose.release.yml" "$COMPOSE_FILE"
|
||||
# GitHub and OSS both publish this root-level overlay; fetch only when used.
|
||||
|
|
@ -415,6 +421,7 @@ run_compose() {
|
|||
compose_cmd="$(find_compose)"
|
||||
if [ "$(get_env_value SKILLHUB_ANALYTICS_NETWORK_ENABLED false)" = "true" ]; then
|
||||
ensure_analytics_network
|
||||
set_env_value SKILLHUB_ANALYTICS_NETWORK "$SKILLHUB_ANALYTICS_NETWORK"
|
||||
# Preserve the normal default network and attach only Web to analytics.
|
||||
set -- -f "$OVERLAY_FILE" "$@"
|
||||
fi
|
||||
|
|
@ -422,26 +429,43 @@ run_compose() {
|
|||
$compose_cmd --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"
|
||||
}
|
||||
|
||||
analytics_project() {
|
||||
get_env_value SKILLHUB_ANALYTICS_PROJECT skillhub-analytics
|
||||
}
|
||||
|
||||
analytics_network() {
|
||||
get_env_value SKILLHUB_ANALYTICS_NETWORK skillhub-analytics
|
||||
ensure_analytics_identity() {
|
||||
# Once initialized, the independent directory owns both identifiers. Neither
|
||||
# a recreated release file nor inherited Compose variables may redirect it.
|
||||
for identity_key in SKILLHUB_ANALYTICS_PROJECT SKILLHUB_ANALYTICS_NETWORK; do
|
||||
identity_value="$(get_env_value "$identity_key" "" "$ANALYTICS_ENV_FILE")"
|
||||
if [ -z "$identity_value" ]; then
|
||||
if [ "$ORIGINAL_ENV_FILE_EXISTS" != "true" ]; then
|
||||
echo "Missing $identity_key in $ANALYTICS_ENV_FILE. Restore the original .env.release or set the verified original identifier in analytics/.env before retrying; no project discovery is performed." >&2
|
||||
exit 1
|
||||
fi
|
||||
identity_value="$(get_env_value "$identity_key" skillhub-analytics)"
|
||||
set_env_value "$identity_key" "$identity_value" "$ANALYTICS_ENV_FILE"
|
||||
fi
|
||||
case "$identity_key" in
|
||||
SKILLHUB_ANALYTICS_PROJECT) SKILLHUB_ANALYTICS_PROJECT="$identity_value" ;;
|
||||
SKILLHUB_ANALYTICS_NETWORK) SKILLHUB_ANALYTICS_NETWORK="$identity_value" ;;
|
||||
esac
|
||||
done
|
||||
chmod 600 "$ANALYTICS_ENV_FILE"
|
||||
# Shell variables take precedence over --env-file in Compose interpolation.
|
||||
export SKILLHUB_ANALYTICS_PROJECT SKILLHUB_ANALYTICS_NETWORK
|
||||
}
|
||||
|
||||
ensure_analytics_network() {
|
||||
network_name="$(analytics_network)"
|
||||
ensure_analytics_identity
|
||||
network_name="$SKILLHUB_ANALYTICS_NETWORK"
|
||||
if ! docker network inspect "$network_name" >/dev/null 2>&1; then
|
||||
docker network create "$network_name" >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
run_analytics_compose() {
|
||||
ensure_analytics_identity
|
||||
compose_cmd="$(find_compose)"
|
||||
# Explicit -p also protects against an inherited COMPOSE_PROJECT_NAME.
|
||||
# shellcheck disable=SC2086
|
||||
$compose_cmd -p "$(analytics_project)" --env-file "$ANALYTICS_ENV_FILE" -f "$ANALYTICS_COMPOSE_FILE" "$@"
|
||||
$compose_cmd -p "$SKILLHUB_ANALYTICS_PROJECT" --env-file "$ANALYTICS_ENV_FILE" -f "$ANALYTICS_COMPOSE_FILE" "$@"
|
||||
}
|
||||
|
||||
prepare_analytics_files() {
|
||||
|
|
@ -455,11 +479,12 @@ prepare_analytics_files() {
|
|||
printf 'UMAMI_DB_PASSWORD=%s\n' "$(generate_secret)"
|
||||
printf 'UMAMI_APP_SECRET=%s\n' "$(generate_secret)"
|
||||
printf 'UMAMI_ADMIN_PORT=8300\n'
|
||||
printf 'SKILLHUB_ANALYTICS_NETWORK=%s\n' "$(analytics_network)"
|
||||
printf 'SKILLHUB_ANALYTICS_PROJECT=%s\n' "$(get_env_value SKILLHUB_ANALYTICS_PROJECT skillhub-analytics)"
|
||||
printf 'SKILLHUB_ANALYTICS_NETWORK=%s\n' "$(get_env_value SKILLHUB_ANALYTICS_NETWORK skillhub-analytics)"
|
||||
} >"$ANALYTICS_ENV_FILE"
|
||||
umask "$old_umask"
|
||||
fi
|
||||
chmod 600 "$ANALYTICS_ENV_FILE"
|
||||
ensure_analytics_identity
|
||||
}
|
||||
|
||||
validate_analytics_input() {
|
||||
|
|
@ -565,6 +590,9 @@ print_runtime_command() {
|
|||
|
||||
build_runtime_lifecycle_args
|
||||
|
||||
# Remember this before preparation can recreate defaults after clean.
|
||||
ORIGINAL_ENV_FILE_EXISTS=false
|
||||
[ ! -f "$ENV_FILE" ] || ORIGINAL_ENV_FILE_EXISTS=true
|
||||
prepare_runtime_files
|
||||
|
||||
case "$COMMAND" in
|
||||
|
|
@ -615,8 +643,8 @@ EOF
|
|||
run_compose pull
|
||||
;;
|
||||
analytics-up)
|
||||
ensure_analytics_network
|
||||
prepare_analytics_files
|
||||
ensure_analytics_network
|
||||
run_analytics_compose up -d
|
||||
admin_port="$(grep '^UMAMI_ADMIN_PORT=' "$ANALYTICS_ENV_FILE" | cut -d= -f2-)"
|
||||
printf 'Umami management: http://127.0.0.1:%s\nChange the default admin/umami password, create a website, copy its UUID, then run analytics-enable --website-id <UUID>.\nAnalytics files: %s\n' "$admin_port" "$ANALYTICS_HOME"
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
RUNTIME_SCRIPT=${RUNTIME_SCRIPT:-$ROOT/scripts/runtime.sh}
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
mkdir -p "$TMP/bin" "$TMP/home"
|
||||
cat >"$TMP/bin/docker" <<'SH'
|
||||
#!/bin/sh
|
||||
printf '%s\n' "$*" >>"$DOCKER_LOG"
|
||||
printf 'identity project=%s network=%s\n' "${SKILLHUB_ANALYTICS_PROJECT:-}" "${SKILLHUB_ANALYTICS_NETWORK:-}" >>"$DOCKER_LOG"
|
||||
# Exercise network creation without contacting a daemon.
|
||||
if [ "$1 $2" = "network inspect" ]; then exit 1; fi
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$TMP/bin/docker"
|
||||
export DOCKER_LOG="$TMP/docker.log" SKILLHUB_HOME="$TMP/home" SKILLHUB_RAW_BASE="file://$ROOT" PATH="$TMP/bin:$PATH"
|
||||
run() { sh "$ROOT/scripts/runtime.sh" "$@" >"$TMP/output"; }
|
||||
run() { sh "$RUNTIME_SCRIPT" "$@" >"$TMP/output"; }
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
run analytics-up
|
||||
[ -f "$TMP/home/analytics/compose.analytics-umami.yml" ] || fail 'missing platform file'
|
||||
|
|
@ -37,7 +41,107 @@ grep -q '^SKILLHUB_ANALYTICS_NETWORK_ENABLED=false$' "$TMP/home/.env.release" ||
|
|||
run analytics-down
|
||||
grep -q -- 'compose.analytics-umami.yml stop' "$DOCKER_LOG" || fail 'analytics down removed volumes'
|
||||
! grep -q -- 'compose.analytics-umami.yml down' "$DOCKER_LOG" || fail 'analytics down removed project'
|
||||
DOCKER_LOG="$TMP/docker.log" SKILLHUB_HOME="$TMP/home" SKILLHUB_RAW_BASE="file://$ROOT" PATH="$TMP/bin:$PATH" sh "$ROOT/scripts/runtime.sh" clean >"$TMP/output"
|
||||
DOCKER_LOG="$TMP/docker.log" SKILLHUB_HOME="$TMP/home" SKILLHUB_RAW_BASE="file://$ROOT" PATH="$TMP/bin:$PATH" sh "$RUNTIME_SCRIPT" clean >"$TMP/output"
|
||||
[ -f "$TMP/home/analytics/.env" ] || fail 'clean deleted independent analytics credentials'
|
||||
[ ! -f "$TMP/home/.env.release" ] || fail 'clean left SkillHub credentials'
|
||||
|
||||
|
||||
# Each home is an independent instance, including custom project/network names.
|
||||
# External environment must never override its saved independent identity.
|
||||
for instance in default tenant-a tenant-b; do
|
||||
export SKILLHUB_HOME="$TMP/$instance"
|
||||
run ps
|
||||
project=skillhub-analytics
|
||||
network=skillhub-analytics
|
||||
if [ "$instance" != default ]; then
|
||||
project="$instance-analytics"
|
||||
network="$instance-network"
|
||||
printf 'SKILLHUB_ANALYTICS_PROJECT=%s\nSKILLHUB_ANALYTICS_NETWORK=%s\n' "$project" "$network" >>"$SKILLHUB_HOME/.env.release"
|
||||
fi
|
||||
run analytics-up
|
||||
config="$SKILLHUB_HOME/analytics/.env"
|
||||
grep -qx "SKILLHUB_ANALYTICS_PROJECT=$project" "$config" || fail "$instance project not saved"
|
||||
grep -qx "SKILLHUB_ANALYTICS_NETWORK=$network" "$config" || fail "$instance network not saved"
|
||||
config_hash=$(shasum -a 256 "$config" | cut -d' ' -f1)
|
||||
run clean
|
||||
[ ! -f "$SKILLHUB_HOME/.env.release" ] || fail "$instance clean retained main config"
|
||||
for command in analytics-ps analytics-logs analytics-pull analytics-down analytics-up analytics-up; do
|
||||
: >"$DOCKER_LOG"
|
||||
SKILLHUB_ANALYTICS_PROJECT=unrelated-project SKILLHUB_ANALYTICS_NETWORK=unrelated-network COMPOSE_PROJECT_NAME=unrelated-main run "$command"
|
||||
grep -q -- "-p $project --env-file $config" "$DOCKER_LOG" || fail "$instance $command changed project after clean"
|
||||
grep -qx "identity project=$project network=$network" "$DOCKER_LOG" || fail "$instance $command lost Compose identity"
|
||||
! grep -q -- '-p unrelated-project' "$DOCKER_LOG" || fail 'inherited project override'
|
||||
[ "$(shasum -a 256 "$config" | cut -d' ' -f1)" = "$config_hash" ] || fail "$instance config or secrets changed"
|
||||
done
|
||||
# A newly generated release file has defaults; later unrelated edits are also
|
||||
# harmless. Local Web must attach to the saved analytics network in both cases.
|
||||
for outer in recreated conflicting; do
|
||||
if [ "$outer" = conflicting ]; then
|
||||
printf 'SKILLHUB_ANALYTICS_PROJECT=other-project\nSKILLHUB_ANALYTICS_NETWORK=other-network\n' >>"$SKILLHUB_HOME/.env.release"
|
||||
fi
|
||||
: >"$DOCKER_LOG"
|
||||
SKILLHUB_ANALYTICS_NETWORK=unrelated-network run analytics-enable --website-id 4398de8e-40df-4483-9eea-ed213643525f
|
||||
grep -q "network create $network" "$DOCKER_LOG" || fail "$instance Web created wrong network"
|
||||
grep -qx "identity project=$project network=$network" "$DOCKER_LOG" || fail "$instance Web interpolation wrong"
|
||||
grep -qx "SKILLHUB_ANALYTICS_NETWORK=$network" "$SKILLHUB_HOME/.env.release" || fail 'Web network not synchronized'
|
||||
: >"$DOCKER_LOG"
|
||||
SKILLHUB_ANALYTICS_NETWORK=unrelated-network run up --no-scanner
|
||||
grep -q "network create $network" "$DOCKER_LOG" || fail "$instance main up lost network"
|
||||
grep -q -- 'compose.release.analytics.yml up -d postgres' "$DOCKER_LOG" || fail "$instance main up lost overlay"
|
||||
run analytics-disable
|
||||
done
|
||||
chmod 644 "$config"
|
||||
run analytics-ps
|
||||
[ "$(file_mode "$config")" = 600 ] || fail 'existing analytics permissions not secured'
|
||||
[ "$(file_mode "$SKILLHUB_HOME/.env.release")" = 600 ] || fail 'main config permissions'
|
||||
[ "$(shasum -a 256 "$config" | cut -d' ' -f1)" = "$config_hash" ] || fail "$instance secret changed after enable/up/disable"
|
||||
! grep -q -- 'compose.analytics-umami.yml down' "$DOCKER_LOG" || fail 'analytics volumes removed'
|
||||
done
|
||||
|
||||
# Legacy files always contained the network, but not the project. Preserve the
|
||||
# original custom project before clean deletes its only previous source.
|
||||
export SKILLHUB_HOME="$TMP/legacy"
|
||||
run ps
|
||||
printf 'SKILLHUB_ANALYTICS_PROJECT=legacy-project\nSKILLHUB_ANALYTICS_NETWORK=legacy-network\n' >>"$SKILLHUB_HOME/.env.release"
|
||||
mkdir -p "$SKILLHUB_HOME/analytics"
|
||||
printf 'UMAMI_DB_PASSWORD=test-db-secret\nUMAMI_APP_SECRET=test-app-secret\nUMAMI_ADMIN_PORT=8300\nSKILLHUB_ANALYTICS_NETWORK=legacy-network\n' >"$SKILLHUB_HOME/analytics/.env"
|
||||
legacy_secrets=$(sed -n '/^UMAMI_/p' "$SKILLHUB_HOME/analytics/.env" | shasum -a 256)
|
||||
run clean
|
||||
grep -qx 'SKILLHUB_ANALYTICS_PROJECT=legacy-project' "$SKILLHUB_HOME/analytics/.env" || fail 'legacy project not migrated before clean'
|
||||
[ "$(sed -n '/^UMAMI_/p' "$SKILLHUB_HOME/analytics/.env" | shasum -a 256)" = "$legacy_secrets" ] || fail 'legacy secrets changed'
|
||||
: >"$DOCKER_LOG"
|
||||
run analytics-ps
|
||||
grep -q -- '-p legacy-project ' "$DOCKER_LOG" || fail 'legacy project reset'
|
||||
|
||||
# If the original main config is already lost, do not trust newly generated
|
||||
# defaults as migration evidence. Operators must restore a verified identifier.
|
||||
for command in analytics-ps analytics-logs analytics-pull analytics-down analytics-up clean analytics-enable analytics-disable up down ps logs pull; do
|
||||
export SKILLHUB_HOME="$TMP/lost-$command"
|
||||
mkdir -p "$SKILLHUB_HOME/analytics"
|
||||
printf 'UMAMI_DB_PASSWORD=test-db-secret\nUMAMI_APP_SECRET=test-app-secret\nSKILLHUB_ANALYTICS_NETWORK=legacy-network\n' >"$SKILLHUB_HOME/analytics/.env"
|
||||
: >"$DOCKER_LOG"
|
||||
if run "$command" --website-id 4398de8e-40df-4483-9eea-ed213643525f 2>"$TMP/error"; then fail "$command guessed lost project"; fi
|
||||
grep -q 'Restore the original .env.release' "$TMP/error" || fail 'missing actionable legacy error'
|
||||
[ ! -s "$DOCKER_LOG" ] || fail "$command contacted Docker for unidentified deployment"
|
||||
! grep -q 'test-.*-secret' "$TMP/error" || fail 'error disclosed secrets'
|
||||
! grep -q '^SKILLHUB_ANALYTICS_PROJECT=' "$SKILLHUB_HOME/analytics/.env" || fail 'lost legacy project overwritten'
|
||||
[ ! -f "$SKILLHUB_HOME/.env.release" ] || fail 'failed migration created misleading defaults'
|
||||
if run "$command" --website-id 4398de8e-40df-4483-9eea-ed213643525f 2>"$TMP/error"; then fail "$command retry guessed lost project"; fi
|
||||
done
|
||||
|
||||
# No configured local platform is needed to use an existing external service.
|
||||
export SKILLHUB_HOME="$TMP/external-only"
|
||||
run analytics-enable --website-id 4398de8e-40df-4483-9eea-ed213643525f --upstream https://stats.example.com
|
||||
[ ! -e "$SKILLHUB_HOME/analytics/.env" ] || fail 'external enable created local platform'
|
||||
grep -qx 'SKILLHUB_ANALYTICS_NETWORK_ENABLED=false' "$SKILLHUB_HOME/.env.release" || fail 'external enable attached network'
|
||||
run analytics-disable
|
||||
|
||||
# An original release file without explicit identifiers used historical defaults.
|
||||
export SKILLHUB_HOME="$TMP/legacy-default"
|
||||
mkdir -p "$SKILLHUB_HOME/analytics"
|
||||
printf 'POSTGRES_USER=skillhub\n' >"$SKILLHUB_HOME/.env.release"
|
||||
printf 'UMAMI_DB_PASSWORD=test-db-secret\nUMAMI_APP_SECRET=test-app-secret\nSKILLHUB_ANALYTICS_NETWORK=skillhub-analytics\n' >"$SKILLHUB_HOME/analytics/.env"
|
||||
run analytics-ps
|
||||
grep -qx 'SKILLHUB_ANALYTICS_PROJECT=skillhub-analytics' "$SKILLHUB_HOME/analytics/.env" || fail 'legacy default project migration'
|
||||
|
||||
echo 'runtime-analytics-test passed'
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue