diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d4ab5e4..aeb5d64e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ assembles all relevant Kubernetes resources before anything is applied ([#814]). - The RBAC ServiceAccount and RoleBinding are now built with the operator-rs `v2::rbac` functions and carry the full set of recommended labels ([#821]). -- Bump stackable-operator to 0.114.0 ([#827]). +- Bump stackable-operator to 0.116.0 ([#827], [#838]). - The reconciler now applies resources and derives the cluster status in discrete apply and update_status steps ([#828]). - The level configured for the `airflow.task` logger now sets the level of the `task` handler, @@ -23,7 +23,16 @@ the logger as well, because a logger discards records before any handler can filter them, so in that direction the UI and the other destinations open up together and cannot be set apart ([#829]). +- Remove the `app.kubernetes.io/component` and `app.kubernetes.io/role-group` labels from the + resources they don't apply to (previously set to `none`) ([#838]). - All product containers now run with `securityContext.runAsNonRoot` set to `true` to improve security ([#840]). +- `envOverrides` names are now validated by the shared `EnvVarName` type rather than by + operator-specific validation code ([#838]). +- Remove the `app.kubernetes.io/component` and `app.kubernetes.io/role-group` labels from the + resources they don't apply to (previously set to `none` or a placeholder value) ([#838]). +- Environment variable overrides (`envOverrides`) are now applied after all environment + variables set by the operator. In particular, `CONTAINERDEBUG_LOG_DIRECTORY` can now be + overridden, whereas previously the operator's value always took precedence ([#838]). ### Fixed @@ -40,6 +49,7 @@ [#829]: https://github.com/stackabletech/airflow-operator/pull/829 [#834]: https://github.com/stackabletech/airflow-operator/pull/834 [#835]: https://github.com/stackabletech/airflow-operator/pull/835 +[#838]: https://github.com/stackabletech/airflow-operator/pull/838 [#840]: https://github.com/stackabletech/airflow-operator/pull/840 [#844]: https://github.com/stackabletech/airflow-operator/pull/844 diff --git a/Cargo.lock b/Cargo.lock index 7540e3ae..224e699e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1714,7 +1714,7 @@ dependencies = [ [[package]] name = "k8s-version" version = "0.1.3" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#fb2d86579f4e3df008f78f0e527a012243483a2d" +source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#7b9f9ac9a76fa425ab27f2821377ef86571ca121" dependencies = [ "darling 0.24.0", "regex", @@ -3177,7 +3177,7 @@ dependencies = [ [[package]] name = "stackable-certs" version = "0.4.1" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#fb2d86579f4e3df008f78f0e527a012243483a2d" +source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#7b9f9ac9a76fa425ab27f2821377ef86571ca121" dependencies = [ "const-oid", "ecdsa", @@ -3200,8 +3200,8 @@ dependencies = [ [[package]] name = "stackable-operator" -version = "0.115.0" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#fb2d86579f4e3df008f78f0e527a012243483a2d" +version = "0.116.0" +source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#7b9f9ac9a76fa425ab27f2821377ef86571ca121" dependencies = [ "base64 0.23.1", "clap", @@ -3245,7 +3245,7 @@ dependencies = [ [[package]] name = "stackable-operator-derive" version = "0.3.1" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#fb2d86579f4e3df008f78f0e527a012243483a2d" +source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#7b9f9ac9a76fa425ab27f2821377ef86571ca121" dependencies = [ "darling 0.24.0", "proc-macro2", @@ -3256,7 +3256,7 @@ dependencies = [ [[package]] name = "stackable-shared" version = "0.1.2" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#fb2d86579f4e3df008f78f0e527a012243483a2d" +source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#7b9f9ac9a76fa425ab27f2821377ef86571ca121" dependencies = [ "jiff", "k8s-openapi", @@ -3273,7 +3273,7 @@ dependencies = [ [[package]] name = "stackable-telemetry" version = "0.6.5" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#fb2d86579f4e3df008f78f0e527a012243483a2d" +source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#7b9f9ac9a76fa425ab27f2821377ef86571ca121" dependencies = [ "axum", "clap", @@ -3297,7 +3297,7 @@ dependencies = [ [[package]] name = "stackable-versioned" version = "0.11.1" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#fb2d86579f4e3df008f78f0e527a012243483a2d" +source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#7b9f9ac9a76fa425ab27f2821377ef86571ca121" dependencies = [ "kube", "schemars", @@ -3311,7 +3311,7 @@ dependencies = [ [[package]] name = "stackable-versioned-macros" version = "0.11.1" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#fb2d86579f4e3df008f78f0e527a012243483a2d" +source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#7b9f9ac9a76fa425ab27f2821377ef86571ca121" dependencies = [ "convert_case", "convert_case_extras", @@ -3329,7 +3329,7 @@ dependencies = [ [[package]] name = "stackable-webhook" version = "0.9.2" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#fb2d86579f4e3df008f78f0e527a012243483a2d" +source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#7b9f9ac9a76fa425ab27f2821377ef86571ca121" dependencies = [ "arc-swap", "async-trait", diff --git a/Cargo.nix b/Cargo.nix index 25187a5e..8493e176 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -5433,8 +5433,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; - rev = "fb2d86579f4e3df008f78f0e527a012243483a2d"; - sha256 = "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb"; + rev = "7b9f9ac9a76fa425ab27f2821377ef86571ca121"; + sha256 = "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9"; }; libName = "k8s_version"; authors = [ @@ -10418,6 +10418,11 @@ rec { name = "serde_yaml"; packageId = "serde_yaml"; } + { + name = "stackable-operator"; + packageId = "stackable-operator"; + features = [ "crds" "webhook" "test-support" ]; + } ]; }; @@ -10428,8 +10433,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; - rev = "fb2d86579f4e3df008f78f0e527a012243483a2d"; - sha256 = "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb"; + rev = "7b9f9ac9a76fa425ab27f2821377ef86571ca121"; + sha256 = "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9"; }; libName = "stackable_certs"; authors = [ @@ -10526,13 +10531,13 @@ rec { }; "stackable-operator" = rec { crateName = "stackable-operator"; - version = "0.115.0"; + version = "0.116.0"; edition = "2024"; workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; - rev = "fb2d86579f4e3df008f78f0e527a012243483a2d"; - sha256 = "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb"; + rev = "7b9f9ac9a76fa425ab27f2821377ef86571ca121"; + sha256 = "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9"; }; libName = "stackable_operator"; authors = [ @@ -10712,12 +10717,13 @@ rec { "client-feature-gates" = [ "dep:winnow" ]; "crds" = [ "dep:stackable-versioned" ]; "default" = [ "crds" ]; - "full" = [ "client-feature-gates" "crds" "certs" "test-support" "time" "webhook" "kube-ws" ]; + "full" = [ "client-feature-gates" "crds" "certs" "test-support" "time" "webhook" "kube-ws" "kube-cel" ]; + "kube-cel" = [ "kube/cel" ]; "kube-ws" = [ "kube/ws" ]; "time" = [ "stackable-shared/time" ]; "webhook" = [ "dep:stackable-webhook" ]; }; - resolvedDefaultFeatures = [ "crds" "default" "webhook" ]; + resolvedDefaultFeatures = [ "crds" "default" "test-support" "webhook" ]; }; "stackable-operator-derive" = rec { crateName = "stackable-operator-derive"; @@ -10726,8 +10732,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; - rev = "fb2d86579f4e3df008f78f0e527a012243483a2d"; - sha256 = "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb"; + rev = "7b9f9ac9a76fa425ab27f2821377ef86571ca121"; + sha256 = "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9"; }; procMacro = true; libName = "stackable_operator_derive"; @@ -10761,8 +10767,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; - rev = "fb2d86579f4e3df008f78f0e527a012243483a2d"; - sha256 = "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb"; + rev = "7b9f9ac9a76fa425ab27f2821377ef86571ca121"; + sha256 = "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9"; }; libName = "stackable_shared"; authors = [ @@ -10842,8 +10848,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; - rev = "fb2d86579f4e3df008f78f0e527a012243483a2d"; - sha256 = "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb"; + rev = "7b9f9ac9a76fa425ab27f2821377ef86571ca121"; + sha256 = "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9"; }; libName = "stackable_telemetry"; authors = [ @@ -10952,8 +10958,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; - rev = "fb2d86579f4e3df008f78f0e527a012243483a2d"; - sha256 = "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb"; + rev = "7b9f9ac9a76fa425ab27f2821377ef86571ca121"; + sha256 = "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9"; }; libName = "stackable_versioned"; authors = [ @@ -11002,8 +11008,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; - rev = "fb2d86579f4e3df008f78f0e527a012243483a2d"; - sha256 = "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb"; + rev = "7b9f9ac9a76fa425ab27f2821377ef86571ca121"; + sha256 = "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9"; }; procMacro = true; libName = "stackable_versioned_macros"; @@ -11070,8 +11076,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech/operator-rs.git"; - rev = "fb2d86579f4e3df008f78f0e527a012243483a2d"; - sha256 = "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb"; + rev = "7b9f9ac9a76fa425ab27f2821377ef86571ca121"; + sha256 = "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9"; }; libName = "stackable_webhook"; authors = [ diff --git a/Cargo.toml b/Cargo.toml index 70bb8663..089b52ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ repository = "https://github.com/stackabletech/airflow-operator" stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", features = [ "crds", "webhook", -], tag = "stackable-operator-0.115.0" } +], tag = "stackable-operator-0.116.0" } anyhow = "1.0" built = { version = "0.8", features = ["chrono", "git2"] } diff --git a/crate-hashes.json b/crate-hashes.json index 43cd4b72..3fae0f9f 100644 --- a/crate-hashes.json +++ b/crate-hashes.json @@ -1,11 +1,11 @@ { - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#k8s-version@0.1.3": "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#stackable-certs@0.4.1": "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#stackable-operator-derive@0.3.1": "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#stackable-operator@0.115.0": "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#stackable-shared@0.1.2": "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#stackable-telemetry@0.6.5": "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#stackable-versioned-macros@0.11.1": "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#stackable-versioned@0.11.1": "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.115.0#stackable-webhook@0.9.2": "1w57n5xx0ik63r252l1v5ymm51jlsf7v4pj682b902k8vinlhyqb" + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#k8s-version@0.1.3": "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#stackable-certs@0.4.1": "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#stackable-operator-derive@0.3.1": "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#stackable-operator@0.116.0": "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#stackable-shared@0.1.2": "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#stackable-telemetry@0.6.5": "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#stackable-versioned-macros@0.11.1": "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#stackable-versioned@0.11.1": "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.116.0#stackable-webhook@0.9.2": "1p3744fxgvs12sqwvi8hhainwrgvhdfwmbyqf0sp0aq3awq3q1v9" } \ No newline at end of file diff --git a/extra/crds.yaml b/extra/crds.yaml index 5972f8cd..282db8fc 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -492,7 +492,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -1006,7 +1007,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -2020,7 +2022,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -2534,7 +2537,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -3098,7 +3102,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -3591,7 +3596,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -4105,7 +4111,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -4607,7 +4614,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -5121,7 +5129,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -5623,7 +5632,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -6164,7 +6174,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -6740,7 +6751,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -7254,7 +7266,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -8244,7 +8257,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -8758,7 +8772,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -9322,7 +9337,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -9815,7 +9831,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -10329,7 +10346,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -10831,7 +10849,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -11345,7 +11364,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -11847,7 +11867,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about @@ -12388,7 +12409,8 @@ spec: default: {} description: |- `envOverrides` configure environment variables to be set in the Pods. - It is a map from strings to strings - environment variables and the value to set. + It is a map from environment variable names to their values. The names are validated to be + valid environment variable names. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index 92feeb34..d12eee75 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -31,3 +31,4 @@ built.workspace = true [dev-dependencies] rstest.workspace = true serde_yaml.workspace = true +stackable-operator = { workspace = true, features = ["test-support"] } diff --git a/rust/operator-binary/src/airflow_controller.rs b/rust/operator-binary/src/airflow_controller.rs index cbe4af8f..106a6996 100644 --- a/rust/operator-binary/src/airflow_controller.rs +++ b/rust/operator-binary/src/airflow_controller.rs @@ -1,15 +1,11 @@ //! Ensures that `Pod`s are configured and running for each [`v1alpha2::AirflowCluster`] -use std::{ - collections::{BTreeMap, HashMap}, - sync::Arc, -}; +use std::sync::Arc; use const_format::concatcp; use snafu::{ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, cluster_resources::ClusterResourceApplyStrategy, - k8s_openapi::api::core::v1::EnvVar, kube::{ core::{DeserializeGuard, error_boundary}, runtime::controller::Action, @@ -138,48 +134,3 @@ pub fn error_policy( _ => Action::requeue(*Duration::from_secs(10)), } } - -/// Convert user-supplied `envOverrides` into a list of [`EnvVar`]s. -pub(crate) fn env_vars_from_overrides(env_overrides: &HashMap) -> Vec { - // Collect into a `BTreeMap` first so the env vars come out in a deterministic (sorted) order; - // `HashMap` iteration order is randomised per instance and would otherwise churn the containers - // this feeds between reconciles. Mirrors the override handling in `env_vars.rs`. - env_overrides - .iter() - .collect::>() - .into_iter() - .map(|(k, v)| EnvVar { - name: k.clone(), - value: Some(v.clone()), - ..EnvVar::default() - }) - .collect() -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - - use super::env_vars_from_overrides; - - /// The env vars must come out in a deterministic (sorted-by-name) order. `env_overrides` is a - /// `HashMap`, whose iteration order is randomised per instance, so iterating it directly would - /// vary the rendered env array between reconciles and churn the git-sync containers it feeds. - #[test] - fn env_vars_from_overrides_are_sorted_by_name() { - let overrides = HashMap::from([ - ("CHARLIE".to_string(), "3".to_string()), - ("ALPHA".to_string(), "1".to_string()), - ("ECHO".to_string(), "5".to_string()), - ("BRAVO".to_string(), "2".to_string()), - ("DELTA".to_string(), "4".to_string()), - ]); - - let names: Vec = env_vars_from_overrides(&overrides) - .into_iter() - .map(|env_var| env_var.name) - .collect(); - - assert_eq!(names, ["ALPHA", "BRAVO", "CHARLIE", "DELTA", "ECHO"]); - } -} diff --git a/rust/operator-binary/src/controller/apply.rs b/rust/operator-binary/src/controller/apply.rs index c3be9c58..e6de8d57 100644 --- a/rust/operator-binary/src/controller/apply.rs +++ b/rust/operator-binary/src/controller/apply.rs @@ -14,8 +14,8 @@ use strum::{EnumDiscriminants, IntoStaticStr}; use crate::{ controller::{ - Applied, KubernetesResources, Prepared, ValidatedCluster, controller_name, operator_name, - product_name, + Applied, CONTROLLER_NAME, KubernetesResources, OPERATOR_NAME, PRODUCT_NAME, Prepared, + ValidatedCluster, }, crd::internal_secret::{ FERNET_KEY_SECRET_KEY, INTERNAL_SECRET_SECRET_KEY, JWT_SECRET_SECRET_KEY, @@ -60,9 +60,9 @@ impl<'a> Applier<'a> { object_overrides: &'a ObjectOverrides, ) -> Applier<'a> { let cluster_resources = cluster_resources_new( - &product_name(), - &operator_name(), - &controller_name(), + &PRODUCT_NAME, + &OPERATOR_NAME, + &CONTROLLER_NAME, &cluster.name, &cluster.namespace, &cluster.uid, @@ -135,7 +135,7 @@ impl<'a> Applier<'a> { pub async fn ensure_random_secrets(client: &Client, cluster: &ValidatedCluster) -> Result<()> { random_secret_creation::create_random_secret_if_not_exists( cluster.internal_secret_name().as_ref(), - INTERNAL_SECRET_SECRET_KEY, + &INTERNAL_SECRET_SECRET_KEY.to_string(), 256, cluster, client, @@ -145,7 +145,7 @@ pub async fn ensure_random_secrets(client: &Client, cluster: &ValidatedCluster) random_secret_creation::create_random_secret_if_not_exists( cluster.jwt_secret_name().as_ref(), - JWT_SECRET_SECRET_KEY, + &JWT_SECRET_SECRET_KEY.to_string(), 256, cluster, client, @@ -159,7 +159,7 @@ pub async fn ensure_random_secrets(client: &Client, cluster: &ValidatedCluster) // which returns 32 bytes. random_secret_creation::create_random_secret_if_not_exists( cluster.fernet_key_name().as_ref(), - FERNET_KEY_SECRET_KEY, + &FERNET_KEY_SECRET_KEY.to_string(), 32, cluster, client, diff --git a/rust/operator-binary/src/controller/build/mod.rs b/rust/operator-binary/src/controller/build/mod.rs index 5d36cbb4..4ae2438f 100644 --- a/rust/operator-binary/src/controller/build/mod.rs +++ b/rust/operator-binary/src/controller/build/mod.rs @@ -6,12 +6,16 @@ use snafu::{ResultExt, Snafu}; use stackable_operator::{ builder::meta::ObjectMetaBuilder, kvp::Labels, - v2::{builder::meta::ownerreference_from_resource, types::operator::RoleGroupName}, + v2::{ + builder::meta::ownerreference_from_resource, + types::operator::{RoleGroupName, RoleName}, + }, }; use crate::{ controller::{ - KubernetesResources, Prepared, ValidatedCluster, + CONTROLLER_NAME, KubernetesResources, OPERATOR_NAME, PRODUCT_NAME, Prepared, + ValidatedCluster, build::resource::{ config_map::build_rolegroup_config_map, executor::build_executor_template_config_map, @@ -122,7 +126,7 @@ pub fn build(cluster: &ValidatedCluster) -> Result config_maps.push( build_rolegroup_config_map( cluster, - &ValidatedCluster::role_name(role), + role, role_group_name, &rg_config.config_overrides, logging, @@ -178,6 +182,75 @@ pub(crate) fn object_meta( builder } +pub(crate) fn recommended_labels_for_cluster_resources(cluster: &ValidatedCluster) -> Labels { + stackable_operator::v2::kvp::label::recommended_labels_for_cluster_resources( + &cluster.name, + &PRODUCT_NAME, + &cluster.product_version, + &OPERATOR_NAME, + &CONTROLLER_NAME, + ) +} + +pub(crate) fn recommended_labels_for_role_resources( + cluster: &ValidatedCluster, + role_name: &RoleName, +) -> Labels { + stackable_operator::v2::kvp::label::recommended_labels_for_role_resources( + &cluster.name, + &PRODUCT_NAME, + &cluster.product_version, + &OPERATOR_NAME, + &CONTROLLER_NAME, + role_name, + ) +} + +pub(crate) fn recommended_labels_for_role_group_resources( + cluster: &ValidatedCluster, + role_name: &RoleName, + role_group_name: &RoleGroupName, +) -> Labels { + stackable_operator::v2::kvp::label::recommended_labels_for_role_group_resources( + &cluster.name, + &PRODUCT_NAME, + &cluster.product_version, + &OPERATOR_NAME, + &CONTROLLER_NAME, + role_name, + role_group_name, + ) +} + +pub(crate) fn recommended_labels_for_unversioned_role_group_resources( + cluster: &ValidatedCluster, + role_name: &RoleName, + role_group_name: &RoleGroupName, +) -> Labels { + stackable_operator::v2::kvp::label::recommended_labels_for_unversioned_role_group_resources( + &cluster.name, + &PRODUCT_NAME, + &OPERATOR_NAME, + &CONTROLLER_NAME, + role_name, + role_group_name, + ) +} + +/// Selector labels matching the pods of a role group. +pub(crate) fn role_group_selector( + cluster: &ValidatedCluster, + role_name: &RoleName, + role_group_name: &RoleGroupName, +) -> Labels { + stackable_operator::v2::kvp::label::role_group_selector( + &cluster.name, + &PRODUCT_NAME, + role_name, + role_group_name, + ) +} + #[cfg(test)] pub(crate) mod test_support { use crate::{ @@ -371,10 +444,13 @@ mod tests { } /// The environment of the `airflow` container of the given StatefulSet, as name/value pairs. + /// + /// This reads the rendered `EnvVar`s straight off the built container rather than going through + /// `EnvVarSet`, keeping the test helper lean and focused on the values it asserts on. fn airflow_container_env( cluster: &ValidatedCluster, stateful_set_name: &str, - ) -> BTreeMap> { + ) -> BTreeMap { let resources = build(cluster).expect("build succeeds"); let stateful_set = resources .stateful_sets @@ -398,7 +474,12 @@ mod tests { .as_ref() .expect("the airflow container has env vars") .iter() - .map(|env_var| (env_var.name.clone(), env_var.value.clone())) + .filter_map(|env_var| { + env_var + .value + .as_ref() + .map(|value| (env_var.name.clone(), value.clone())) + }) .collect() } @@ -475,14 +556,12 @@ mod tests { let expected_labels = BTreeMap::from( [ - ("app.kubernetes.io/component", "none"), ("app.kubernetes.io/instance", "my-airflow"), ( "app.kubernetes.io/managed-by", "airflow.stackable.tech_airflowcluster", ), ("app.kubernetes.io/name", "airflow"), - ("app.kubernetes.io/role-group", "none"), ("app.kubernetes.io/version", &app_version_label("3.1.6")), ("stackable.tech/vendor", "Stackable"), ] @@ -531,7 +610,7 @@ mod tests { assert_eq!( env.get("FORWARDED_ALLOW_IPS"), - Some(&Some("10.244.0.0/16,192.168.1.1".to_string())) + Some(&"10.244.0.0/16,192.168.1.1".to_owned()) ); } @@ -591,11 +670,11 @@ mod tests { assert_eq!(env.get("FORWARDED_ALLOW_IPS"), None); assert_eq!( env.get("AIRFLOW__WEBSERVER__ENABLE_PROXY_FIX"), - Some(&Some("True".to_string())) + Some(&"True".to_owned()) ); assert_eq!( env.get("AIRFLOW__WEBSERVER__PROXY_FIX_X_FOR"), - Some(&Some("1".to_string())) + Some(&"1".to_owned()) ); } @@ -606,7 +685,7 @@ mod tests { let env = airflow_container_env(&cluster, "my-airflow-webserver-default"); assert_eq!( env.get("AIRFLOW__WEBSERVER__ENABLE_PROXY_FIX"), - Some(&Some("True".to_string())) + Some(&"True".to_owned()) ); } diff --git a/rust/operator-binary/src/controller/build/properties/env_vars.rs b/rust/operator-binary/src/controller/build/properties/env_vars.rs index 347848e6..b4fe6403 100644 --- a/rust/operator-binary/src/controller/build/properties/env_vars.rs +++ b/rust/operator-binary/src/controller/build/properties/env_vars.rs @@ -1,15 +1,16 @@ -use std::{ - collections::{BTreeMap, BTreeSet, HashMap}, - path::PathBuf, -}; +use std::{collections::BTreeSet, path::PathBuf, str::FromStr}; use snafu::Snafu; use stackable_operator::{ + constant, crd::{authentication::oidc, git_sync}, - k8s_openapi::api::core::v1::EnvVar, kube::ResourceExt, product_logging::framework::create_vector_shutdown_file_command, - v2::{builder::pod::container::EnvVarSet, product_logging::framework::STACKABLE_LOG_DIR}, + v2::{ + builder::pod::container::{EnvVarName, EnvVarSet}, + product_logging::framework::STACKABLE_LOG_DIR, + types::kubernetes::SecretKey, + }, }; use crate::{ @@ -25,40 +26,61 @@ use crate::{ }, trusted_proxies::TrustedProxy, }, - util::{env_var_from_secret, role_service_name}, + util::role_service_name, }; -const AIRFLOW_CORE_AUTH_MANAGER: &str = "AIRFLOW__CORE__AUTH_MANAGER"; +constant!(AIRFLOW_CORE_AUTH_MANAGER: EnvVarName = "AIRFLOW__CORE__AUTH_MANAGER"); // Airflow 3 envs -const AIRFLOW_CORE_AUTH_OPA_REQUEST_URL: &str = "AIRFLOW__CORE__AUTH_OPA_REQUEST_URL"; -const AIRFLOW_CORE_AUTH_OPA_CACHE_TTL_IN_SEC: &str = "AIRFLOW__CORE__AUTH_OPA_CACHE_TTL_IN_SEC"; -const AIRFLOW_CORE_AUTH_OPA_CACHE_MAXSIZE: &str = "AIRFLOW__CORE__AUTH_OPA_CACHE_MAXSIZE"; - -const AIRFLOW_LOGGING_LOGGING_CONFIG_CLASS: &str = "AIRFLOW__LOGGING__LOGGING_CONFIG_CLASS"; -const AIRFLOW_METRICS_STATSD_ON: &str = "AIRFLOW__METRICS__STATSD_ON"; -const AIRFLOW_METRICS_STATSD_HOST: &str = "AIRFLOW__METRICS__STATSD_HOST"; -const AIRFLOW_METRICS_STATSD_PORT: &str = "AIRFLOW__METRICS__STATSD_PORT"; -const AIRFLOW_WEBSERVER_SECRET_KEY: &str = "AIRFLOW__WEBSERVER__SECRET_KEY"; -const AIRFLOW_CELERY_RESULT_BACKEND: &str = "AIRFLOW__CELERY__RESULT_BACKEND"; -const AIRFLOW_CELERY_BROKER_URL: &str = "AIRFLOW__CELERY__BROKER_URL"; -const AIRFLOW_CORE_DAGS_FOLDER: &str = "AIRFLOW__CORE__DAGS_FOLDER"; -const AIRFLOW_CORE_LOAD_EXAMPLES: &str = "AIRFLOW__CORE__LOAD_EXAMPLES"; -const AIRFLOW_API_AUTH_BACKENDS: &str = "AIRFLOW__API__AUTH_BACKENDS"; -const AIRFLOW_DATABASE_SQL_ALCHEMY_CONN: &str = "AIRFLOW__DATABASE__SQL_ALCHEMY_CONN"; - -const AIRFLOW_WEBSERVER_EXPOSE_CONFIG: &str = "AIRFLOW__WEBSERVER__EXPOSE_CONFIG"; -const AIRFLOW_CORE_EXECUTOR: &str = "AIRFLOW__CORE__EXECUTOR"; -const AIRFLOW_KUBERNETES_EXECUTOR_POD_TEMPLATE_FILE: &str = - "AIRFLOW__KUBERNETES_EXECUTOR__POD_TEMPLATE_FILE"; -const AIRFLOW_KUBERNETES_EXECUTOR_NAMESPACE: &str = "AIRFLOW__KUBERNETES_EXECUTOR__NAMESPACE"; - -const ADMIN_FIRSTNAME: &str = "ADMIN_FIRSTNAME"; -const ADMIN_USERNAME: &str = "ADMIN_USERNAME"; -const ADMIN_LASTNAME: &str = "ADMIN_LASTNAME"; -const ADMIN_PASSWORD: &str = "ADMIN_PASSWORD"; -const ADMIN_EMAIL: &str = "ADMIN_EMAIL"; - -const PYTHONPATH: &str = "PYTHONPATH"; +constant!(AIRFLOW_CORE_AUTH_OPA_REQUEST_URL: EnvVarName = "AIRFLOW__CORE__AUTH_OPA_REQUEST_URL"); +constant!(AIRFLOW_CORE_AUTH_OPA_CACHE_TTL_IN_SEC: EnvVarName = "AIRFLOW__CORE__AUTH_OPA_CACHE_TTL_IN_SEC"); +constant!(AIRFLOW_CORE_AUTH_OPA_CACHE_MAXSIZE: EnvVarName = "AIRFLOW__CORE__AUTH_OPA_CACHE_MAXSIZE"); + +constant!(AIRFLOW_LOGGING_LOGGING_CONFIG_CLASS: EnvVarName = "AIRFLOW__LOGGING__LOGGING_CONFIG_CLASS"); +constant!(AIRFLOW_METRICS_STATSD_ON: EnvVarName = "AIRFLOW__METRICS__STATSD_ON"); +constant!(AIRFLOW_METRICS_STATSD_HOST: EnvVarName = "AIRFLOW__METRICS__STATSD_HOST"); +constant!(AIRFLOW_METRICS_STATSD_PORT: EnvVarName = "AIRFLOW__METRICS__STATSD_PORT"); +constant!(AIRFLOW_WEBSERVER_SECRET_KEY: EnvVarName = "AIRFLOW__WEBSERVER__SECRET_KEY"); +constant!(AIRFLOW_API_SECRET_KEY: EnvVarName = "AIRFLOW__API__SECRET_KEY"); +constant!(AIRFLOW_CORE_FERNET_KEY: EnvVarName = "AIRFLOW__CORE__FERNET_KEY"); +constant!(AIRFLOW_CELERY_RESULT_BACKEND: EnvVarName = "AIRFLOW__CELERY__RESULT_BACKEND"); +constant!(AIRFLOW_CELERY_BROKER_URL: EnvVarName = "AIRFLOW__CELERY__BROKER_URL"); +constant!(AIRFLOW_CORE_DAGS_FOLDER: EnvVarName = "AIRFLOW__CORE__DAGS_FOLDER"); +constant!(AIRFLOW_CORE_LOAD_EXAMPLES: EnvVarName = "AIRFLOW__CORE__LOAD_EXAMPLES"); +constant!(AIRFLOW_API_AUTH_BACKENDS: EnvVarName = "AIRFLOW__API__AUTH_BACKENDS"); +constant!(AIRFLOW_WEBSERVER_ENABLE_PROXY_FIX: EnvVarName = "AIRFLOW__WEBSERVER__ENABLE_PROXY_FIX"); +constant!(AIRFLOW_WEBSERVER_PROXY_FIX_X_FOR: EnvVarName = "AIRFLOW__WEBSERVER__PROXY_FIX_X_FOR"); +constant!(AIRFLOW_SCHEDULER_STANDALONE_DAG_PROCESSOR: EnvVarName = "AIRFLOW__SCHEDULER__STANDALONE_DAG_PROCESSOR"); +constant!(AIRFLOW_API_AUTH_JWT_SECRET: EnvVarName = "AIRFLOW__API_AUTH__JWT_SECRET"); +constant!(AIRFLOW_API_WORKERS: EnvVarName = "AIRFLOW__API__WORKERS"); +constant!(AIRFLOW_FAB_UPDATE_FAB_PERMS: EnvVarName = "AIRFLOW__FAB__UPDATE_FAB_PERMS"); +constant!(FORWARDED_ALLOW_IPS: EnvVarName = "FORWARDED_ALLOW_IPS"); +constant!(AIRFLOW_DATABASE_SQL_ALCHEMY_CONN: EnvVarName = "AIRFLOW__DATABASE__SQL_ALCHEMY_CONN"); + +constant!(AIRFLOW_WEBSERVER_EXPOSE_CONFIG: EnvVarName = "AIRFLOW__WEBSERVER__EXPOSE_CONFIG"); +constant!(AIRFLOW_CORE_EXECUTOR: EnvVarName = "AIRFLOW__CORE__EXECUTOR"); +constant!(AIRFLOW_KUBERNETES_EXECUTOR_POD_TEMPLATE_FILE: EnvVarName = + "AIRFLOW__KUBERNETES_EXECUTOR__POD_TEMPLATE_FILE"); +constant!(AIRFLOW_KUBERNETES_EXECUTOR_NAMESPACE: EnvVarName = "AIRFLOW__KUBERNETES_EXECUTOR__NAMESPACE"); + +constant!(AIRFLOW_CORE_EXECUTION_API_SERVER_URL: EnvVarName = "AIRFLOW__CORE__EXECUTION_API_SERVER_URL"); +constant!(AIRFLOW_CORE_BASE_URL: EnvVarName = "AIRFLOW__CORE__BASE_URL"); + +constant!(ADMIN_FIRSTNAME: EnvVarName = "ADMIN_FIRSTNAME"); +constant!(ADMIN_USERNAME: EnvVarName = "ADMIN_USERNAME"); +constant!(ADMIN_LASTNAME: EnvVarName = "ADMIN_LASTNAME"); +constant!(ADMIN_PASSWORD: EnvVarName = "ADMIN_PASSWORD"); +constant!(ADMIN_EMAIL: EnvVarName = "ADMIN_EMAIL"); + +constant!(ADMIN_USERNAME_SECRET_KEY: SecretKey = "adminUser.username"); +constant!(ADMIN_FIRSTNAME_SECRET_KEY: SecretKey = "adminUser.firstname"); +constant!(ADMIN_LASTNAME_SECRET_KEY: SecretKey = "adminUser.lastname"); +constant!(ADMIN_EMAIL_SECRET_KEY: SecretKey = "adminUser.email"); +constant!(ADMIN_PASSWORD_SECRET_KEY: SecretKey = "adminUser.password"); + +constant!(PYTHONPATH: EnvVarName = "PYTHONPATH"); + +constant!(CONTAINERDEBUG_LOG_DIRECTORY: EnvVarName = "CONTAINERDEBUG_LOG_DIRECTORY"); +constant!(STACKABLE_POST_HOOK: EnvVarName = "_STACKABLE_POST_HOOK"); #[derive(Snafu, Debug)] pub enum Error { @@ -76,7 +98,7 @@ pub fn build_airflow_statefulset_envs( airflow_role: &AirflowRole, env_overrides: &EnvVarSet, git_sync_resources: &git_sync::v1alpha2::GitSyncResources, -) -> Result, Error> { +) -> EnvVarSet { let executor = &cluster.cluster_config.executor; let auth_config = &cluster.cluster_config.authentication_config; let authorization_config = &cluster.cluster_config.authorization_config; @@ -84,138 +106,67 @@ pub fn build_airflow_statefulset_envs( let metadata_database_connection_details = cluster.metadata_database_connection_details(); let celery_database_connection_details = cluster.celery_database_connection_details(); - let mut env: BTreeMap = BTreeMap::new(); let internal_secret_name = cluster.internal_secret_name(); - env.extend(static_envs(git_sync_resources)); - - add_version_specific_env_vars(cluster, airflow_role, &mut env); - - // N.B. this has been deprecated and replaced with AIRFLOW__API__SECRET_KEY since 3.0.2. Can be removed when 3.0.1 is no longer supported. - env.insert( - AIRFLOW_WEBSERVER_SECRET_KEY.into(), - // The secret key is used to run the webserver flask app and also - // used to authorize requests to Celery workers when logs are retrieved. - env_var_from_secret( - AIRFLOW_WEBSERVER_SECRET_KEY, + let mut env_vars = static_envs(git_sync_resources) + .merge(add_version_specific_env_vars(cluster, airflow_role)) + // N.B. this has been deprecated and replaced with AIRFLOW__API__SECRET_KEY since 3.0.2. Can be removed when 3.0.1 is no longer supported. + .with_secret_key_ref( + &AIRFLOW_WEBSERVER_SECRET_KEY, &internal_secret_name, - INTERNAL_SECRET_SECRET_KEY, - ), - ); - // Replaces AIRFLOW__WEBSERVER__SECRET_KEY >= 3.0.2. - env.insert( - "AIRFLOW__API__SECRET_KEY".into(), - env_var_from_secret( - "AIRFLOW__API__SECRET_KEY", + &INTERNAL_SECRET_SECRET_KEY, + ) + // Replaces AIRFLOW__WEBSERVER__SECRET_KEY >= 3.0.2. + .with_secret_key_ref( + &AIRFLOW_API_SECRET_KEY, &internal_secret_name, - INTERNAL_SECRET_SECRET_KEY, - ), - ); - - env.insert( - "AIRFLOW__CORE__FERNET_KEY".into(), - env_var_from_secret( - "AIRFLOW__CORE__FERNET_KEY", - cluster.fernet_key_name(), - FERNET_KEY_SECRET_KEY, - ), - ); + &INTERNAL_SECRET_SECRET_KEY, + ) + .with_secret_key_ref( + &AIRFLOW_CORE_FERNET_KEY, + &cluster.fernet_key_name(), + &FERNET_KEY_SECRET_KEY, + ) + .with_value( + &AIRFLOW_DATABASE_SQL_ALCHEMY_CONN, + metadata_database_connection_details.url_template, + ); - env.insert( - AIRFLOW_DATABASE_SQL_ALCHEMY_CONN.into(), - EnvVar { - name: AIRFLOW_DATABASE_SQL_ALCHEMY_CONN.into(), - value: Some(metadata_database_connection_details.url_template.clone()), - ..Default::default() - }, - ); // Only needed when celery executors are used if let Some((celery_result_backend, celery_broker)) = celery_database_connection_details { - env.insert( - AIRFLOW_CELERY_RESULT_BACKEND.into(), - EnvVar { - name: AIRFLOW_CELERY_RESULT_BACKEND.into(), - value: Some(celery_result_backend.url_template.clone()), - ..Default::default() - }, - ); - env.insert( - AIRFLOW_CELERY_BROKER_URL.into(), - EnvVar { - name: AIRFLOW_CELERY_BROKER_URL.into(), - value: Some(celery_broker.url_template.clone()), - ..Default::default() - }, - ); + env_vars = env_vars + .with_value( + &AIRFLOW_CELERY_RESULT_BACKEND, + celery_result_backend.url_template, + ) + .with_value(&AIRFLOW_CELERY_BROKER_URL, celery_broker.url_template); } let dags_folder = get_dags_folder(git_sync_resources); - env.insert( - AIRFLOW_CORE_DAGS_FOLDER.into(), - EnvVar { - name: AIRFLOW_CORE_DAGS_FOLDER.into(), - value: Some(dags_folder), - ..Default::default() - }, - ); - - if cluster.cluster_config.load_examples { - env.insert( - AIRFLOW_CORE_LOAD_EXAMPLES.into(), - EnvVar { - name: AIRFLOW_CORE_LOAD_EXAMPLES.into(), - value: Some("True".into()), - ..Default::default() - }, - ); - } else { - env.insert( - AIRFLOW_CORE_LOAD_EXAMPLES.into(), - EnvVar { - name: AIRFLOW_CORE_LOAD_EXAMPLES.into(), - value: Some("False".into()), - ..Default::default() + env_vars = env_vars + .with_value(&AIRFLOW_CORE_DAGS_FOLDER, dags_folder) + .with_value( + &AIRFLOW_CORE_LOAD_EXAMPLES, + if cluster.cluster_config.load_examples { + "True" + } else { + "False" }, ); - } if cluster.cluster_config.expose_config { - env.insert( - AIRFLOW_WEBSERVER_EXPOSE_CONFIG.into(), - EnvVar { - name: AIRFLOW_WEBSERVER_EXPOSE_CONFIG.into(), - value: Some("True".into()), - ..Default::default() - }, - ); + env_vars = env_vars.with_value(&AIRFLOW_WEBSERVER_EXPOSE_CONFIG, "True"); } - env.insert( - AIRFLOW_CORE_EXECUTOR.into(), - EnvVar { - name: AIRFLOW_CORE_EXECUTOR.into(), - value: Some(executor.as_airflow_core_executor().to_owned()), - ..Default::default() - }, - ); + env_vars = env_vars.with_value(&AIRFLOW_CORE_EXECUTOR, executor.as_airflow_core_executor()); if let AirflowExecutor::KubernetesExecutors { .. } = executor { - env.insert( - AIRFLOW_KUBERNETES_EXECUTOR_POD_TEMPLATE_FILE.into(), - EnvVar { - name: AIRFLOW_KUBERNETES_EXECUTOR_POD_TEMPLATE_FILE.into(), - value: Some(format!("{TEMPLATE_LOCATION}/{TEMPLATE_NAME}")), - ..Default::default() - }, - ); - env.insert( - AIRFLOW_KUBERNETES_EXECUTOR_NAMESPACE.into(), - EnvVar { - name: AIRFLOW_KUBERNETES_EXECUTOR_NAMESPACE.into(), - value: Some(cluster.namespace.to_string()), - ..Default::default() - }, - ); + env_vars = env_vars + .with_value( + &AIRFLOW_KUBERNETES_EXECUTOR_POD_TEMPLATE_FILE, + format!("{TEMPLATE_LOCATION}/{TEMPLATE_NAME}"), + ) + .with_value(&AIRFLOW_KUBERNETES_EXECUTOR_NAMESPACE, &cluster.namespace); } match airflow_role { @@ -223,56 +174,38 @@ pub fn build_airflow_statefulset_envs( // See https://github.com/stackabletech/airflow-operator/issues/259 AirflowRole::Scheduler => { let secret = &cluster.cluster_config.credentials_secret_name; - env.insert( - ADMIN_USERNAME.into(), - env_var_from_secret(ADMIN_USERNAME, secret, "adminUser.username"), - ); - env.insert( - ADMIN_FIRSTNAME.into(), - env_var_from_secret(ADMIN_FIRSTNAME, secret, "adminUser.firstname"), - ); - env.insert( - ADMIN_LASTNAME.into(), - env_var_from_secret(ADMIN_LASTNAME, secret, "adminUser.lastname"), - ); - env.insert( - ADMIN_EMAIL.into(), - env_var_from_secret(ADMIN_EMAIL, secret, "adminUser.email"), - ); - env.insert( - ADMIN_PASSWORD.into(), - env_var_from_secret(ADMIN_PASSWORD, secret, "adminUser.password"), - ); + env_vars = env_vars + .with_secret_key_ref(&ADMIN_USERNAME, secret, &ADMIN_USERNAME_SECRET_KEY) + .with_secret_key_ref(&ADMIN_FIRSTNAME, secret, &ADMIN_FIRSTNAME_SECRET_KEY) + .with_secret_key_ref(&ADMIN_LASTNAME, secret, &ADMIN_LASTNAME_SECRET_KEY) + .with_secret_key_ref(&ADMIN_EMAIL, secret, &ADMIN_EMAIL_SECRET_KEY) + .with_secret_key_ref(&ADMIN_PASSWORD, secret, &ADMIN_PASSWORD_SECRET_KEY); } AirflowRole::Webserver => { - let mut vars = authentication_env_vars(auth_config); - vars.extend(authorization_env_vars( - authorization_config, - &resolved_product_image.product_version, - )); - env.extend(vars.into_iter().map(|var| (var.name.to_owned(), var))); + env_vars = + env_vars + .merge(authentication_env_vars(auth_config)) + .merge(authorization_env_vars( + authorization_config, + &resolved_product_image.product_version, + )); } _ => {} } - // apply overrides last of all; `EnvVarSet` is keyed by name, so iteration is already - // in a fixed (sorted-by-name) order - for env_var in env_overrides.clone() { - env.insert(env_var.name.clone(), env_var); - } - // Needed for the `containerdebug` process to log it's tracing information to. - env.insert( - "CONTAINERDEBUG_LOG_DIRECTORY".to_string(), - EnvVar { - name: "CONTAINERDEBUG_LOG_DIRECTORY".to_string(), - value: Some(format!("{STACKABLE_LOG_DIR}/containerdebug")), - value_from: None, - }, - ); - - tracing::debug!("Env-var set [{:?}]", env); - Ok(transform_map_to_vec(env)) + env_vars = env_vars + .with_value( + &CONTAINERDEBUG_LOG_DIRECTORY, + format!("{STACKABLE_LOG_DIR}/containerdebug"), + ) + // apply overrides last of all; `EnvVarSet` is keyed by name, so iteration is already + // in a fixed (sorted-by-name) order + .merge(env_overrides.clone()); + + tracing::debug!("Env-var set: {:?}", Vec::from(&env_vars)); + + env_vars } pub fn get_dags_folder(git_sync_resources: &git_sync::v1alpha2::GitSyncResources) -> String { @@ -296,224 +229,108 @@ pub fn get_dags_folder(git_sync_resources: &git_sync::v1alpha2::GitSyncResources // This set of environment variables is a standard set that is not dependent on any // conditional logic and should be applied to the statefulset or the executor template config map. -fn static_envs( - git_sync_resources: &git_sync::v1alpha2::GitSyncResources, -) -> BTreeMap { - let mut env: BTreeMap = BTreeMap::new(); - +fn static_envs(git_sync_resources: &git_sync::v1alpha2::GitSyncResources) -> EnvVarSet { let dags_folder = get_dags_folder(git_sync_resources); - env.insert( - PYTHONPATH.into(), - EnvVar { - // PYTHONPATH must be extended to include the dags folder so that dag - // dependencies can be found: this must be the actual path and not a variable. - // Also include the airflow site-packages by default (for airflow and kubernetes classes etc.) - name: PYTHONPATH.into(), - value: Some(format!("{LOG_CONFIG_DIR}:{dags_folder}")), - ..Default::default() - }, - ); - env.insert( - AIRFLOW_LOGGING_LOGGING_CONFIG_CLASS.into(), - EnvVar { - name: AIRFLOW_LOGGING_LOGGING_CONFIG_CLASS.into(), - value: Some("log_config.LOGGING_CONFIG".into()), - ..Default::default() - }, - ); - - env.insert( - AIRFLOW_METRICS_STATSD_ON.into(), - EnvVar { - name: AIRFLOW_METRICS_STATSD_ON.into(), - value: Some("True".into()), - ..Default::default() - }, - ); - - env.insert( - AIRFLOW_METRICS_STATSD_HOST.into(), - EnvVar { - name: AIRFLOW_METRICS_STATSD_HOST.into(), - value: Some("0.0.0.0".into()), - ..Default::default() - }, - ); - - env.insert( - AIRFLOW_METRICS_STATSD_PORT.into(), - EnvVar { - name: AIRFLOW_METRICS_STATSD_PORT.into(), - value: Some("9125".into()), - ..Default::default() - }, - ); - - env + EnvVarSet::new() + // PYTHONPATH must be extended to include the dags folder so that dag + // dependencies can be found: this must be the actual path and not a variable. + // Also include the airflow site-packages by default (for airflow and kubernetes classes etc.) + .with_value(&PYTHONPATH, format!("{LOG_CONFIG_DIR}:{dags_folder}")) + .with_value( + &AIRFLOW_LOGGING_LOGGING_CONFIG_CLASS, + "log_config.LOGGING_CONFIG", + ) + .with_value(&AIRFLOW_METRICS_STATSD_ON, "True") + .with_value(&AIRFLOW_METRICS_STATSD_HOST, "0.0.0.0") + .with_value(&AIRFLOW_METRICS_STATSD_PORT, "9125") } /// Return environment variables to be applied to the configuration map used in conjunction with /// the `kubernetesExecutor` worker. pub fn build_airflow_template_envs( cluster: &ValidatedCluster, - env_overrides: &HashMap, + env_overrides: &EnvVarSet, logging: &ValidatedLogging, git_sync_resources: &git_sync::v1alpha2::GitSyncResources, -) -> Vec { - let mut env: BTreeMap = BTreeMap::new(); - - env.insert( - AIRFLOW_DATABASE_SQL_ALCHEMY_CONN.into(), - EnvVar { - name: AIRFLOW_DATABASE_SQL_ALCHEMY_CONN.into(), - value: Some( - cluster - .metadata_database_connection_details() - .url_template - .clone(), - ), - ..Default::default() - }, - ); - - env.insert( - AIRFLOW_CORE_EXECUTOR.into(), - EnvVar { - name: AIRFLOW_CORE_EXECUTOR.into(), - value: Some("LocalExecutor".to_string()), - ..Default::default() - }, - ); - - env.insert( - AIRFLOW_KUBERNETES_EXECUTOR_NAMESPACE.into(), - EnvVar { - name: AIRFLOW_KUBERNETES_EXECUTOR_NAMESPACE.into(), - value: Some(cluster.namespace.to_string()), - ..Default::default() - }, - ); - +) -> EnvVarSet { // the config map also requires the dag-folder location as this will be passed on // to the pods started by airflow. let dags_folder = get_dags_folder(git_sync_resources); - env.insert( - AIRFLOW_CORE_DAGS_FOLDER.into(), - EnvVar { - name: AIRFLOW_CORE_DAGS_FOLDER.into(), - value: Some(dags_folder), - ..Default::default() - }, - ); - - env.extend(static_envs(git_sync_resources)); - add_version_specific_env_vars(cluster, &AirflowRole::Worker, &mut env); + let mut env_vars = EnvVarSet::new() + .with_value( + &AIRFLOW_DATABASE_SQL_ALCHEMY_CONN, + cluster.metadata_database_connection_details().url_template, + ) + .with_value(&AIRFLOW_CORE_EXECUTOR, "LocalExecutor") + .with_value(&AIRFLOW_KUBERNETES_EXECUTOR_NAMESPACE, &cluster.namespace) + .with_value(&AIRFLOW_CORE_DAGS_FOLDER, dags_folder) + .merge(static_envs(git_sync_resources)) + .merge(add_version_specific_env_vars(cluster, &AirflowRole::Worker)); // _STACKABLE_POST_HOOK will contain a command to create a shutdown hook that will be // evaluated in the wrapper for each stackable spark container: this is necessary for pods // that are created and then terminated (we do a similar thing for spark-k8s). if logging.enable_vector_agent { - env.insert( - "_STACKABLE_POST_HOOK".into(), - EnvVar { - name: "_STACKABLE_POST_HOOK".into(), - value: Some( - [ - // Wait for Vector to gather the logs. - "sleep 10", - &create_vector_shutdown_file_command(STACKABLE_LOG_DIR), - ] - .join("; "), - ), - ..Default::default() - }, + env_vars = env_vars.with_value( + &STACKABLE_POST_HOOK, + [ + // Wait for Vector to gather the logs. + "sleep 10", + &create_vector_shutdown_file_command(STACKABLE_LOG_DIR), + ] + .join("; "), ); } - // iterate over a BTreeMap to ensure the vars are written in a predictable order - for (k, v) in env_overrides.iter().collect::>() { - env.insert( - k.to_string(), - EnvVar { - name: k.to_string(), - value: Some(v.to_string()), - ..Default::default() - }, - ); - } + env_vars = env_vars.merge(env_overrides.clone()); + + tracing::debug!("Env-var set [{:?}]", env_vars); - tracing::debug!("Env-var set [{:?}]", env); - transform_map_to_vec(env) + env_vars } fn add_version_specific_env_vars( cluster: &ValidatedCluster, airflow_role: &AirflowRole, - env: &mut BTreeMap, -) { +) -> EnvVarSet { + let mut env_vars = EnvVarSet::new(); + if cluster.image.product_version.starts_with("3.") { - env.extend(execution_server_env_vars(cluster)); - env.insert( - AIRFLOW_CORE_AUTH_MANAGER.into(), - EnvVar { - name: AIRFLOW_CORE_AUTH_MANAGER.into(), - value: Some( - "airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager" - .to_string(), - ), - ..Default::default() - }, - ); - env.insert( - AIRFLOW_API_AUTH_BACKENDS.into(), - EnvVar { - name: AIRFLOW_API_AUTH_BACKENDS.into(), - value: Some("airflow.api.auth.backend.session".into()), - ..Default::default() - }, - ); - // As of 3.x a JWT key is required. - // See https://airflow.apache.org/docs/apache-airflow/stable/configurations-ref.html#jwt-secret - // This should be random, but must also be consistent across - // api-services and replicas/roles for a given - // cluster, but should also be cluster-specific. - // It is accessed from a secret to avoid cluster restarts - // being triggered by an operator restart. - env.insert( - "AIRFLOW__API_AUTH__JWT_SECRET".into(), - env_var_from_secret( - "AIRFLOW__API_AUTH__JWT_SECRET", - cluster.jwt_secret_name(), - JWT_SECRET_SECRET_KEY, - ), - ); - // The Airflow default for this is 4. - // However, with the default resources this could cause problems, - // as the Pod went to 100% CPU usage and didn't get healthy - // quick enough, resulting in a crashloop. - env.insert( - "AIRFLOW__API__WORKERS".into(), - EnvVar { - name: "AIRFLOW__API__WORKERS".into(), - value: Some("1".into()), - ..Default::default() - }, - ); + env_vars = env_vars + .merge(execution_server_env_vars(cluster)) + .with_value( + &AIRFLOW_CORE_AUTH_MANAGER, + "airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager", + ) + .with_value( + &AIRFLOW_API_AUTH_BACKENDS, + "airflow.api.auth.backend.session", + ) + // As of 3.x a JWT key is required. + // See https://airflow.apache.org/docs/apache-airflow/stable/configurations-ref.html#jwt-secret + // This should be random, but must also be consistent across + // api-services and replicas/roles for a given + // cluster, but should also be cluster-specific. + // It is accessed from a secret to avoid cluster restarts + // being triggered by an operator restart. + .with_secret_key_ref( + &AIRFLOW_API_AUTH_JWT_SECRET, + &cluster.jwt_secret_name(), + &JWT_SECRET_SECRET_KEY, + ) + // The Airflow default for this is 4. + // However, with the default resources this could cause problems, + // as the Pod went to 100% CPU usage and didn't get healthy + // quick enough, resulting in a crashloop. + .with_value(&AIRFLOW_API_WORKERS, "1"); if airflow_role == &AirflowRole::Webserver { // Sometimes a race condition can arise when both scheduler and // api-server are updating the DB, which adds overhead (conflicts // are logged) and can result in inconsistencies. This setting // ensure that only the scheduler will do this by default. - env.insert( - "AIRFLOW__FAB__UPDATE_FAB_PERMS".into(), - EnvVar { - name: "AIRFLOW__FAB__UPDATE_FAB_PERMS".into(), - value: Some("False".into()), - ..Default::default() - }, - ); + env_vars = env_vars.with_value(&AIRFLOW_FAB_UPDATE_FAB_PERMS, "False"); // This env var is needed in addition to `--proxy-headers` when Airflow runs // behind a reverse proxy. @@ -529,26 +346,13 @@ fn add_version_specific_env_vars( .join(","); if !trusted_proxies.is_empty() { - env.insert( - "FORWARDED_ALLOW_IPS".into(), - EnvVar { - name: "FORWARDED_ALLOW_IPS".into(), - value: Some(trusted_proxies), - ..Default::default() - }, - ); + env_vars = env_vars.with_value(&FORWARDED_ALLOW_IPS, trusted_proxies); } } } else { - env.insert( - AIRFLOW_API_AUTH_BACKENDS.into(), - EnvVar { - name: AIRFLOW_API_AUTH_BACKENDS.into(), - value: Some( - "airflow.api.auth.backend.basic_auth, airflow.api.auth.backend.session".into(), - ), - ..Default::default() - }, + env_vars = env_vars.with_value( + &AIRFLOW_API_AUTH_BACKENDS, + "airflow.api.auth.backend.basic_auth, airflow.api.auth.backend.session", ); // The 2.x uses Werkzeug's `ProxyFix` to allow forwarded-headers and it does so regardless @@ -561,22 +365,9 @@ fn add_version_specific_env_vars( .unwrap_or_default(); if !trusted_proxies.is_empty() { - env.insert( - "AIRFLOW__WEBSERVER__ENABLE_PROXY_FIX".into(), - EnvVar { - name: "AIRFLOW__WEBSERVER__ENABLE_PROXY_FIX".into(), - value: Some("True".into()), - ..Default::default() - }, - ); - env.insert( - "AIRFLOW__WEBSERVER__PROXY_FIX_X_FOR".into(), - EnvVar { - name: "AIRFLOW__WEBSERVER__PROXY_FIX_X_FOR".into(), - value: Some("1".into()), - ..Default::default() - }, - ); + env_vars = env_vars + .with_value(&AIRFLOW_WEBSERVER_ENABLE_PROXY_FIX, "True") + .with_value(&AIRFLOW_WEBSERVER_PROXY_FIX_X_FOR, "1"); if !trusted_proxies.iter().any(TrustedProxy::is_wildcard) { let product_version = &cluster.image.product_version; @@ -594,27 +385,14 @@ fn add_version_specific_env_vars( // In airflow 2.x the dag-processor can optionally be started as a // standalone process (rather then as a scheduler subprocess), // accompanied by this env-var being set to True. - env.insert( - "AIRFLOW__SCHEDULER__STANDALONE_DAG_PROCESSOR".into(), - EnvVar { - name: "AIRFLOW__SCHEDULER__STANDALONE_DAG_PROCESSOR".into(), - value: Some("True".into()), - ..Default::default() - }, - ); + env_vars = env_vars.with_value(&AIRFLOW_SCHEDULER_STANDALONE_DAG_PROCESSOR, "True"); } } -} -// Internally the environment variable collection uses a map so that overrides can actually -// override existing keys. The returned collection will be a vector. -fn transform_map_to_vec(env_map: BTreeMap) -> Vec { - env_map.into_values().collect::>() + env_vars } -fn authentication_env_vars( - auth_config: &AirflowClientAuthenticationDetailsResolved, -) -> Vec { +fn authentication_env_vars(auth_config: &AirflowClientAuthenticationDetailsResolved) -> EnvVarSet { // Different OIDC authentication entries can reference the same // client secret. It must be ensured that the env variables are only // added once in such a case. @@ -635,7 +413,11 @@ fn authentication_env_vars( .iter() .cloned() .flat_map(oidc::v1alpha1::AuthenticationProvider::client_credentials_env_var_mounts) - .collect() + .fold(EnvVarSet::new(), + |env_vars, env_var| env_vars + .with_env_var(env_var) + .expect("env_var name is valid because it is either OIDC_<16-hex-characters>_CLIENT_ID or OIDC_<16-hex-characters>_CLIENT_SECRET") + ) } /// Constructs the needed authorization env vars for the specific Airflow version. @@ -647,43 +429,35 @@ fn authentication_env_vars( fn authorization_env_vars( authorization_config: &AirflowAuthorizationResolved, product_version: &str, -) -> Vec { +) -> EnvVarSet { let Some(opa) = &authorization_config.opa else { - return vec![]; + return EnvVarSet::new(); }; - let mut env = vec![EnvVar { - name: AIRFLOW_CORE_AUTH_MANAGER.into(), - value: Some("opa_auth_manager.opa_fab_auth_manager.OpaFabAuthManager".to_string()), - ..Default::default() - }]; + let mut env_vars = EnvVarSet::new().with_value( + &AIRFLOW_CORE_AUTH_MANAGER, + "opa_auth_manager.opa_fab_auth_manager.OpaFabAuthManager", + ); if product_version.starts_with("2.") { // OPA config needs to go into `webserver_config.py` } else { - env.extend([ - EnvVar { - name: AIRFLOW_CORE_AUTH_OPA_REQUEST_URL.into(), - value: Some(opa.connection_string.to_owned()), - ..Default::default() - }, - EnvVar { - name: AIRFLOW_CORE_AUTH_OPA_CACHE_TTL_IN_SEC.into(), - value: Some(opa.cache_entry_time_to_live.as_secs().to_string()), - ..Default::default() - }, - EnvVar { - name: AIRFLOW_CORE_AUTH_OPA_CACHE_MAXSIZE.into(), - value: Some(opa.cache_max_entries.to_string()), - ..Default::default() - }, - ]); + env_vars = env_vars + .with_value(&AIRFLOW_CORE_AUTH_OPA_REQUEST_URL, &opa.connection_string) + .with_value( + &AIRFLOW_CORE_AUTH_OPA_CACHE_TTL_IN_SEC, + opa.cache_entry_time_to_live.as_secs().to_string(), + ) + .with_value( + &AIRFLOW_CORE_AUTH_OPA_CACHE_MAXSIZE, + opa.cache_max_entries.to_string(), + ); } - env + env_vars } -fn execution_server_env_vars(cluster: &ValidatedCluster) -> BTreeMap { - let mut env: BTreeMap = BTreeMap::new(); +fn execution_server_env_vars(cluster: &ValidatedCluster) -> EnvVarSet { + let mut env_vars = EnvVarSet::new(); let name = cluster.name_any(); // The execution API server URL can be any webserver (if there @@ -693,25 +467,18 @@ fn execution_server_env_vars(cluster: &ValidatedCluster) -> BTreeMap = std::result::Result; pub fn build_executor_template_config_map( cluster: &ValidatedCluster, executor_config: &ValidatedAirflowConfig, - env_overrides: &HashMap, + env_overrides: &EnvVarSet, pod_overrides: &PodTemplateSpec, ) -> Result { let resolved_product_image = &cluster.image; @@ -88,13 +87,13 @@ pub fn build_executor_template_config_map( let git_sync_resources = &executor_config.git_sync_resources; let mut pb = PodBuilder::new(); - let pb_metadata = - ObjectMetaBuilder::new() - .with_labels(cluster.recommended_labels_for( - &executor_role_name(), - &executor_template_role_group_name(), - )) - .build(); + let pb_metadata = ObjectMetaBuilder::new() + .with_labels(recommended_labels_for_role_group_resources( + cluster, + &executor_role_name(), + &executor_template_role_group_name(), + )) + .build(); pb.metadata(pb_metadata) .image_pull_secrets_from_product_image(resolved_product_image) @@ -193,7 +192,8 @@ pub fn build_executor_template_config_map( object_meta( cluster, cluster.executor_template_configmap_name(), - cluster.recommended_labels_for( + recommended_labels_for_role_group_resources( + cluster, &executor_role_name(), &executor_template_role_group_name(), ), diff --git a/rust/operator-binary/src/controller/build/resource/listener.rs b/rust/operator-binary/src/controller/build/resource/listener.rs index e346eb25..d8abe9f4 100644 --- a/rust/operator-binary/src/controller/build/resource/listener.rs +++ b/rust/operator-binary/src/controller/build/resource/listener.rs @@ -1,22 +1,16 @@ -use std::str::FromStr; - use stackable_operator::{ crd::listener, - v2::types::{ - kubernetes::{ListenerClassName, ListenerName}, - operator::RoleGroupName, - }, + v2::types::kubernetes::{ListenerClassName, ListenerName}, }; use crate::{ - controller::{ValidatedCluster, build::object_meta}, + controller::{ + ValidatedCluster, + build::{object_meta, recommended_labels_for_role_resources}, + }, crd::{AirflowRole, HTTP_PORT, HTTP_PORT_NAME}, }; -// The group listener is a role-level object, so a constant `none` role-group is used as the -// role-group label value. -stackable_operator::constant!(NONE_ROLE_GROUP_NAME: RoleGroupName = "none"); - pub fn build_group_listener( cluster: &ValidatedCluster, role: &AirflowRole, @@ -27,8 +21,7 @@ pub fn build_group_listener( metadata: object_meta( cluster, listener_group_name, - cluster - .recommended_labels_for(&ValidatedCluster::role_name(role), &NONE_ROLE_GROUP_NAME), + recommended_labels_for_role_resources(cluster, role), ) .build(), spec: listener::v1alpha1::ListenerSpec { diff --git a/rust/operator-binary/src/controller/build/resource/pdb.rs b/rust/operator-binary/src/controller/build/resource/pdb.rs index 22d82556..f56a7802 100644 --- a/rust/operator-binary/src/controller/build/resource/pdb.rs +++ b/rust/operator-binary/src/controller/build/resource/pdb.rs @@ -4,7 +4,7 @@ use stackable_operator::{ }; use crate::{ - controller::{ValidatedCluster, controller_name, operator_name, product_name}, + controller::{CONTROLLER_NAME, OPERATOR_NAME, PRODUCT_NAME, ValidatedCluster}, crd::{AirflowExecutor, AirflowRole}, }; @@ -37,10 +37,10 @@ pub fn build_pdb( let pdb = pod_disruption_budget_builder_with_role( cluster, - &product_name(), - &ValidatedCluster::role_name(role), - &operator_name(), - &controller_name(), + &PRODUCT_NAME, + role, + &OPERATOR_NAME, + &CONTROLLER_NAME, ) .with_max_unavailable(max_unavailable) .build(); diff --git a/rust/operator-binary/src/controller/build/resource/rbac.rs b/rust/operator-binary/src/controller/build/resource/rbac.rs index 7bbb3aa7..8bf95cfc 100644 --- a/rust/operator-binary/src/controller/build/resource/rbac.rs +++ b/rust/operator-binary/src/controller/build/resource/rbac.rs @@ -1,20 +1,11 @@ //! Builds the RBAC resources (ServiceAccount + RoleBinding) shared by all role groups. -use std::str::FromStr; - use stackable_operator::{ k8s_openapi::api::{core::v1::ServiceAccount, rbac::v1::RoleBinding}, - kvp::Labels, - v2::{ - rbac, - types::operator::{RoleGroupName, RoleName}, - }, + v2::rbac, }; -use crate::controller::ValidatedCluster; - -stackable_operator::constant!(NONE_ROLE_NAME: RoleName = "none"); -stackable_operator::constant!(NONE_ROLE_GROUP_NAME: RoleGroupName = "none"); +use crate::controller::{ValidatedCluster, build::recommended_labels_for_cluster_resources}; /// Builds the [`ServiceAccount`] that the role-group Pods and the Kubernetes-executor Pods run /// under. @@ -22,7 +13,7 @@ pub fn build_service_account(cluster: &ValidatedCluster) -> ServiceAccount { rbac::build_service_account( cluster, &cluster.cluster_resource_names(), - rbac_labels(cluster), + recommended_labels_for_cluster_resources(cluster), ) } @@ -32,12 +23,6 @@ pub fn build_role_binding(cluster: &ValidatedCluster) -> RoleBinding { rbac::build_role_binding( cluster, &cluster.cluster_resource_names(), - rbac_labels(cluster), + recommended_labels_for_cluster_resources(cluster), ) } - -/// Both resources are shared by the whole cluster rather than tied to a role or role group, so -/// the recommended labels carry `none` for both values. -fn rbac_labels(cluster: &ValidatedCluster) -> Labels { - cluster.recommended_labels_for(&NONE_ROLE_NAME, &NONE_ROLE_GROUP_NAME) -} diff --git a/rust/operator-binary/src/controller/build/resource/service.rs b/rust/operator-binary/src/controller/build/resource/service.rs index 6bab8c0c..363ab9f4 100644 --- a/rust/operator-binary/src/controller/build/resource/service.rs +++ b/rust/operator-binary/src/controller/build/resource/service.rs @@ -7,7 +7,10 @@ use stackable_operator::{ }; use crate::{ - controller::{ValidatedCluster, build::object_meta}, + controller::{ + ValidatedCluster, + build::{object_meta, recommended_labels_for_role_group_resources, role_group_selector}, + }, crd::{AirflowRole, HTTP_PORT, HTTP_PORT_NAME, METRICS_PORT, METRICS_PORT_NAME}, }; @@ -22,10 +25,10 @@ pub fn build_rolegroup_headless_service( metadata: object_meta( cluster, cluster - .role_group_resource_names(&ValidatedCluster::role_name(role), role_group_name) + .role_group_resource_names(role, role_group_name) .headless_service_name() .to_string(), - cluster.recommended_labels(role, role_group_name), + recommended_labels_for_role_group_resources(cluster, role, role_group_name), ) .build(), spec: Some(ServiceSpec { @@ -33,7 +36,7 @@ pub fn build_rolegroup_headless_service( type_: Some("ClusterIP".to_string()), cluster_ip: Some("None".to_string()), ports: Some(headless_service_ports()), - selector: Some(cluster.role_group_selector(role, role_group_name).into()), + selector: Some(role_group_selector(cluster, role, role_group_name).into()), publish_not_ready_addresses: Some(true), ..ServiceSpec::default() }), @@ -51,10 +54,10 @@ pub fn build_rolegroup_metrics_service( metadata: object_meta( cluster, cluster - .role_group_resource_names(&ValidatedCluster::role_name(role), role_group_name) + .role_group_resource_names(role, role_group_name) .metrics_service_name() .to_string(), - cluster.recommended_labels(role, role_group_name), + recommended_labels_for_role_group_resources(cluster, role, role_group_name), ) .with_labels(prometheus_labels(&Scraping::Enabled)) .with_annotations(prometheus_annotations( @@ -69,7 +72,7 @@ pub fn build_rolegroup_metrics_service( type_: Some("ClusterIP".to_string()), cluster_ip: Some("None".to_string()), ports: Some(metrics_service_ports()), - selector: Some(cluster.role_group_selector(role, role_group_name).into()), + selector: Some(role_group_selector(cluster, role, role_group_name).into()), publish_not_ready_addresses: Some(true), ..ServiceSpec::default() }), @@ -85,7 +88,7 @@ pub fn stateful_set_service_name( ) -> Option { Some( cluster - .role_group_resource_names(&ValidatedCluster::role_name(role), role_group_name) + .role_group_resource_names(role, role_group_name) .headless_service_name() .to_string(), ) diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 48aaac50..bfec1448 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -35,6 +35,8 @@ use crate::{ graceful_shutdown::add_graceful_shutdown_config, object_meta, properties::env_vars, + recommended_labels_for_role_group_resources, + recommended_labels_for_unversioned_role_group_resources, resource::{ pod::{ GitSyncSidecarsAddition, add_authentication_volumes_and_volume_mounts, @@ -42,6 +44,7 @@ use crate::{ }, service::stateful_set_service_name, }, + role_group_selector, volumes::{self, CONFIG_VOLUME_NAME, LOG_CONFIG_VOLUME_NAME, LOG_VOLUME_NAME}, }, }, @@ -73,9 +76,6 @@ pub enum Error { source: stackable_operator::builder::pod::container::Error, }, - #[snafu(display("failed to build Statefulset environmental variables"))] - BuildStatefulsetEnvVars { source: env_vars::Error }, - #[snafu(display("failed to build shared pod resources"))] Pod { source: crate::controller::build::resource::pod::Error, @@ -94,7 +94,7 @@ fn build_rolegroup_metadata( object_meta( cluster, name, - cluster.recommended_labels(role, role_group_name), + recommended_labels_for_role_group_resources(cluster, role, role_group_name), ) .with_label(prometheus_label) .build() @@ -119,15 +119,20 @@ pub fn build_server_rolegroup_statefulset( let executor = &validated_cluster.cluster_config.executor; let mut pb = PodBuilder::new(); - let resource_names = validated_cluster - .role_group_resource_names(&ValidatedCluster::role_name(airflow_role), role_group_name); + let resource_names = validated_cluster.role_group_resource_names(airflow_role, role_group_name); - let recommended_object_labels = - validated_cluster.recommended_labels(airflow_role, role_group_name); + let recommended_object_labels = recommended_labels_for_role_group_resources( + validated_cluster, + airflow_role, + role_group_name, + ); // Used for PVC templates that cannot be modified once they are deployed (a constant "none" // version keeps the labels stable across version upgrades). - let unversioned_recommended_labels = - validated_cluster.unversioned_recommended_labels(airflow_role, role_group_name); + let unversioned_recommended_labels = recommended_labels_for_unversioned_role_group_resources( + validated_cluster, + airflow_role, + role_group_name, + ); let pb_metadata = ObjectMetaBuilder::new() .with_labels(recommended_object_labels) @@ -182,15 +187,12 @@ pub fn build_server_rolegroup_statefulset( ]) .args(vec![airflow_container_args.join("\n")]); - airflow_container.add_env_vars( - env_vars::build_airflow_statefulset_envs( - validated_cluster, - airflow_role, - env_overrides, - git_sync_resources, - ) - .context(BuildStatefulsetEnvVarsSnafu)?, - ); + airflow_container.add_env_vars(env_vars::build_airflow_statefulset_envs( + validated_cluster, + airflow_role, + env_overrides, + git_sync_resources, + )); let volume_mounts = validated_cluster.volume_mounts(); airflow_container @@ -343,7 +345,7 @@ pub fn build_server_rolegroup_statefulset( ); let statefulset_match_labels = - validated_cluster.role_group_selector(airflow_role, role_group_name); + role_group_selector(validated_cluster, airflow_role, role_group_name); let statefulset_spec = StatefulSetSpec { pod_management_policy: Some( diff --git a/rust/operator-binary/src/controller/mod.rs b/rust/operator-binary/src/controller/mod.rs index 93058b42..0c34e0f4 100644 --- a/rust/operator-binary/src/controller/mod.rs +++ b/rust/operator-binary/src/controller/mod.rs @@ -1,8 +1,4 @@ -use std::{ - collections::{BTreeMap, HashMap}, - marker::PhantomData, - str::FromStr, -}; +use std::{collections::BTreeMap, marker::PhantomData, str::FromStr}; use stackable_operator::{ commons::{ @@ -10,6 +6,7 @@ use stackable_operator::{ product_image_selection::ResolvedProductImage, resources::{NoRuntimeLimits, Resources}, }, + constant, crd::{git_sync, listener}, database_connections::{ TemplatingMechanism, @@ -25,12 +22,11 @@ use stackable_operator::{ rbac::v1::RoleBinding, }, kube::{Resource, ResourceExt, api::ObjectMeta}, - kvp::Labels, product_logging::spec::ContainerLogConfig, shared::time::Duration, v2::{ HasName, HasUid, NameIsValidLabelValue, - kvp::label::{recommended_labels, role_group_selector}, + builder::pod::container::EnvVarSet, product_logging::framework::{ValidatedContainerLogConfigChoice, VectorContainerLogConfig}, role_group_utils::ResourceNames, role_utils, @@ -49,8 +45,8 @@ use stackable_operator::{ use crate::{ airflow_controller::AIRFLOW_CONTROLLER_NAME, crd::{ - APP_NAME, AirflowConfig, AirflowConfigOverrides, AirflowExecutor, AirflowRole, - AirflowStorageConfig, ExecutorConfig, OPERATOR_NAME, + self, APP_NAME, AirflowConfig, AirflowConfigOverrides, AirflowExecutor, AirflowRole, + AirflowStorageConfig, ExecutorConfig, authentication::AirflowClientAuthenticationDetailsResolved, authorization::AirflowAuthorizationResolved, databases::{ @@ -67,8 +63,9 @@ pub mod dereference; pub mod update_status; pub mod validate; -// Placeholder version label value for resources whose labels must not change after deployment. -stackable_operator::constant!(UNVERSIONED_PRODUCT_VERSION: ProductVersion = "none"); +constant!(PRODUCT_NAME: ProductName = APP_NAME); +constant!(OPERATOR_NAME: OperatorName = crd::OPERATOR_NAME); +constant!(CONTROLLER_NAME: ControllerName = AIRFLOW_CONTROLLER_NAME); /// Marker for prepared Kubernetes resources which are not applied yet. pub struct Prepared; @@ -158,7 +155,7 @@ pub struct ValidatedExecutorTemplate { /// The merged + validated executor config (resources, affinity, logging, …). pub config: ValidatedAirflowConfig, /// Env-var overrides for the executor pod template (`spec.kubernetesExecutors.envOverrides`). - pub env_overrides: HashMap, + pub env_overrides: EnvVarSet, /// Pod overrides for the executor pod template (`spec.kubernetesExecutors.podOverrides`). pub pod_overrides: PodTemplateSpec, } @@ -344,7 +341,7 @@ impl ValidatedCluster { pub fn cluster_resource_names(&self) -> role_utils::ResourceNames { role_utils::ResourceNames { cluster_name: self.name.clone(), - product_name: product_name(), + product_name: PRODUCT_NAME.clone(), } } @@ -360,95 +357,6 @@ impl ValidatedCluster { role_group_name: role_group_name.clone(), } } - - /// The type-safe role name for an Airflow role. - /// - /// Infallible: every `AirflowRole` serialises to a short, valid role name. - pub fn role_name(role: &AirflowRole) -> RoleName { - role.to_string() - .parse() - .expect("an AirflowRole serialises to a valid RoleName") - } - - /// Recommended labels for a role-group resource. - pub fn recommended_labels( - &self, - role: &AirflowRole, - role_group_name: &RoleGroupName, - ) -> Labels { - self.recommended_labels_for(&Self::role_name(role), role_group_name) - } - - /// Recommended labels for a resource that is not tied to a concrete [`AirflowRole`] (e.g. the - /// Kubernetes executor pod template), using a free-form role/role-group label value. - pub fn recommended_labels_for( - &self, - role_name: &RoleName, - role_group_name: &RoleGroupName, - ) -> Labels { - self.recommended_labels_with(&self.product_version, role_name, role_group_name) - } - - /// Recommended labels with the constant [`UNVERSIONED_PRODUCT_VERSION`], for PVC templates - /// that cannot be modified after deployment (keeps the labels stable across version upgrades). - pub fn unversioned_recommended_labels( - &self, - role: &AirflowRole, - role_group_name: &RoleGroupName, - ) -> Labels { - self.recommended_labels_with( - &UNVERSIONED_PRODUCT_VERSION, - &Self::role_name(role), - role_group_name, - ) - } - - fn recommended_labels_with( - &self, - product_version: &ProductVersion, - role_name: &RoleName, - role_group_name: &RoleGroupName, - ) -> Labels { - recommended_labels( - self, - &product_name(), - product_version, - &operator_name(), - &controller_name(), - role_name, - role_group_name, - ) - } - - /// Selector labels matching the pods of a role group. - pub fn role_group_selector( - &self, - role: &AirflowRole, - role_group_name: &RoleGroupName, - ) -> Labels { - role_group_selector( - self, - &product_name(), - &Self::role_name(role), - role_group_name, - ) - } -} - -/// The product name (`airflow`) as a type-safe label value. -pub(crate) fn product_name() -> ProductName { - ProductName::from_str(APP_NAME).expect("'airflow' is a valid product name") -} - -/// The operator name as a type-safe label value. -pub(crate) fn operator_name() -> OperatorName { - OperatorName::from_str(OPERATOR_NAME).expect("the operator name is a valid label value") -} - -/// The controller name as a type-safe label value. -pub(crate) fn controller_name() -> ControllerName { - ControllerName::from_str(AIRFLOW_CONTROLLER_NAME) - .expect("the controller name is a valid label value") } /// Pseudo role/role-group names for the Kubernetes executor's resources (it is not a real @@ -530,17 +438,13 @@ impl HasUid for ValidatedCluster { #[cfg(test)] mod tests { - use strum::IntoEnumIterator; + use super::*; - use super::ValidatedCluster; - use crate::crd::AirflowRole; - - /// Locks the invariant behind the `expect` in [`ValidatedCluster::role_name`]: every - /// `AirflowRole` variant (present and future) must serialise to a valid `RoleName`. #[test] - fn every_airflow_role_serialises_to_a_valid_role_name() { - for role in AirflowRole::iter() { - ValidatedCluster::role_name(&role); - } + fn test_constants() { + // Test that dereferencing the constants does not panic. + let _ = *PRODUCT_NAME; + let _ = *OPERATOR_NAME; + let _ = *CONTROLLER_NAME; } } diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index ca5a0a4f..96d045b8 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -5,17 +5,17 @@ use stackable_operator::{ commons::product_image_selection, config::fragment, crd::git_sync, - k8s_openapi::api::core::v1::{EnvVar, VolumeMount}, + k8s_openapi::api::core::v1::VolumeMount, kube::ResourceExt, product_logging::spec::Logging, - role_utils::{GenericRoleConfig, RoleGroup}, + role_utils::GenericRoleConfig, v2::{ - builder::pod::container::{EnvVarName, EnvVarSet}, + builder::pod::container::EnvVarSet, controller_utils::{get_namespace, get_uid}, product_logging::framework::{ VectorContainerLogConfig, validate_logging_configuration_for_container, }, - role_utils::{GenericCommonConfig, RoleGroupConfig, with_validated_config}, + role_utils::{GenericCommonConfig, RoleGroup, RoleGroupConfig, with_validated_config}, types::{ kubernetes::ConfigMapName, operator::{ClusterName, RoleGroupName}, @@ -30,7 +30,7 @@ use super::{ build::volumes::LOG_VOLUME_NAME, dereference::DereferencedObjects, }; use crate::{ - airflow_controller::{CONTAINER_IMAGE_BASE_NAME, env_vars_from_overrides}, + airflow_controller::CONTAINER_IMAGE_BASE_NAME, crd::{ AirflowConfig, AirflowConfigFragment, AirflowConfigOverrides, AirflowExecutor, AirflowRole, AirflowRoleType, Container, v1alpha2, @@ -75,11 +75,6 @@ pub enum Error { role_group: RoleGroupName, }, - #[snafu(display("failed to parse an environment variable override name"))] - ParseEnvVarName { - source: stackable_operator::v2::macros::attributed_string_type::Error, - }, - #[snafu(display("failed to validate the logging configuration"))] ValidateLoggingConfig { source: stackable_operator::v2::product_logging::framework::Error, @@ -215,11 +210,14 @@ pub fn validate_cluster( &Container::Base, &vector_aggregator_config_map_name, )?; + + let env_overrides: EnvVarSet = common_configuration.env_overrides.clone().into(); + // Resolve the executor's git-sync resources up-front too, mirroring the role groups. let git_sync_resources = git_sync::v1alpha2::GitSyncResources::new( &airflow.spec.cluster_config.dags_git_sync, &resolved_product_image, - &env_vars_from_overrides(&common_configuration.env_overrides), + &env_overrides.clone().into_iter().collect::>(), &airflow.spec.cluster_config.volume_mounts, LOG_VOLUME_NAME.as_ref(), &logging.git_sync_container, @@ -231,7 +229,7 @@ pub fn validate_cluster( logging, git_sync_resources, ), - env_overrides: common_configuration.env_overrides.clone(), + env_overrides, pod_overrides: common_configuration.pod_overrides.clone(), }) } @@ -290,14 +288,6 @@ fn validate_role_group( role_group: role_group_name.clone(), })?; - let mut env_overrides = EnvVarSet::new(); - for (env_var_name, env_var_value) in validated.config.env_overrides { - env_overrides = env_overrides.with_value( - &EnvVarName::from_str(&env_var_name).context(ParseEnvVarNameSnafu)?, - env_var_value, - ); - } - let merged_config = validated.config.config; let logging = validate_logging( &merged_config.logging, @@ -305,12 +295,14 @@ fn validate_role_group( vector_aggregator_config_map_name, )?; + let env_overrides: EnvVarSet = validated.config.env_overrides.clone().into(); + // The git-sync resources depend on this role group's env-var overrides and (git-sync) logging // config, so they are resolved (and validated) here, up-front, rather than at build time. let git_sync_resources = git_sync::v1alpha2::GitSyncResources::new( dags_git_sync, image, - &Vec::::from(env_overrides.clone()), + &env_overrides.clone().into_iter().collect::>(), volume_mounts, LOG_VOLUME_NAME.as_ref(), &logging.git_sync_container, @@ -367,7 +359,7 @@ pub(crate) fn validate_logging( mod tests { use std::collections::BTreeMap; - use stackable_operator::k8s_openapi::api::core::v1::EnvVar; + use stackable_operator::v2::builder::pod::container::{EnvVarName, EnvVarSet}; use super::validate_role_group; use crate::crd::{AirflowConfig, AirflowRole, v1alpha2}; @@ -467,19 +459,17 @@ mod tests { ); // env overrides layer role-group on top of role. - let env_overrides: BTreeMap> = - Vec::::from(validated.env_overrides) - .into_iter() - .map(|env_var| (env_var.name, env_var.value)) - .collect(); - assert_eq!(env_overrides.len(), 2); - assert_eq!( - env_overrides.get("ROLE_ENV_VAR").unwrap().as_deref(), - Some("role-env-value") - ); assert_eq!( - env_overrides.get("GROUP_ENV_VAR").unwrap().as_deref(), - Some("group-env-value") + validated.env_overrides, + EnvVarSet::new() + .with_value( + &EnvVarName::from_str_unsafe("ROLE_ENV_VAR"), + "role-env-value" + ) + .with_value( + &EnvVarName::from_str_unsafe("GROUP_ENV_VAR"), + "group-env-value" + ) ); } @@ -562,7 +552,7 @@ mod tests { .overrides .is_empty() ); - assert!(Vec::::from(validated.env_overrides).is_empty()); + assert_eq!(validated.env_overrides, EnvVarSet::new()); } /// `replicas` and the role←role-group merged `pod_overrides` are produced by diff --git a/rust/operator-binary/src/crd/internal_secret.rs b/rust/operator-binary/src/crd/internal_secret.rs index c5755fd2..0730700a 100644 --- a/rust/operator-binary/src/crd/internal_secret.rs +++ b/rust/operator-binary/src/crd/internal_secret.rs @@ -1,12 +1,29 @@ +use std::str::FromStr; + +use stackable_operator::{constant, v2::types::kubernetes::SecretKey}; + // Secret key used to run the api server. It should be as random as possible. // It should be consistent across instances of the webserver. The webserver key // is also used to authorize requests to Celery workers when logs are retrieved. -pub const INTERNAL_SECRET_SECRET_KEY: &str = "INTERNAL_SECRET"; +constant!(pub INTERNAL_SECRET_SECRET_KEY: SecretKey = "INTERNAL_SECRET"); // Used for env-var: AIRFLOW__API_AUTH__JWT_SECRET // Secret key used to encode and decode JWTs to authenticate to public and // private APIs. It should be as random as possible, but consistent across // instances of API services. -pub const JWT_SECRET_SECRET_KEY: &str = "JWT_SECRET"; +constant!(pub JWT_SECRET_SECRET_KEY: SecretKey = "JWT_SECRET"); // Used for env-var: AIRFLOW__CORE__FERNET_KEY // See https://airflow.apache.org/docs/apache-airflow/stable/security/secrets/fernet.html#security-fernet -pub const FERNET_KEY_SECRET_KEY: &str = "FERNET_KEY"; +constant!(pub FERNET_KEY_SECRET_KEY: SecretKey = "FERNET_KEY"); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_constants() { + // Test that dereferencing the constants does not panic. + let _ = *FERNET_KEY_SECRET_KEY; + let _ = *INTERNAL_SECRET_SECRET_KEY; + let _ = *JWT_SECRET_SECRET_KEY; + } +} diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 6ce8ca0c..15ba05e9 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeSet, str::FromStr}; +use std::{collections::BTreeSet, ops::Deref, str::FromStr}; use serde::{Deserialize, Serialize}; use snafu::{ResultExt, Snafu}; @@ -18,6 +18,7 @@ use stackable_operator::{ fragment::{self, Fragment, ValidationError}, merge::Merge, }, + constant, crd::git_sync, deep_merger::ObjectOverrides, k8s_openapi::{ @@ -31,7 +32,7 @@ use stackable_operator::{ framework::{create_vector_shutdown_file_command, remove_vector_shutdown_file_command}, spec::Logging, }, - role_utils::{CommonConfiguration, GenericRoleConfig, Role, RoleGroup}, + role_utils::GenericRoleConfig, schemars::{self, JsonSchema}, shared::time::Duration, status::condition::{ClusterCondition, HasStatusCondition}, @@ -40,13 +41,14 @@ use stackable_operator::{ config_overrides::KeyValueConfigOverrides, flask_config_writer::{FlaskAppConfigOptions, PythonType}, product_logging::framework::STACKABLE_LOG_DIR, - role_utils::GenericCommonConfig, + role_utils::{CommonConfiguration, GenericCommonConfig, Role, RoleGroup}, types::{ common::Port, kubernetes::{ ConfigMapName, ContainerName, ListenerClassName, ListenerName, PersistentVolumeClaimName, SecretName, VolumeName, }, + operator::RoleName, }, }, versioned::versioned, @@ -104,6 +106,12 @@ pub const MAX_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { unit: BinaryMultiple::Mebi, }; +constant!(WEBSERVER_ROLE_NAME: RoleName = "webserver"); +constant!(SCHEDULER_ROLE_NAME: RoleName = "scheduler"); +constant!(WORKER_ROLE_NAME: RoleName = "worker"); +constant!(DAG_PROCESSOR_ROLE_NAME: RoleName = "dagprocessor"); +constant!(TRIGGERER_ROLE_NAME: RoleName = "triggerer"); + pub type AirflowRoleType = Role; @@ -524,35 +532,12 @@ pub struct AirflowOpaConfig { pub cache: UserInformationCache, } -#[derive( - Clone, - Debug, - Deserialize, - Display, - EnumIter, - Eq, - Hash, - JsonSchema, - Ord, - PartialEq, - PartialOrd, - Serialize, - EnumString, -)] +#[derive(Clone, Debug, EnumIter, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum AirflowRole { - #[strum(serialize = "webserver")] Webserver, - - #[strum(serialize = "scheduler")] Scheduler, - - #[strum(serialize = "worker")] Worker, - - #[strum(serialize = "dagprocessor")] DagProcessor, - - #[strum(serialize = "triggerer")] Triggerer, } @@ -812,6 +797,20 @@ impl AirflowRole { } } +impl Deref for AirflowRole { + type Target = RoleName; + + fn deref(&self) -> &Self::Target { + match self { + AirflowRole::Webserver => &WEBSERVER_ROLE_NAME, + AirflowRole::Scheduler => &SCHEDULER_ROLE_NAME, + AirflowRole::Worker => &WORKER_ROLE_NAME, + AirflowRole::DagProcessor => &DAG_PROCESSOR_ROLE_NAME, + AirflowRole::Triggerer => &TRIGGERER_ROLE_NAME, + } + } +} + fn container_debug_command() -> String { format!("containerdebug --output={STACKABLE_LOG_DIR}/containerdebug-state.json --loop &") } @@ -1036,10 +1035,18 @@ mod tests { versioned::test_utils::RoundtripTestData, }; - use crate::{ - crd::{AirflowRole, trusted_proxies::TrustedProxy}, - v1alpha1, v1alpha2, - }; + use super::*; + use crate::{v1alpha1, v1alpha2}; + + #[test] + fn test_constants() { + // Test that dereferencing the constants does not panic. + let _ = *WEBSERVER_ROLE_NAME; + let _ = *SCHEDULER_ROLE_NAME; + let _ = *WORKER_ROLE_NAME; + let _ = *DAG_PROCESSOR_ROLE_NAME; + let _ = *TRIGGERER_ROLE_NAME; + } #[test] fn test_cluster_config() { @@ -1216,7 +1223,7 @@ mod tests { role.trusted_proxies(&cluster) .expect("no proxies to parse") .is_empty(), - "role {role} must not have trusted proxies" + "role {role:?} must not have trusted proxies" ); } } diff --git a/rust/operator-binary/src/util.rs b/rust/operator-binary/src/util.rs index 428e67b2..2f4b1fbb 100644 --- a/rust/operator-binary/src/util.rs +++ b/rust/operator-binary/src/util.rs @@ -1,20 +1,3 @@ -use stackable_operator::k8s_openapi::api::core::v1::{EnvVar, EnvVarSource, SecretKeySelector}; - -pub fn env_var_from_secret(var_name: &str, secret: impl AsRef, secret_key: &str) -> EnvVar { - EnvVar { - name: String::from(var_name), - value_from: Some(EnvVarSource { - secret_key_ref: Some(SecretKeySelector { - name: String::from(secret.as_ref()), - key: String::from(secret_key), - ..Default::default() - }), - ..Default::default() - }), - ..Default::default() - } -} - pub fn role_service_name(name: &str, role: &str) -> String { format!("{name}-{role}") }