Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/check-setup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.

name: Check setup readiness

on:
pull_request:
paths:
- .github/workflows/check-setup.yml
- config/components.ts
- config/components.test.ts
- setup/cluster-ready.ts
- setup/cluster-ready.test.ts
- setup/opsfile.yml

permissions:
contents: read

jobs:
readiness:
name: Task readiness regression tests
runs-on: ubuntu-22.04
timeout-minutes: 5
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Install checksum-pinned Bun 1.3.14
run: |
curl --fail --location --retry 3 \
https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64.zip \
--output "$RUNNER_TEMP/bun.zip"
echo "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f $RUNNER_TEMP/bun.zip" | sha256sum --check
unzip -q "$RUNNER_TEMP/bun.zip" -d "$RUNNER_TEMP"
echo "$RUNNER_TEMP/bun-linux-x64" >> "$GITHUB_PATH"
- name: Test component dependencies and setup readiness
run: bun test config/components.test.ts setup/cluster-ready.test.ts
57 changes: 57 additions & 0 deletions config/components.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.

import {test, expect} from "bun:test";
import {plan, validate, keys} from "./components";
test("all is symmetric and includes Alert Manager", () => {
const enabled=plan("enable",["all"],{}),disabled=plan("disable",["all"],{});
for(const k of Object.keys(enabled)) expect(disabled[k]).toBe("false");
expect(disabled[keys.alertmanager]).toBe("false");
expect(Object.keys(disabled).length).toBe(Object.keys(keys).length);
expect(enabled[keys.slack]).toBeUndefined();expect(enabled[keys.mail]).toBeUndefined();
});
test("full includes services but not notifications or scheduling",()=>{
const p=plan("full",[],{});validate(p);
for(const x of ["slack","mail","affinity","tolerations"])expect(p[keys[x]]).toBeUndefined();
for(const x of ["seaweedfs","static","etcd","milvus","alertmanager","registry"])expect(p[keys[x]]).toBe("true");
});
test("enable adds transitive dependencies",()=>{
expect(plan("enable",["milvus"],{})).toEqual({[keys.milvus]:"true",[keys.etcd]:"true",[keys.seaweedfs]:"true"});
expect(plan("enable",["mongodb"],{})[keys.postgres]).toBe("true");
expect(plan("enable",["static"],{})[keys.seaweedfs]).toBe("true");
});
test("disable cascades and preserves unrelated prerequisites",()=>{
expect(plan("disable",["prometheus"],{})[keys.mail]).toBe("false");
expect(plan("disable",["alertmanager"],{})[keys.prometheus]).toBeUndefined();
expect(plan("disable",["milvus"],{})[keys.etcd]).toBeUndefined();
expect(plan("disable",["etcd"],{})[keys.milvus]).toBe("false");
expect(plan("disable",["seaweedfs"],{})[keys.static]).toBe("false");
expect(plan("disable",["postgres"],{})[keys.mongodb]).toBe("false");
});
test("notifications require configuration before any changes",()=>{
expect(()=>plan("enable",["all","mail"],{})).toThrow("Configure notifications");
expect(()=>plan("enable",["slack"],{})).toThrow("Configure notifications");
const p=plan("enable",["slack"],{OPERATOR_CONFIG_SLACK_APIURL:"https://example.test",OPERATOR_CONFIG_SLACK_CHANNELNAME:"#test"});
expect(p[keys.prometheus]).toBe("true");expect(p[keys.alertmanager]).toBe("true");
});
test("preflight rejects missing frontend, dependencies and unconfigured alerts",()=>{
expect(()=>validate({})).toThrow("frontend");
const full=plan("full",[],{});
expect(()=>validate({...full,[keys.etcd]:"false"})).toThrow("milvus requires etcd");
expect(()=>validate({...full,[keys.mail]:"true"})).toThrow("Configure notifications");
});
test("unknown input is rejected",()=>{expect(()=>plan("enable",["bogus"],{})).toThrow("Unknown component")});
74 changes: 74 additions & 0 deletions config/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.

export const keys: Record<string,string> = Object.fromEntries(
["redis","mongodb","cron","static","postgres","prometheus","quota","milvus","registry","seaweedfs","etcd"].map(x => [x, `OPERATOR_COMPONENT_${x.toUpperCase()}`])
);
Object.assign(keys, {alertmanager:"OPERATOR_COMPONENT_AM", slack:"OPERATOR_CONFIG_ALERTSLACK", mail:"OPERATOR_CONFIG_ALERTGMAIL", affinity:"OPERATOR_CONFIG_AFFINITY", tolerations:"OPERATOR_CONFIG_TOLERATIONS"});
const dependencies: Record<string,string[]> = {mongodb:["postgres"], milvus:["etcd","seaweedfs"], static:["seaweedfs"], alertmanager:["prometheus"], slack:["alertmanager"], mail:["alertmanager"]};
export function plan(mode: string, flags: string[], env: Record<string,string|undefined>) {
if (!["enable","disable","full"].includes(mode)) throw new Error("Invalid configuration operation");
for (const flag of flags) if (flag !== "all" && !keys[flag]) throw new Error(`Unknown component: ${flag}`);
const on = mode !== "disable";
const selected = new Set(flags.filter(x => x !== "all"));
if (flags.includes("all") || mode === "full") for (const name of Object.keys(keys)) {
// Notification destinations require explicit opt-in. Full also excludes scheduling policies.
if (on && ["slack","mail"].includes(name)) continue;
if (mode === "full" && ["affinity","tolerations"].includes(name)) continue;
selected.add(name);
}
if (on) {
const include = (name: string) => {for (const dep of dependencies[name] || []) if (!selected.has(dep)) {selected.add(dep);include(dep)}};
for (const name of selected) include(name);
const required: Record<string,string[]> = {
slack:["OPERATOR_CONFIG_SLACK_APIURL","OPERATOR_CONFIG_SLACK_CHANNELNAME"],
mail:["OPERATOR_CONFIG_GMAIL_USERNAME","OPERATOR_CONFIG_GMAIL_PASSWORD","OPERATOR_CONFIG_EMAIL_FROM","OPERATOR_CONFIG_EMAIL_TO"]
};
for (const name of ["slack","mail"]) if (selected.has(name)) {
const missing = required[name].filter(k => !env[k]?.trim());
if (missing.length) throw new Error(`Configure notifications first with ops config ${name}; missing: ${missing.join(", ")}`);
}
} else {
// Disabling a prerequisite disables its dependants, never unrelated prerequisites.
let changed = true;
while (changed) {changed = false;for (const [name,deps] of Object.entries(dependencies)) {
if (!selected.has(name) && deps.some(dep => selected.has(dep))) {selected.add(name);changed=true;}
}}
}
return Object.fromEntries([...selected].map(name => [keys[name], String(on)]));
}
export function validate(env: Record<string,string|undefined>) {
const failures: string[] = [];
for (const name of ["seaweedfs","static"]) if (env[keys[name]] !== "true") failures.push(`frontend requires ${name}`);
for (const [name,deps] of Object.entries(dependencies)) if (env[keys[name]] === "true") {
for (const dep of deps) if (env[keys[dep]] !== "true") failures.push(`${name} requires ${dep}`);
}
for (const name of ["slack","mail"]) if (env[keys[name]] === "true") {
try {plan("enable",[name],env)} catch(e) {failures.push((e as Error).message)}
}
if (failures.length) throw new Error(`Invalid setup configuration: ${failures.join("; ")}. Review ops config enable/disable before retrying.`);
}
if (import.meta.main) {
try {
const mode = process.argv[2];
if (mode === "validate") validate(process.env);
else {
const flags = process.argv.slice(3).filter(x => x.endsWith("=true")).map(x => x.slice(2,-5));
for (const [key,value] of Object.entries(plan(mode,flags,process.env))) console.log(`${key}=${value}`);
}
} catch(e) {console.error(`ERROR: ${(e as Error).message}`);process.exitCode=1;}
}
16 changes: 14 additions & 2 deletions config/docopts.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Configure OpenServerless

```text
Usage:
config (enable|disable) [--all] [--redis] [--mongodb] [--cron] [--static] [--postgres] [--prometheus] [--slack] [--mail] [--affinity] [--tolerations] [--quota] [--milvus] [--registry] [--seaweedfs]
config (enable|disable) [--all] [--redis] [--mongodb] [--cron] [--static] [--postgres] [--prometheus] [--slack] [--mail] [--affinity] [--tolerations] [--quota] [--milvus] [--registry] [--seaweedfs] [--etcd] [--alertmanager]
config apihost (<apihost>|auto) [--tls=<email>] [--protocol=<http/https>|auto]
config sso
config runtimes [<runtimesjson>]
Expand All @@ -50,6 +50,7 @@ Usage:
config seaweedfs [--s3] [--console]
config (status|export|reset)
config use [<n>] [--delete] [--rename=<rename>]
config full
config minimal
config slim
```
Expand All @@ -60,6 +61,7 @@ Usage:
config apihost configure the apihost (auto: auto assign) and enable tls
config sso configure SSO/OIDC integration for admin-api; run `ops config sso --help` for details
config runtime show the current runtime.json or import the <runtime-json> if provided
config full enable all services without automatically enabling notification destinations or scheduling policies
config enable enable OpenServerless services to install
config disable disable OpenServerless services to install
config slack configure Alert Manager over a given slack channel
Expand Down Expand Up @@ -92,7 +94,9 @@ Usage:
## Options

```
--all select all services
--etcd select etcd (required by Milvus)
--alertmanager select Alert Manager (requires Prometheus)
--all select all services; enable excludes Slack/mail unless explicitly selected
--redis select redis
--mongodb select mongodb (FerretDB Proxy)
--cron select cron
Expand Down Expand Up @@ -125,3 +129,11 @@ Usage:
--class specify the ingress class. It can be auto, traefik, nginx. On microk8s it should be public.
--seaweedfs activate the support of SEAWEEDFS as S3 api and buckets provider
```

Enable automatically includes dependencies. Disable cascades to dependent services:
PostgreSQL → MongoDB/FerretDB; etcd → Milvus; SeaweedFS → Milvus/static;
Prometheus → Alert Manager → Slack/mail. Disabling a dependent does not disable
its prerequisites. Slack/mail require explicit selection and configured destinations.
`config full` preserves existing notification/scheduling selections; it does not
activate them. `disable --all` disables every selectable flag, including Alert Manager.
These commands change local configuration, not running Kubernetes resources.
Loading
Loading