diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eeb1fd6..e690f902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## [Unreleased] +### Added + +- Configuration option `rowLimit` added ([#173]). +- Configuration and environment overrides enabled ([#173]). + +[#173]: https://github.com/stackabletech/superset-operator/pull/173 + ## [0.4.0] - 2022-04-05 ### Added diff --git a/Cargo.lock b/Cargo.lock index 6b4601ef..3ea08485 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,17 @@ version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +[[package]] +name = "async-trait" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atty" version = "0.2.14" @@ -103,6 +114,18 @@ dependencies = [ "git2", ] +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "1.1.0" @@ -216,6 +239,26 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "crossbeam-channel" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +dependencies = [ + "cfg-if", + "lazy_static", +] + [[package]] name = "darling" version = "0.13.4" @@ -628,6 +671,24 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-openssl" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ee5d7a8f718585d1c3c61dfde28ef5b0bb14734b4db13f5ada856cdc6c612b" +dependencies = [ + "http", + "hyper", + "linked_hash_set", + "once_cell", + "openssl", + "openssl-sys", + "parking_lot", + "tokio", + "tokio-openssl", + "tower-layer", +] + [[package]] name = "hyper-timeout" version = "0.4.1" @@ -689,6 +750,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "integer-encoding" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e85a1509a128c855368e135cffcde7eac17d8e1083f41e2b98c58bc1a5074be" + [[package]] name = "itoa" version = "1.0.1" @@ -715,6 +782,15 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "json-patch" version = "0.2.6" @@ -754,9 +830,9 @@ dependencies = [ [[package]] name = "kube" -version = "0.70.0" +version = "0.71.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcc72fdf0c491160a34d4a1bfb03f96da8a5054288d61c816d514b5c2fa49ea" +checksum = "342744dfeb81fe186b84f485b33f12c6a15d3396987d933b06a566a3db52ca38" dependencies = [ "k8s-openapi", "kube-client", @@ -767,9 +843,9 @@ dependencies = [ [[package]] name = "kube-client" -version = "0.70.0" +version = "0.71.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b01c722d55ffedec74cbc259b4508d8a59bf19540006ec87618f76ab156579" +checksum = "3f69a504997799340408635d6e351afb8aab2c34ca3165e162f41b3b34a69a79" dependencies = [ "base64", "bytes", @@ -780,6 +856,7 @@ dependencies = [ "http", "http-body", "hyper", + "hyper-openssl", "hyper-timeout", "hyper-tls", "jsonpath_lib", @@ -803,9 +880,9 @@ dependencies = [ [[package]] name = "kube-core" -version = "0.70.0" +version = "0.71.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd9e3535777edd122cc26fe3fe6357066b33eff63d8b919862edbe7a956a679" +checksum = "a4a247487699941baaf93438d65b12d4e32450bea849d619d19ed394e8a4a645" dependencies = [ "chrono", "form_urlencoded", @@ -821,9 +898,9 @@ dependencies = [ [[package]] name = "kube-derive" -version = "0.70.0" +version = "0.71.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1322e25c20dd6f18ca6baecc88bb130331e99d988df9d7a9a207f15819e05bff" +checksum = "203f7c5acf9d0dfb0b08d44ec1d66ace3d1dfe0cdd82e65e274f3f96615d666c" dependencies = [ "darling", "proc-macro2", @@ -834,9 +911,9 @@ dependencies = [ [[package]] name = "kube-runtime" -version = "0.70.0" +version = "0.71.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816c8c086f8bbcf9a4db0b7a68db90b784ef6292a57de35c64cccb90d5edfbe5" +checksum = "02ea50e6ed56578e1d1d02548901b12fe6d3edbf110269a396955e285d487973" dependencies = [ "ahash", "backoff", @@ -898,6 +975,15 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +[[package]] +name = "linked_hash_set" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47186c6da4d81ca383c7c47c1bfc80f4b95f4720514d860a5407aaf4233f9588" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "lock_api" version = "0.4.7" @@ -1056,6 +1142,60 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6105e89802af13fdf48c49d7646d3b533a70e536d818aae7e78ba0433d01acb8" +dependencies = [ + "async-trait", + "crossbeam-channel", + "futures-channel", + "futures-executor", + "futures-util", + "js-sys", + "lazy_static", + "percent-encoding", + "pin-project", + "rand", + "thiserror", + "tokio", + "tokio-stream", +] + +[[package]] +name = "opentelemetry-jaeger" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c0b12cd9e3f9b35b52f6e0dac66866c519b26f424f4bbf96e3fe8bfbdc5229" +dependencies = [ + "async-trait", + "lazy_static", + "opentelemetry", + "opentelemetry-semantic-conventions", + "thiserror", + "thrift", + "tokio", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985cc35d832d412224b2cffe2f9194b1b89b6aa5d0bef76d080dce09d90e62bd" +dependencies = [ + "opentelemetry", +] + +[[package]] +name = "ordered-float" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3305af35278dd29f46fcdd139e0b1fbfae2153f0e5928b39b035542dd31e37b7" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-float" version = "2.10.0" @@ -1191,8 +1331,8 @@ dependencies = [ [[package]] name = "product-config" -version = "0.3.1" -source = "git+https://github.com/stackabletech/product-config.git?tag=0.3.1#40c93e5283beef100c9fecdb6368f1e1480db3e8" +version = "0.4.0" +source = "git+https://github.com/stackabletech/product-config.git?tag=0.4.0#e1e5938b4f6120f85a088194e86d22433fdba731" dependencies = [ "fancy-regex", "java-properties", @@ -1408,7 +1548,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" dependencies = [ - "ordered-float", + "ordered-float 2.10.0", "serde", ] @@ -1522,8 +1662,8 @@ dependencies = [ [[package]] name = "stackable-operator" -version = "0.15.0" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=0.15.0#c7c408d476c0b7ba06833c19e5c9d2aefa5875ae" +version = "0.17.0" +source = "git+https://github.com/stackabletech/operator-rs.git?tag=0.17.0#ea47f679e2fe3f198691de7c1b742ac8ed8462c6" dependencies = [ "backoff", "chrono", @@ -1536,6 +1676,8 @@ dependencies = [ "k8s-openapi", "kube", "lazy_static", + "opentelemetry", + "opentelemetry-jaeger", "product-config", "rand", "regex", @@ -1547,6 +1689,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "tracing-opentelemetry", "tracing-subscriber", ] @@ -1677,6 +1820,28 @@ dependencies = [ "once_cell", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "thrift" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b82ca8f46f95b3ce96081fe3dd89160fdea970c254bb72925255d1b62aae692e" +dependencies = [ + "byteorder", + "integer-encoding", + "log", + "ordered-float 1.1.1", + "threadpool", +] + [[package]] name = "time" version = "0.1.43" @@ -1750,6 +1915,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-openssl" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08f9ffb7809f1b20c1b398d92acf4cc719874b3b2b2d9ea2f09b4a80350878a" +dependencies = [ + "futures-util", + "openssl", + "openssl-sys", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.1" @@ -1868,6 +2056,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f9378e96a9361190ae297e7f3a8ff644aacd2897f244b1ff81f381669196fa6" +dependencies = [ + "opentelemetry", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", +] + [[package]] name = "tracing-subscriber" version = "0.3.11" @@ -1980,6 +2181,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index b02b6ee4..3d9de0bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,2 @@ [workspace] members = ["rust/crd", "rust/operator-binary"] - diff --git a/deploy/config-spec/properties.yaml b/deploy/config-spec/properties.yaml index b7e9866e..fd84d3da 100644 --- a/deploy/config-spec/properties.yaml +++ b/deploy/config-spec/properties.yaml @@ -14,3 +14,22 @@ properties: required: true asOfVersion: "0.0.0" description: "The secret where the Superset credentials are stored." + - property: &rowLimit + propertyNames: + - name: "ROW_LIMIT" + kind: + type: "file" + file: "superset_config.py" + datatype: + type: "integer" + defaultValues: + - fromVersion: "0.0.0" + value: "50000" + recommendedValues: + - fromVersion: "0.0.0" + value: "50000" + roles: + - name: "node" + required: false + asOfVersion: "0.0.0" + description: "row limit when requesting chart data" diff --git a/deploy/crd/supersetcluster.crd.yaml b/deploy/crd/supersetcluster.crd.yaml index 729d6851..0902a429 100644 --- a/deploy/crd/supersetcluster.crd.yaml +++ b/deploy/crd/supersetcluster.crd.yaml @@ -37,6 +37,11 @@ spec: type: object config: default: {} + properties: + rowLimit: + format: int32 + nullable: true + type: integer type: object configOverrides: additionalProperties: @@ -60,6 +65,11 @@ spec: type: object config: default: {} + properties: + rowLimit: + format: int32 + nullable: true + type: integer type: object configOverrides: additionalProperties: diff --git a/deploy/helm/superset-operator/configs/properties.yaml b/deploy/helm/superset-operator/configs/properties.yaml index b7e9866e..fd84d3da 100644 --- a/deploy/helm/superset-operator/configs/properties.yaml +++ b/deploy/helm/superset-operator/configs/properties.yaml @@ -14,3 +14,22 @@ properties: required: true asOfVersion: "0.0.0" description: "The secret where the Superset credentials are stored." + - property: &rowLimit + propertyNames: + - name: "ROW_LIMIT" + kind: + type: "file" + file: "superset_config.py" + datatype: + type: "integer" + defaultValues: + - fromVersion: "0.0.0" + value: "50000" + recommendedValues: + - fromVersion: "0.0.0" + value: "50000" + roles: + - name: "node" + required: false + asOfVersion: "0.0.0" + description: "row limit when requesting chart data" diff --git a/deploy/helm/superset-operator/crds/crds.yaml b/deploy/helm/superset-operator/crds/crds.yaml index 82f4cf2e..5fe0e6ce 100644 --- a/deploy/helm/superset-operator/crds/crds.yaml +++ b/deploy/helm/superset-operator/crds/crds.yaml @@ -39,6 +39,11 @@ spec: type: object config: default: {} + properties: + rowLimit: + format: int32 + nullable: true + type: integer type: object configOverrides: additionalProperties: @@ -62,6 +67,11 @@ spec: type: object config: default: {} + properties: + rowLimit: + format: int32 + nullable: true + type: integer type: object configOverrides: additionalProperties: diff --git a/deploy/manifests/configmap.yaml b/deploy/manifests/configmap.yaml index f8ea5936..09a5129a 100644 --- a/deploy/manifests/configmap.yaml +++ b/deploy/manifests/configmap.yaml @@ -18,6 +18,25 @@ data: required: true asOfVersion: "0.0.0" description: "The secret where the Superset credentials are stored." + - property: &rowLimit + propertyNames: + - name: "ROW_LIMIT" + kind: + type: "file" + file: "superset_config.py" + datatype: + type: "integer" + defaultValues: + - fromVersion: "0.0.0" + value: "50000" + recommendedValues: + - fromVersion: "0.0.0" + value: "50000" + roles: + - name: "node" + required: false + asOfVersion: "0.0.0" + description: "row limit when requesting chart data" kind: ConfigMap metadata: name: superset-operator-configmap diff --git a/deploy/manifests/crds.yaml b/deploy/manifests/crds.yaml index e1044b39..15a37301 100644 --- a/deploy/manifests/crds.yaml +++ b/deploy/manifests/crds.yaml @@ -40,6 +40,11 @@ spec: type: object config: default: {} + properties: + rowLimit: + format: int32 + nullable: true + type: integer type: object configOverrides: additionalProperties: @@ -63,6 +68,11 @@ spec: type: object config: default: {} + properties: + rowLimit: + format: int32 + nullable: true + type: integer type: object configOverrides: additionalProperties: diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index 6f61e759..6c33eecb 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -65,7 +65,7 @@ spec: roleGroups: default: config: - + rowLimit: 10000 ---- `metadata.name` contains the name of the Superset cluster. @@ -78,6 +78,8 @@ The previously created secret must be referenced in `spec.credentialsSecret`. The `spec.loadExamplesOnInit` key is optional and defaults to `false`, it can be set to `true` to load example data into superset when the database is initialized. +The `rowLimit` configuration option defines the row limit when requesting chart data. + === Initialization of the Superset database The first time the cluster is created, the operator creates a `SupersetDB` resource with the same name as the cluster. It ensures that the database is initialized (schema created, admin user created). @@ -140,3 +142,92 @@ image::superset-databases.png[Superset databases showing the connected Druid clu The managed Superset instances are automatically configured to export Prometheus metrics. See xref:home::monitoring.adoc[] for more details. + +== Configuration & Environment Overrides + +The cluster definition also supports overriding configuration properties and environment variables, +either per role or per role group, where the more specific override (role group) has precedence over +the less specific one (role). + +IMPORTANT: Overriding certain properties which are set by the operator (such as the `STATS_LOGGER`) +can interfere with the operator and can lead to problems. + +=== Configuration Properties + +For a role or role group, at the same level of `config`, you can specify `configOverrides` for the +`superset_config.py`. For example, if you want to set the CSV export encoding and the preferred +databases adapt the `nodes` section of the cluster resource like so: + +[source,yaml] +---- +nodes: + roleGroups: + default: + config: {} + configOverrides: + superset_config.py: + CSV_EXPORT: "{'encoding': 'utf-8'}" + PREFERRED_DATABASES: |- + [ + 'PostgreSQL', + 'Presto', + 'MySQL', + 'SQLite', + # etc. + ] +---- + +Just as for the `config`, it is possible to specify this at the role level as well: + +[source,yaml] +---- +nodes: + configOverrides: + superset_config.py: + CSV_EXPORT: "{'encoding': 'utf-8'}" + PREFERRED_DATABASES: |- + [ + 'PostgreSQL', + 'Presto', + 'MySQL', + 'SQLite', + # etc. + ] + roleGroups: + default: + config: {} +---- + +All override property values must be strings. They are treated as Python expressions. So care must +be taken to not produce an invalid configuration. + +For a full list of configuration options we refer to the +https://github.com/apache/superset/blob/master/superset/config.py[main config file for Superset]. + +=== Environment Variables + +In a similar fashion, environment variables can be (over)written. For example per role group: + +[source,yaml] +---- +nodes: + roleGroups: + default: + config: {} + envOverrides: + FLASK_ENV: development +---- + +or per role: + +[source,yaml] +---- +nodes: + envOverrides: + FLASK_ENV: development + roleGroups: + default: + config: {} +---- + +// cliOverrides don't make sense for this operator, so the feature is omitted for now diff --git a/examples/simple-superset-cluster.yaml b/examples/simple-superset-cluster.yaml index d9953522..797a5b97 100644 --- a/examples/simple-superset-cluster.yaml +++ b/examples/simple-superset-cluster.yaml @@ -26,3 +26,4 @@ spec: roleGroups: default: config: + rowLimit: 10000 diff --git a/rust/crd/Cargo.toml b/rust/crd/Cargo.toml index b54b1da3..041021a9 100644 --- a/rust/crd/Cargo.toml +++ b/rust/crd/Cargo.toml @@ -10,6 +10,6 @@ version = "0.4.0" [dependencies] serde = "1.0" serde_json = "1.0" -stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", tag = "0.15.0" } +stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", tag = "0.17.0" } strum = { version = "0.24", features = ["derive"] } snafu = "0.7" diff --git a/rust/crd/src/lib.rs b/rust/crd/src/lib.rs index f5f72226..06903b8e 100644 --- a/rust/crd/src/lib.rs +++ b/rust/crd/src/lib.rs @@ -2,18 +2,64 @@ pub mod druidconnection; pub mod supersetdb; use std::collections::BTreeMap; +use std::num::ParseIntError; use serde::{Deserialize, Serialize}; +use snafu::Snafu; use stackable_operator::kube::runtime::reflector::ObjectRef; use stackable_operator::kube::CustomResource; +use stackable_operator::product_config::flask_app_config_writer::{ + FlaskAppConfigOptions, PythonType, +}; use stackable_operator::product_config_utils::{ConfigError, Configuration}; use stackable_operator::role_utils::{Role, RoleGroupRef}; use stackable_operator::schemars::{self, JsonSchema}; -use strum::{Display, EnumIter}; +use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; pub const APP_NAME: &str = "superset"; pub const MANAGED_BY: &str = "superset-operator"; +pub const PYTHONPATH: &str = "/app/pythonpath"; +pub const SUPERSET_CONFIG_FILENAME: &str = "superset_config.py"; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("invalid int config value"))] + InvalidIntConfigValue { source: ParseIntError }, +} + +#[derive(Display, EnumIter, EnumString)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +pub enum SupersetConfigOptions { + SecretKey, + SqlalchemyDatabaseUri, + StatsLogger, + RowLimit, +} + +impl SupersetConfigOptions { + /// Mapping from `SupersetConfigOptions` to the values set in `SupersetConfig`. + /// `None` is returned if either the according option is not set or is not exposed in the + /// `SupersetConfig`. + fn config_type_to_string(&self, superset_config: &SupersetConfig) -> Option { + match self { + SupersetConfigOptions::RowLimit => superset_config.row_limit.map(|v| v.to_string()), + _ => None, + } + } +} + +impl FlaskAppConfigOptions for SupersetConfigOptions { + fn python_type(&self) -> PythonType { + match self { + SupersetConfigOptions::RowLimit => PythonType::IntLiteral, + SupersetConfigOptions::SecretKey => PythonType::Expression, + SupersetConfigOptions::SqlalchemyDatabaseUri => PythonType::Expression, + SupersetConfigOptions::StatsLogger => PythonType::Expression, + } + } +} + pub const HTTP_PORT: &str = "http"; #[derive(Clone, CustomResource, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] @@ -82,7 +128,9 @@ pub enum SupersetRole { #[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] -pub struct SupersetConfig {} +pub struct SupersetConfig { + pub row_limit: Option, +} impl SupersetConfig { pub const CREDENTIALS_SECRET_PROPERTY: &'static str = "credentialsSecret"; @@ -115,9 +163,19 @@ impl Configuration for SupersetConfig { &self, _resource: &Self::Configurable, _role_name: &str, - _file: &str, + file: &str, ) -> Result>, ConfigError> { - Ok(BTreeMap::new()) + let mut result = BTreeMap::new(); + + if file == SUPERSET_CONFIG_FILENAME { + for option in SupersetConfigOptions::iter() { + if let Some(value) = option.config_type_to_string(self) { + result.insert(option.to_string(), Some(value)); + } + } + } + + Ok(result) } } diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index a18d4fea..cd626de8 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -15,7 +15,7 @@ futures = { version = "0.3", features = ["compat"] } serde = "1.0" serde_yaml = "0.8" snafu = "0.7" -stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", tag = "0.15.0" } +stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", tag = "0.17.0" } stackable-superset-crd = { path = "../crd" } strum = { version = "0.24", features = ["derive"] } tokio = { version = "1.17", features = ["macros", "rt-multi-thread"] } @@ -23,5 +23,5 @@ tracing = "0.1" [build-dependencies] built = { version = "0.5", features = ["chrono", "git2"] } -stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", tag = "0.15.0" } +stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", tag = "0.17.0" } stackable-superset-crd = { path = "../crd" } diff --git a/rust/operator-binary/src/druid_connection_controller.rs b/rust/operator-binary/src/druid_connection_controller.rs index 48ced88d..a38e8d34 100644 --- a/rust/operator-binary/src/druid_connection_controller.rs +++ b/rust/operator-binary/src/druid_connection_controller.rs @@ -1,4 +1,4 @@ -use crate::util::{env_var_from_secret, get_job_state, JobState}; +use crate::util::{get_job_state, JobState}; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ @@ -18,10 +18,11 @@ use stackable_operator::{ }, logging::controller::ReconcilerError, }; -use stackable_superset_crd::druidconnection::{ - DruidConnection, DruidConnectionStatus, DruidConnectionStatusCondition, +use stackable_superset_crd::{ + druidconnection::{DruidConnection, DruidConnectionStatus, DruidConnectionStatusCondition}, + supersetdb::{SupersetDB, SupersetDBStatusCondition}, + PYTHONPATH, SUPERSET_CONFIG_FILENAME, }; -use stackable_superset_crd::supersetdb::{SupersetDB, SupersetDBStatusCondition}; use std::{sync::Arc, time::Duration}; use strum::{EnumDiscriminants, IntoStaticStr}; @@ -229,6 +230,13 @@ async fn build_import_job( sqlalchemy_str: &str, ) -> Result { let mut commands = vec![]; + + let config = "import os; SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI')"; + commands.push(format!("mkdir -p {PYTHONPATH}")); + commands.push(format!( + "echo \"{config}\" > {PYTHONPATH}/{SUPERSET_CONFIG_FILENAME}" + )); + let druid_info = build_druid_db_yaml(&druid_connection.spec.druid.name, sqlalchemy_str)?; commands.push(format!("echo \"{}\" > /tmp/druids.yaml", druid_info)); commands.push(String::from( @@ -239,19 +247,12 @@ async fn build_import_job( let container = ContainerBuilder::new("superset-import-druid-connection") .image(format!( - "docker.stackable.tech/stackable/superset:{}-stackable0", + "docker.stackable.tech/stackable/superset:{}-stackable1", superset_db.spec.superset_version )) .command(vec!["/bin/sh".to_string()]) .args(vec![String::from("-c"), commands.join("; ")]) - .add_env_vars(vec![ - env_var_from_secret("SECRET_KEY", secret, "connections.secretKey"), - env_var_from_secret( - "SQLALCHEMY_DATABASE_URI", - secret, - "connections.sqlalchemyDatabaseUri", - ), - ]) + .add_env_var_from_secret("DATABASE_URI", secret, "connections.sqlalchemyDatabaseUri") .build(); let pod = PodTemplateSpec { diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index a8f69346..cb3122a7 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -39,8 +39,6 @@ struct Opts { #[tokio::main] async fn main() -> anyhow::Result<()> { - stackable_operator::logging::initialize_logging("SUPERSET_OPERATOR_LOG"); - let opts = Opts::parse(); match opts.cmd { Command::Crd => println!( @@ -52,6 +50,7 @@ async fn main() -> anyhow::Result<()> { Command::Run(ProductOperatorRun { product_config, watch_namespace, + tracing_target, }) => { stackable_operator::utils::print_startup_string( built_info::PKG_DESCRIPTION, @@ -61,6 +60,12 @@ async fn main() -> anyhow::Result<()> { built_info::BUILT_TIME_UTC, built_info::RUSTC_VERSION, ); + stackable_operator::logging::initialize_logging( + "SUPERSET_OPERATOR_LOG", + APP_NAME, + tracing_target, + ); + let product_config = product_config.load(&[ "deploy/config-spec/properties.yaml", "/etc/stackable/superset-operator/config-spec/properties.yaml", diff --git a/rust/operator-binary/src/superset_controller.rs b/rust/operator-binary/src/superset_controller.rs index a8b7ec26..e60091cb 100644 --- a/rust/operator-binary/src/superset_controller.rs +++ b/rust/operator-binary/src/superset_controller.rs @@ -1,7 +1,7 @@ //! Ensures that `Pod`s are configured and running for each [`SupersetCluster`] use crate::{ - util::{env_var_from_secret, statsd_exporter_version, superset_version}, + util::{statsd_exporter_version, superset_version}, APP_NAME, APP_PORT, }; @@ -14,23 +14,30 @@ use std::{ use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ - builder::{ContainerBuilder, ObjectMetaBuilder, PodBuilder}, + builder::{ConfigMapBuilder, ContainerBuilder, ObjectMetaBuilder, PodBuilder}, k8s_openapi::{ api::{ apps::v1::{StatefulSet, StatefulSetSpec}, - core::v1::{Service, ServicePort, ServiceSpec}, + core::v1::{ + ConfigMap, ConfigMapVolumeSource, Service, ServicePort, ServiceSpec, Volume, + }, }, apimachinery::pkg::apis::meta::v1::LabelSelector, }, kube::runtime::controller::{Action, Context}, labels::{role_group_selector_labels, role_selector_labels}, logging::controller::ReconcilerError, - product_config::{types::PropertyNameKind, ProductConfigManager}, + product_config::{ + flask_app_config_writer::{self, FlaskAppConfigWriterError}, + types::PropertyNameKind, + ProductConfigManager, + }, product_config_utils::{transform_all_roles_to_config, validate_all_roles_and_groups_config}, role_utils::RoleGroupRef, }; use stackable_superset_crd::{ - supersetdb::SupersetDB, SupersetCluster, SupersetConfig, SupersetRole, + supersetdb::SupersetDB, SupersetCluster, SupersetConfig, SupersetConfigOptions, SupersetRole, + PYTHONPATH, SUPERSET_CONFIG_FILENAME, }; use strum::{EnumDiscriminants, IntoStaticStr}; @@ -74,6 +81,21 @@ pub enum Error { source: stackable_operator::error::Error, rolegroup: RoleGroupRef, }, + #[snafu(display("failed to build config file for {}", rolegroup))] + BuildRoleGroupConfigFile { + source: FlaskAppConfigWriterError, + rolegroup: RoleGroupRef, + }, + #[snafu(display("failed to build ConfigMap for {}", rolegroup))] + BuildRoleGroupConfig { + source: stackable_operator::error::Error, + rolegroup: RoleGroupRef, + }, + #[snafu(display("failed to apply ConfigMap for {}", rolegroup))] + ApplyRoleGroupConfig { + source: stackable_operator::error::Error, + rolegroup: RoleGroupRef, + }, #[snafu(display("failed to apply StatefulSet for {}", rolegroup))] ApplyRoleGroupStatefulSet { source: stackable_operator::error::Error, @@ -123,7 +145,10 @@ pub async fn reconcile_superset( [( SupersetRole::Node.to_string(), ( - vec![PropertyNameKind::Env], + vec![ + PropertyNameKind::Env, + PropertyNameKind::File(SUPERSET_CONFIG_FILENAME.into()), + ], superset.spec.nodes.clone().context(NoNodeRoleSnafu)?, ), )] @@ -149,6 +174,7 @@ pub async fn reconcile_superset( let rolegroup = superset.node_rolegroup_ref(rolegroup_name); let rg_service = build_node_rolegroup_service(&rolegroup, &superset)?; + let rg_configmap = build_rolegroup_config_map(&superset, &rolegroup, rolegroup_config)?; let rg_statefulset = build_server_rolegroup_statefulset(&rolegroup, &superset, rolegroup_config)?; client @@ -157,6 +183,12 @@ pub async fn reconcile_superset( .with_context(|_| ApplyRoleGroupServiceSnafu { rolegroup: rolegroup.clone(), })?; + client + .apply_patch(FIELD_MANAGER_SCOPE, &rg_configmap, &rg_configmap) + .await + .with_context(|_| ApplyRoleGroupConfigSnafu { + rolegroup: rolegroup.clone(), + })?; client .apply_patch(FIELD_MANAGER_SCOPE, &rg_statefulset, &rg_statefulset) .await @@ -208,6 +240,73 @@ pub fn build_node_role_service(superset: &SupersetCluster) -> Result { }) } +/// The rolegroup [`ConfigMap`] configures the rolegroup based on the configuration given by the administrator +fn build_rolegroup_config_map( + superset: &SupersetCluster, + rolegroup: &RoleGroupRef, + rolegroup_config: &HashMap>, +) -> Result { + let mut config = rolegroup_config + .get(&PropertyNameKind::File( + SUPERSET_CONFIG_FILENAME.to_string(), + )) + .cloned() + .unwrap_or_default(); + + config.insert( + SupersetConfigOptions::SecretKey.to_string(), + "os.environ.get('SECRET_KEY')".into(), + ); + config.insert( + SupersetConfigOptions::SqlalchemyDatabaseUri.to_string(), + "os.environ.get('SQLALCHEMY_DATABASE_URI')".into(), + ); + config.insert( + SupersetConfigOptions::StatsLogger.to_string(), + "StatsdStatsLogger(host='0.0.0.0', port=9125)".into(), + ); + + let imports = [ + "import os", + "from superset.stats_logger import StatsdStatsLogger", + ]; + + let mut config_file = Vec::new(); + flask_app_config_writer::write::( + &mut config_file, + config.iter(), + &imports, + ) + .with_context(|_| BuildRoleGroupConfigFileSnafu { + rolegroup: rolegroup.clone(), + })?; + + ConfigMapBuilder::new() + .metadata( + ObjectMetaBuilder::new() + .name_and_namespace(superset) + .name(rolegroup.object_name()) + .ownerreference_from_resource(superset, None, Some(true)) + .context(ObjectMissingMetadataForOwnerRefSnafu)? + .with_recommended_labels( + superset, + APP_NAME, + superset_version(superset).context(NoSupersetVersionSnafu)?, + &rolegroup.role, + &rolegroup.role_group, + ) + .build(), + ) + .add_data( + SUPERSET_CONFIG_FILENAME, + String::from_utf8(config_file).unwrap(), + ) + .build() + .with_context(|_| BuildRoleGroupConfigSnafu { + rolegroup: rolegroup.clone(), + }) +} + /// The rolegroup [`Service`] is a headless service that allows direct access to the instances of a certain rolegroup /// /// This is mostly useful for internal communication between peers, or for clients that perform client-side load balancing. @@ -281,7 +380,7 @@ fn build_server_rolegroup_statefulset( let superset_version = superset_version(superset).context(NoSupersetVersionSnafu)?; - let image = format!("docker.stackable.tech/stackable/superset:{superset_version}-stackable0"); + let image = format!("docker.stackable.tech/stackable/superset:{superset_version}-stackable1"); let statsd_exporter_version = statsd_exporter_version(superset).context(NoStatsdExporterVersionSnafu)?; @@ -289,25 +388,29 @@ fn build_server_rolegroup_statefulset( let statsd_exporter_image = format!("docker.stackable.tech/prom/statsd-exporter:{statsd_exporter_version}"); - let env = node_config + let mut cb = ContainerBuilder::new("superset"); + + for (name, value) in node_config .get(&PropertyNameKind::Env) - .and_then(|vars| vars.get(SupersetConfig::CREDENTIALS_SECRET_PROPERTY)) - .map(|secret| { - vec![ - env_var_from_secret("SECRET_KEY", secret, "connections.secretKey"), - env_var_from_secret( - "SQLALCHEMY_DATABASE_URI", - secret, - "connections.sqlalchemyDatabaseUri", - ), - ] - }) - .unwrap_or_default(); + .cloned() + .unwrap_or_default() + { + if name == SupersetConfig::CREDENTIALS_SECRET_PROPERTY { + cb.add_env_var_from_secret("SECRET_KEY", &value, "connections.secretKey"); + cb.add_env_var_from_secret( + "SQLALCHEMY_DATABASE_URI", + &value, + "connections.sqlalchemyDatabaseUri", + ); + } else { + cb.add_env_var(name, value); + }; + } - let container = ContainerBuilder::new("superset") + let container = cb .image(image) - .add_env_vars(env) .add_container_port("http", APP_PORT.into()) + .add_volume_mount("config", PYTHONPATH) .build(); let metrics_container = ContainerBuilder::new("metrics") .image(statsd_exporter_image) @@ -358,6 +461,14 @@ fn build_server_rolegroup_statefulset( }) .add_container(container) .add_container(metrics_container) + .add_volume(Volume { + name: "config".to_string(), + config_map: Some(ConfigMapVolumeSource { + name: Some(rolegroup_ref.object_name()), + ..Default::default() + }), + ..Default::default() + }) .build_template(), ..StatefulSetSpec::default() }), diff --git a/rust/operator-binary/src/superset_db_controller.rs b/rust/operator-binary/src/superset_db_controller.rs index 142e75b8..23afb7c0 100644 --- a/rust/operator-binary/src/superset_db_controller.rs +++ b/rust/operator-binary/src/superset_db_controller.rs @@ -1,4 +1,4 @@ -use crate::util::{env_var_from_secret, get_job_state, JobState}; +use crate::util::{get_job_state, JobState}; use snafu::{ResultExt, Snafu}; use stackable_operator::{ @@ -16,7 +16,10 @@ use stackable_operator::{ }, logging::controller::ReconcilerError, }; -use stackable_superset_crd::supersetdb::{SupersetDB, SupersetDBStatus, SupersetDBStatusCondition}; +use stackable_superset_crd::{ + supersetdb::{SupersetDB, SupersetDBStatus, SupersetDBStatusCondition}, + PYTHONPATH, SUPERSET_CONFIG_FILENAME, +}; use std::{sync::Arc, time::Duration}; use strum::{EnumDiscriminants, IntoStaticStr}; @@ -145,7 +148,11 @@ pub async fn reconcile_superset_db( } fn build_init_job(superset_db: &SupersetDB) -> Result { + let config = "import os; SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI')"; + let mut commands = vec![ + format!("mkdir -p {PYTHONPATH}"), + format!("echo \"{config}\" > {PYTHONPATH}/{SUPERSET_CONFIG_FILENAME}"), String::from( "superset fab create-admin \ --username \"$ADMIN_USERNAME\" \ @@ -165,7 +172,7 @@ fn build_init_job(superset_db: &SupersetDB) -> Result { let container = ContainerBuilder::new("superset-init-db") .image(format!( - "docker.stackable.tech/stackable/superset:{}-stackable0", + "docker.stackable.tech/stackable/superset:{}-stackable1", superset_db.spec.superset_version )) .command(vec!["/bin/bash".to_string()]) @@ -175,19 +182,12 @@ fn build_init_job(superset_db: &SupersetDB) -> Result { String::from("-c"), commands.join("; "), ]) - .add_env_vars(vec![ - env_var_from_secret("SECRET_KEY", secret, "connections.secretKey"), - env_var_from_secret( - "SQLALCHEMY_DATABASE_URI", - secret, - "connections.sqlalchemyDatabaseUri", - ), - env_var_from_secret("ADMIN_USERNAME", secret, "adminUser.username"), - env_var_from_secret("ADMIN_FIRSTNAME", secret, "adminUser.firstname"), - env_var_from_secret("ADMIN_LASTNAME", secret, "adminUser.lastname"), - env_var_from_secret("ADMIN_EMAIL", secret, "adminUser.email"), - env_var_from_secret("ADMIN_PASSWORD", secret, "adminUser.password"), - ]) + .add_env_var_from_secret("DATABASE_URI", secret, "connections.sqlalchemyDatabaseUri") + .add_env_var_from_secret("ADMIN_USERNAME", secret, "adminUser.username") + .add_env_var_from_secret("ADMIN_FIRSTNAME", secret, "adminUser.firstname") + .add_env_var_from_secret("ADMIN_LASTNAME", secret, "adminUser.lastname") + .add_env_var_from_secret("ADMIN_EMAIL", secret, "adminUser.email") + .add_env_var_from_secret("ADMIN_PASSWORD", secret, "adminUser.password") .build(); let pod = PodTemplateSpec { diff --git a/rust/operator-binary/src/util.rs b/rust/operator-binary/src/util.rs index ddae5030..e810648a 100644 --- a/rust/operator-binary/src/util.rs +++ b/rust/operator-binary/src/util.rs @@ -1,6 +1,5 @@ use snafu::{OptionExt, Snafu}; use stackable_operator::k8s_openapi::api::batch::v1::Job; -use stackable_operator::k8s_openapi::api::core::v1::{EnvVar, EnvVarSource, SecretKeySelector}; use stackable_superset_crd::SupersetCluster; #[derive(Snafu, Debug)] @@ -52,18 +51,3 @@ pub fn statsd_exporter_version(superset: &SupersetCluster) -> Result<&str, Error .as_deref() .context(ObjectHasNoStatsdExporterVersion) } - -pub fn env_var_from_secret(var_name: &str, secret: &str, secret_key: &str) -> EnvVar { - EnvVar { - name: String::from(var_name), - value_from: Some(EnvVarSource { - secret_key_ref: Some(SecretKeySelector { - name: Some(String::from(secret)), - key: String::from(secret_key), - ..Default::default() - }), - ..Default::default() - }), - ..Default::default() - } -}