mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-27 14:27:10 +00:00
feat(server): add the RUSTFS_S3_STACK selector and dual-stack e2e lane (#8411)
This commit is contained in:
@@ -16,7 +16,9 @@ name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
# gateway/integration receives the daily merge of main as a direct push
|
||||
# (rustfs/backlog#2734), so its pushes must prove the result like main's.
|
||||
branches: [ main, gateway/integration ]
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- "docs/**"
|
||||
@@ -780,6 +782,8 @@ jobs:
|
||||
needs: [ build-rustfs-debug-binary ]
|
||||
runs-on: sm-standard-2
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
RUSTFS_S3_STACK: legacy
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
@@ -898,18 +902,105 @@ jobs:
|
||||
path: ${{ runner.temp }}/rustfs-e2e-*/rustfs.log
|
||||
retention-days: 3
|
||||
|
||||
# The same e2e-smoke suite against the same debug binary with the gateway
|
||||
# S3 stack selected (rustfs/backlog#2734). Report-only: it is outside the
|
||||
# Test and Lint aggregate and becomes required at T2.10. The archive and run
|
||||
# steps must stay identical to e2e-tests; scripts/ci_gate.py enforces that.
|
||||
e2e-tests-gateway:
|
||||
name: e2e-smoke (gateway, report-only until T2.10)
|
||||
needs: [ build-rustfs-debug-binary ]
|
||||
runs-on: sm-standard-2
|
||||
timeout-minutes: 30
|
||||
continue-on-error: true
|
||||
env:
|
||||
RUSTFS_S3_STACK: gateway
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Rust environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
rust-version: stable
|
||||
cache-shared-key: ci-dev
|
||||
cache-save-if: 'false'
|
||||
install-build-packaging-tools: 'false'
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install awscurl
|
||||
run: |
|
||||
python3 -m pip install --user --upgrade pip "awscurl==0.44"
|
||||
echo "AWSCURL_PATH=$HOME/.local/bin/awscurl" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Verify awscurl
|
||||
run: test -x "$AWSCURL_PATH"
|
||||
|
||||
- name: Download debug binary
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: rustfs-debug-binary
|
||||
path: target/debug
|
||||
|
||||
- name: Make binary executable
|
||||
run: chmod +x ./target/debug/rustfs
|
||||
|
||||
- name: Archive e2e smoke test binaries
|
||||
env:
|
||||
NEXTEST_ARCHIVE: ${{ runner.temp }}/rustfs-e2e-smoke.tar.zst
|
||||
NEXTEST_LISTING: ${{ runner.temp }}/rustfs-e2e-smoke-list.json
|
||||
run: |
|
||||
cargo nextest archive --profile e2e-smoke -p e2e_test --archive-file "${NEXTEST_ARCHIVE}"
|
||||
cargo nextest list --profile e2e-smoke --archive-file "${NEXTEST_ARCHIVE}" --message-format json > "${NEXTEST_LISTING}"
|
||||
python3 ./scripts/check_test_wiring.py --check-profile e2e-smoke "${NEXTEST_LISTING}"
|
||||
./scripts/check_security_smoke_count.sh check "${NEXTEST_LISTING}"
|
||||
|
||||
- name: Run e2e smoke suite
|
||||
env:
|
||||
NEXTEST_ARCHIVE: ${{ runner.temp }}/rustfs-e2e-smoke.tar.zst
|
||||
RUSTFS_E2E_LOG_DIR: ${{ runner.temp }}/rustfs-e2e-smoke-logs
|
||||
SAMPLER_INTERVAL_SECS: "15"
|
||||
run: |
|
||||
./scripts/ci/resource_sampler.sh start e2e-smoke
|
||||
trap './scripts/ci/resource_sampler.sh stop' EXIT
|
||||
python3 scripts/e2e_binary.py run --features e2e-test-hooks -- cargo nextest run --profile e2e-smoke --archive-file "${NEXTEST_ARCHIVE}" \
|
||||
--status-level all --final-status-level all --failure-output final
|
||||
|
||||
- name: Upload e2e smoke diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-smoke-gateway-diagnostics-${{ github.run_number }}
|
||||
path: |
|
||||
${{ runner.temp }}/rustfs-e2e-smoke-logs/
|
||||
${{ runner.temp }}/rustfs-e2e-smoke-list.json
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload e2e smoke JUnit report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-smoke-gateway-junit-${{ github.run_number }}
|
||||
path: target/nextest/e2e-smoke/junit.xml
|
||||
if-no-files-found: warn
|
||||
|
||||
e2e-full:
|
||||
name: End-to-End Tests (full merge gate)
|
||||
# Merge gate only (backlog#1149 ci-5): the never-automated user-visible
|
||||
# suites — KMS, object_lock, multipart_auth, quota, checksum, encryption,
|
||||
# security-boundary, ... — via the e2e-full nextest profile. Too heavy for
|
||||
# every PR, so it is gated to main pushes, the merge queue, and manual
|
||||
# dispatch. protocols / the 7 cluster suites / replication / #[ignore] are
|
||||
# owned by other lanes (see .config/nextest.toml profile.e2e-full).
|
||||
# every PR, so it is gated to main and gateway/integration pushes, the merge
|
||||
# queue, and manual dispatch. protocols / the 7 cluster suites / replication
|
||||
# / #[ignore] are owned by other lanes (see .config/nextest.toml profile.e2e-full).
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event_name == 'merge_group' ||
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/gateway/integration'))
|
||||
needs: [ build-rustfs-debug-binary ]
|
||||
runs-on: sm-standard-2
|
||||
timeout-minutes: 55
|
||||
|
||||
@@ -19,9 +19,9 @@ Every PR enters `ci.yml`. The `classify-changes` job uses the base revision of `
|
||||
|
||||
PRs changing only Rust sources under `crates/e2e_test/src/` and known E2E selection digests (optionally with those documentation paths) use the `e2e` scope. They keep workspace validation, the server build, E2E smoke, and both S3 lanes; independent workflows retain their path selection. Connect boundary, serial ILM, optional protocol/rio-v2, and real io_uring jobs are skipped because their sources and test inputs are unchanged. Manifests, fixtures, shared nextest configuration, scripts, production sources, and unknown paths still select `full`. A dependency on `e2e_test` from another workspace member or reachable local dependency disables this shortcut. All non-PR events retain their full selection.
|
||||
|
||||
`required-checks` runs even after failed or skipped dependencies. `scripts/ci_gate.py verify` rejects missing jobs, unexpected jobs, failure, cancellation, and unexpected skips; optional lanes are required only on their declared events. `Workspace Test and Lint` is the ordinary Rust job, while `Test and Lint` uniquely names the aggregate. New validation jobs must update both its direct dependencies and the script contract. Test this wiring and its failure cases with `python3 scripts/ci_gate.py --self-test`.
|
||||
`required-checks` runs even after failed or skipped dependencies. `scripts/ci_gate.py verify` rejects missing jobs, unexpected jobs, failure, cancellation, and unexpected skips; optional lanes are required only on their declared events. `Workspace Test and Lint` is the ordinary Rust job, while `Test and Lint` uniquely names the aggregate. New validation jobs must update both its direct dependencies and the script contract. A report-only lane is registered in `REPORT_ONLY_JOBS` instead: it stays out of the aggregate, must keep its distinguishing environment, and must keep the steps it reruns identical to the required job it mirrors. Test this wiring and its failure cases with `python3 scripts/ci_gate.py --self-test`.
|
||||
|
||||
The `gateway/integration` ruleset (`24663286`) requires the same three contexts with the same review rules for the s3s to gateway integration branch (rustfs/backlog#2734). It omits `required_linear_history` because `gateway-integration-sync.yml` lands `--no-ff` merge commits from `main` on that branch every weekday, and it names the `rustfs-cla-assistant` app as the bypass actor that pushes them. `ci.yml` runs on pull requests targeting either branch.
|
||||
The `gateway/integration` ruleset (`24663286`) requires the same three contexts with the same review rules for the s3s to gateway integration branch (rustfs/backlog#2734). It omits `required_linear_history` because `gateway-integration-sync.yml` lands `--no-ff` merge commits from `main` on that branch every weekday, and it names the `rustfs-cla-assistant` app as the bypass actor that pushes them. `ci.yml` runs on pull requests targeting either branch and on pushes to either branch; a push to `gateway/integration` also runs `e2e-full`, because the daily merge commit lands without a pull request and must be proven like a `main` push.
|
||||
|
||||
Verify the live rule before changing merge policy:
|
||||
|
||||
@@ -54,6 +54,7 @@ The four site-replication state-writer concurrency proofs reserve the available
|
||||
| PR, non-doc change | `Build RustFS Debug Binary` | `ci.yml` `build-rustfs-debug-binary` | Via aggregate; prerequisite for black-box jobs | `python3 scripts/e2e_binary.py build --bins --features e2e-test-hooks` (binary plus its `rustfs.e2e.json` sidecar) |
|
||||
| PR, full selection | `io_uring Integration (real)` | `ci.yml` `uring-integration` | Via aggregate | `cargo test -p rustfs-ecstore --lib uring_ -- --test-threads=1 --nocapture` |
|
||||
| PR, non-doc change | `End-to-End Tests` | `ci.yml` `e2e-tests` | Via aggregate | `python3 scripts/e2e_binary.py run --features e2e-test-hooks -- cargo nextest run --profile e2e-smoke -p e2e_test`, then `./scripts/e2e-run.sh ./target/debug/rustfs <data-dir>` under the same wrapper; membership guards `scripts/check_test_wiring.py --check-profile e2e-smoke <listing.json>` and `scripts/check_security_smoke_count.sh check <listing.json>` |
|
||||
| PR targeting or push to `gateway/integration`, non-doc change | `e2e-smoke (gateway, report-only until T2.10)` | `ci.yml` `e2e-tests-gateway` | Report-only until rustfs/backlog#2734 T2.10 makes it required; `continue-on-error`, outside the aggregate | `RUSTFS_S3_STACK=gateway python3 scripts/e2e_binary.py run --features e2e-test-hooks -- cargo nextest run --profile e2e-smoke -p e2e_test`; `scripts/ci_gate.py` keeps its archive and run steps identical to `e2e-tests`, which sets `RUSTFS_S3_STACK=legacy` on this branch |
|
||||
| PR, non-doc change | `S3 Implemented Tests` | `ci.yml` `s3-implemented-tests` | Via aggregate | build `rustfs`, then `scripts/s3-tests/run.sh` with the job's `DEPLOY_MODE` / `TEST_MODE` / `MAXFAIL` env |
|
||||
| PR, non-doc change | `S3 Lifecycle Behavior Tests` | `ci.yml` `s3-lifecycle-behavior-tests` | Via aggregate | `scripts/s3-tests/run.sh` with the job's accelerated-scanner env |
|
||||
| PR touching `paths` in `audit.yml` | `Cargo Deny`, `Workflow Pin Report`, `Dependency Review` | `audit.yml` `cargo-deny`, `workflow-pin-report`, `dependency-review` | Report-only | `cargo deny check`; `scripts/security/check_workflow_pins.sh` |
|
||||
@@ -65,7 +66,7 @@ The four site-replication state-writer concurrency proofs reserve the available
|
||||
| PR touching `paths` in `oidc-keycloak.yml` | `OIDC Keycloak live gate` | `oidc-keycloak.yml` `oidc-keycloak-live` | Report-only | `cargo build --locked -p rustfs --bin rustfs`, then `bash scripts/test/oidc_keycloak_live.sh ./target/debug/rustfs` |
|
||||
| PR touching `paths` in `targets-integration.yml` | `PostgreSQL, MySQL, AMQP, and NATS` | `targets-integration.yml` `targets-live` | Report-only | start the containers as in the job, export the `RUSTFS_TEST_*` DSNs, then the job's `cargo test --locked -p rustfs-targets --test <name> -- --ignored --test-threads=1` commands |
|
||||
| PR, documentation-only selection | `Quick Checks`, `Typos`, `Test and Lint` | `ci.yml` `quick-checks`, `typos`, `required-checks` | Required directly or via aggregate | Quick Checks commands; `python3 scripts/ci_gate.py --self-test` |
|
||||
| `merge_group`; push to `main` | `End-to-End Tests (full merge gate)` | `ci.yml` `e2e-full` | Via aggregate on these events | `python3 scripts/e2e_binary.py run --features e2e-test-hooks -- cargo nextest run --profile e2e-full -p e2e_test` (CI adds `--binary "$RUSTFS_E2E_STARTUP_CAS_BINARY"` for the downloaded binary and sidecar) |
|
||||
| `merge_group`; push to `main` or `gateway/integration` | `End-to-End Tests (full merge gate)` | `ci.yml` `e2e-full` | Via aggregate on these events | `python3 scripts/e2e_binary.py run --features e2e-test-hooks -- cargo nextest run --profile e2e-full -p e2e_test` (CI adds `--binary "$RUSTFS_E2E_STARTUP_CAS_BINARY"` for the downloaded binary and sidecar) |
|
||||
|
||||
e2e filters live in `.config/nextest.toml`; extend a profile instead of adding a second selector. Before a profile runs, `scripts/check_test_wiring.py` compares its listing to the committed digest in `.config/e2e-<profile>-selection.txt`, so a silent test drop fails closed.
|
||||
|
||||
|
||||
@@ -24,12 +24,71 @@ use rustfs_config::{
|
||||
};
|
||||
use rustfs_credentials::{DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY, Masked};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
pub(crate) const LEGACY_ENV_RUSTFS_ROOT_USER: &str = "RUSTFS_ROOT_USER";
|
||||
pub(crate) const LEGACY_ENV_RUSTFS_ROOT_PASSWORD: &str = "RUSTFS_ROOT_PASSWORD";
|
||||
static LEGACY_CREDENTIAL_WARNED_KEYS: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
|
||||
|
||||
/// Development-only selector between the s3s-based S3 stack and the gateway
|
||||
/// stack while both coexist on the integration branch (rustfs/backlog#2734).
|
||||
/// It is removed when the gateway stack becomes the only one.
|
||||
const ENV_RUSTFS_S3_STACK: &str = "RUSTFS_S3_STACK";
|
||||
|
||||
/// The S3 protocol stack that serves requests.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum S3Stack {
|
||||
/// The s3s-based stack every release ships today.
|
||||
#[default]
|
||||
Legacy,
|
||||
/// The rustfs-gateway stack under construction.
|
||||
Gateway,
|
||||
}
|
||||
|
||||
/// A `RUSTFS_S3_STACK` value other than the exact names `legacy` and `gateway`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InvalidS3Stack {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for InvalidS3Stack {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, r#"{ENV_RUSTFS_S3_STACK} must be "legacy" or "gateway", got {:?}"#, self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for InvalidS3Stack {}
|
||||
|
||||
impl FromStr for S3Stack {
|
||||
type Err = InvalidS3Stack;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
// Exact names only: a near miss must refuse startup rather than fall
|
||||
// back to a stack the operator did not ask for.
|
||||
match value {
|
||||
"legacy" => Ok(Self::Legacy),
|
||||
"gateway" => Ok(Self::Gateway),
|
||||
_ => Err(InvalidS3Stack { value: value.to_owned() }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl S3Stack {
|
||||
/// Unset selects the default; a set but invalid value, including an empty
|
||||
/// one, is a startup error.
|
||||
fn from_env() -> std::io::Result<Self> {
|
||||
match std::env::var_os(ENV_RUSTFS_S3_STACK) {
|
||||
None => Ok(Self::default()),
|
||||
Some(value) => value
|
||||
.to_string_lossy()
|
||||
.parse()
|
||||
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn warn_legacy_credential_env_once(legacy_key: &str, canonical_key: &str) {
|
||||
let warned = LEGACY_CREDENTIAL_WARNED_KEYS.get_or_init(|| Mutex::new(HashSet::new()));
|
||||
let mut warned = match warned.lock() {
|
||||
@@ -148,6 +207,9 @@ pub struct Config {
|
||||
|
||||
/// Workload profile for adaptive buffer sizing
|
||||
pub buffer_profile: String,
|
||||
|
||||
/// S3 protocol stack selected by `RUSTFS_S3_STACK`
|
||||
pub s3_stack: S3Stack,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -179,6 +241,7 @@ impl Config {
|
||||
kms_allow_insecure_dev_defaults: false,
|
||||
buffer_profile_disable: false,
|
||||
buffer_profile: "GeneralPurpose".to_string(),
|
||||
s3_stack: S3Stack::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +295,7 @@ impl Config {
|
||||
|
||||
// Region is optional, but if not set, we should default to "us-east-1" for signing compatibility with AWS S3 clients
|
||||
let region = region.or_else(|| Some(RUSTFS_REGION.to_string()));
|
||||
let s3_stack = S3Stack::from_env()?;
|
||||
|
||||
Ok(Config {
|
||||
volumes,
|
||||
@@ -256,6 +320,7 @@ impl Config {
|
||||
kms_allow_insecure_dev_defaults,
|
||||
buffer_profile_disable,
|
||||
buffer_profile,
|
||||
s3_stack,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -299,6 +364,7 @@ impl std::fmt::Debug for Config {
|
||||
.field("kms_allow_insecure_dev_defaults", &self.kms_allow_insecure_dev_defaults)
|
||||
.field("buffer_profile_disable", &self.buffer_profile_disable)
|
||||
.field("buffer_profile", &self.buffer_profile)
|
||||
.field("s3_stack", &self.s3_stack)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#[allow(unsafe_op_in_unsafe_fn)]
|
||||
mod tests {
|
||||
use crate::config::cli::{InspectCommands, default_server_opts};
|
||||
use crate::config::{CommandResult, Config, Opt, TlsCommands};
|
||||
use crate::config::{CommandResult, Config, Opt, S3Stack, TlsCommands};
|
||||
use crate::storage_api::config_test::DisksLayout;
|
||||
use rustfs_config::{DEFAULT_CONSOLE_ADDRESS, DEFAULT_CONSOLE_ENABLE, DEFAULT_OBS_ENDPOINT, RUSTFS_REGION};
|
||||
use rustfs_credentials::{DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY};
|
||||
@@ -916,4 +916,61 @@ mod tests {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Resolves the server configuration with `RUSTFS_S3_STACK` set to `value`
|
||||
/// (`None` removes it), so each case starts from a known environment.
|
||||
fn config_with_s3_stack(value: Option<&str>) -> std::io::Result<Config> {
|
||||
temp_env::with_var("RUSTFS_S3_STACK", value, || Config::from_opt(Opt::parse_from(["rustfs", "/data/vol1"])))
|
||||
}
|
||||
|
||||
fn assert_s3_stack_rejected(value: &str) {
|
||||
let err = match config_with_s3_stack(Some(value)) {
|
||||
Ok(config) => panic!("RUSTFS_S3_STACK={value:?} must refuse startup, got {:?}", config.s3_stack),
|
||||
Err(err) => err,
|
||||
};
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
format!(r#"RUSTFS_S3_STACK must be "legacy" or "gateway", got "{value}""#)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn s3_stack_unset_selects_legacy() {
|
||||
let config = config_with_s3_stack(None).expect("unset RUSTFS_S3_STACK should parse");
|
||||
assert_eq!(config.s3_stack, S3Stack::Legacy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn s3_stack_legacy_selects_legacy() {
|
||||
let config = config_with_s3_stack(Some("legacy")).expect("RUSTFS_S3_STACK=legacy should parse");
|
||||
assert_eq!(config.s3_stack, S3Stack::Legacy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn s3_stack_gateway_selects_gateway() {
|
||||
let config = config_with_s3_stack(Some("gateway")).expect("RUSTFS_S3_STACK=gateway should parse");
|
||||
assert_eq!(config.s3_stack, S3Stack::Gateway);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn s3_stack_rejects_capitalized_gateway() {
|
||||
assert_s3_stack_rejected("Gateway");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn s3_stack_rejects_both() {
|
||||
assert_s3_stack_rejected("both");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn s3_stack_rejects_empty_value() {
|
||||
assert_s3_stack_rejected("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ pub use cli::{ConnectTopCaptureOpts, ConnectTopCommands};
|
||||
pub use cli::{DiagnoseFormat, DiagnoseOpts};
|
||||
pub use cli::{InspectBucketMetaOpts, InspectCommands, InspectOpts};
|
||||
pub use cli::{TlsCommands, TlsInspectOpts, TlsOpts};
|
||||
pub use config_struct::Config;
|
||||
pub use config_struct::{Config, InvalidS3Stack, S3Stack};
|
||||
pub use info::execute_info;
|
||||
pub use opt::Opt;
|
||||
pub(crate) use snapshot::get_config_snapshot;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
config::{Config, S3Stack},
|
||||
server::{ServiceState, ServiceStateManager, ShutdownHandle, start_http_server},
|
||||
startup_runtime_sources,
|
||||
storage_api::server::http::ServerContextSlot,
|
||||
@@ -41,6 +41,7 @@ const EVENT_SERVER_STARTING: &str = "server_starting";
|
||||
const EVENT_ACTION_CREDENTIALS_INITIALIZED: &str = "action_credentials_initialized";
|
||||
const EVENT_ACTION_CREDENTIALS_INITIALIZATION_FAILED: &str = "action_credentials_initialization_failed";
|
||||
const DEFAULT_CREDENTIALS_WARNING_MESSAGE: &str = "Detected default root credentials; set RUSTFS_ACCESS_KEY and RUSTFS_SECRET_KEY to non-default values for production deployments";
|
||||
const GATEWAY_STACK_NOT_ASSEMBLED: &str = "gateway stack is not assembled yet (rustfs/backlog#2734 T2.5)";
|
||||
|
||||
pub(crate) struct StartupListenContext {
|
||||
pub(crate) readiness: Arc<GlobalReadiness>,
|
||||
@@ -81,6 +82,12 @@ pub(crate) async fn init_startup_listen_context(
|
||||
config: &Config,
|
||||
instance_ctx: &Arc<InstanceContext>,
|
||||
) -> Result<StartupListenContext> {
|
||||
match config.s3_stack {
|
||||
S3Stack::Legacy => {}
|
||||
// REPLACED BY T2.5
|
||||
// Refuse before any listener or storage exists; never fall back to legacy.
|
||||
S3Stack::Gateway => return Err(Error::new(ErrorKind::Unsupported, GATEWAY_STACK_NOT_ASSEMBLED)),
|
||||
}
|
||||
crate::server::init_console_prefix()?;
|
||||
log_sanitized_server_config(config);
|
||||
let readiness = Arc::new(GlobalReadiness::new());
|
||||
|
||||
+61
-3
@@ -36,6 +36,14 @@ E2E_SELECTION_FILES = {
|
||||
}
|
||||
OPTIONAL_JOBS = ("build-rustfs-debug-binary-rio-v2", "e2e-tests-rio-v2", "e2e-full")
|
||||
NON_VALIDATION_JOBS = {"required-checks", "cancel-closed-pr-runs", "alert-on-failure"}
|
||||
# Report-only lanes stay outside the aggregate until their milestone makes them
|
||||
# required. Each reruns a required job's suite with one environment change, so
|
||||
# the named steps must stay identical to that job's and the change must stay.
|
||||
REPORT_ONLY_JOBS = {
|
||||
# rustfs/backlog#2734: required at T2.10.
|
||||
"e2e-tests-gateway": ("e2e-tests", ("Archive e2e smoke test binaries", "Run e2e smoke suite"), "RUSTFS_S3_STACK: gateway"),
|
||||
}
|
||||
INTEGRATION_PUSH_REFS = ("refs/heads/main", "refs/heads/gateway/integration")
|
||||
|
||||
|
||||
def documentation_path(path: str) -> bool:
|
||||
@@ -129,7 +137,7 @@ def expected_results(mode: str, event: str, ref: str) -> dict[str, str]:
|
||||
expected.update({job: "success" if mode == "full" or (mode == "e2e" and job in E2E_JOBS) else "skipped" for job in CODE_JOBS})
|
||||
rio = mode == "full" and event in ("schedule", "workflow_dispatch")
|
||||
expected.update({job: "success" if rio else "skipped" for job in OPTIONAL_JOBS[:2]})
|
||||
full = mode == "full" and (event in ("merge_group", "workflow_dispatch") or (event == "push" and ref == "refs/heads/main"))
|
||||
full = mode == "full" and (event in ("merge_group", "workflow_dispatch") or (event == "push" and ref in INTEGRATION_PUSH_REFS))
|
||||
expected["e2e-full"] = "success" if full else "skipped"
|
||||
return expected
|
||||
|
||||
@@ -157,7 +165,7 @@ def verify_results(needs: object, event: str, ref: str) -> list[str]:
|
||||
|
||||
def check_workflow(root: Path) -> list[str]:
|
||||
# Reuse the repository's canonical-indentation checker; actionlint validates YAML syntax.
|
||||
from check_test_wiring import yaml_block, yaml_scalar_continues
|
||||
from check_test_wiring import workflow_step_block, yaml_block, yaml_scalar_continues
|
||||
|
||||
errors = []
|
||||
lines = (root / ".github/workflows/ci.yml").read_text().splitlines()
|
||||
@@ -176,8 +184,23 @@ def check_workflow(root: Path) -> list[str]:
|
||||
names.add(name)
|
||||
jobs[index] = f" {name}:"
|
||||
required = set(ALWAYS_JOBS + CODE_JOBS + OPTIONAL_JOBS)
|
||||
if names - NON_VALIDATION_JOBS != required:
|
||||
if names - NON_VALIDATION_JOBS - set(REPORT_ONLY_JOBS) != required:
|
||||
errors.append("CI verification jobs and the required gate contract differ")
|
||||
|
||||
def code(block):
|
||||
return [line for line in block if line.strip() and not line.lstrip().startswith("#")]
|
||||
|
||||
for job, (mirror, steps, env) in REPORT_ONLY_JOBS.items():
|
||||
block, mirrored = yaml_block(jobs, job, 2), yaml_block(jobs, mirror, 2)
|
||||
if block is None or mirrored is None:
|
||||
errors.append(f"report-only {job} and its required {mirror} must both exist")
|
||||
continue
|
||||
if f" {env}" not in (yaml_block(block, "env", 4) or []):
|
||||
errors.append(f"{job} must set {env}")
|
||||
for step in steps:
|
||||
ours, theirs = workflow_step_block(block, step, "name"), workflow_step_block(mirrored, step, "name")
|
||||
if ours is None or theirs is None or code(ours[1]) != code(theirs[1]):
|
||||
errors.append(f"{job} step {step!r} must match {mirror}")
|
||||
for job in required:
|
||||
block = yaml_block(jobs, job, 2) or []
|
||||
if any(re.match(r"\s+(?:- )?[\"']?continue-on-error[\"']?\s*:", line) for line in block):
|
||||
@@ -352,6 +375,8 @@ class SelfTests(unittest.TestCase):
|
||||
def test_full_e2e_gate_preserves_workflow_branch_and_event_scope(self):
|
||||
for event, ref, required in (
|
||||
("push", "refs/heads/main", "success"),
|
||||
("push", "refs/heads/gateway/integration", "success"),
|
||||
("push", "refs/heads/gateway/integration-old", "skipped"),
|
||||
("push", "refs/heads/release", "skipped"),
|
||||
("push", "refs/heads/feature", "skipped"),
|
||||
("push", "refs/heads/release-candidate", "skipped"),
|
||||
@@ -403,6 +428,39 @@ class SelfTests(unittest.TestCase):
|
||||
path.write_text(source + "\n cancel-after-test-and-lint-failure:\n runs-on: ubuntu-latest\n")
|
||||
self.assertTrue(check_workflow(root))
|
||||
|
||||
def test_report_only_stack_lane_mirrors_e2e_smoke_outside_the_aggregate(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / ".github/workflows").mkdir(parents=True)
|
||||
source = (ROOT / ".github/workflows/ci.yml").read_text()
|
||||
path = root / ".github/workflows/ci.yml"
|
||||
before, lane = source.split(" e2e-tests-gateway:\n", 1)
|
||||
lane, after = lane.split(" e2e-full:\n", 1)
|
||||
legacy = before.split(" e2e-tests:\n", 1)[1]
|
||||
|
||||
def with_lane(text):
|
||||
return before + " e2e-tests-gateway:\n" + text + " e2e-full:\n" + after
|
||||
|
||||
smoke = "--profile e2e-smoke --archive-file"
|
||||
run_ignored = "--profile e2e-smoke --run-ignored all --archive-file"
|
||||
for case, broken in enumerate((
|
||||
# The lane can neither gate the aggregate nor vanish.
|
||||
source.replace(" - e2e-tests\n", " - e2e-tests\n - e2e-tests-gateway\n", 1),
|
||||
before + " e2e-full:\n" + after,
|
||||
# It must select the gateway stack.
|
||||
with_lane(lane.replace("RUSTFS_S3_STACK: gateway", "RUSTFS_S3_STACK: legacy")),
|
||||
with_lane(lane.replace(" env:\n RUSTFS_S3_STACK: gateway\n", "")),
|
||||
# Its suite must not drift from the required lane in either direction.
|
||||
with_lane(lane.replace(smoke, run_ignored)),
|
||||
with_lane(lane).replace(legacy, legacy.replace(smoke, run_ignored), 1),
|
||||
with_lane(lane.replace("check_security_smoke_count.sh check", "check_security_smoke_count.sh update")),
|
||||
with_lane(lane.replace(" - name: Run e2e smoke suite\n", " - name: Run gateway smoke suite\n")),
|
||||
)):
|
||||
path.write_text(broken)
|
||||
self.assertTrue(check_workflow(root), case)
|
||||
path.write_text(with_lane(lane.replace(" if-no-files-found: warn\n", " if-no-files-found: ignore\n")))
|
||||
self.assertEqual(check_workflow(root), [], "only the mirrored archive and run steps are bound")
|
||||
|
||||
def test_job_ids_and_display_names_cannot_hide_validation(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
|
||||
Reference in New Issue
Block a user