diff --git a/.gitignore b/.gitignore index 5b443a73..76ce7878 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ .elixir_ls +.env +antora_public +node_modules +antora_tmp \ No newline at end of file diff --git a/LICENSE b/LICENSE index 3b65e1bc..8591af3d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2024 SUSE LLC +Copyright 2025 SUSE LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index dde945d8..e8388893 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,45 @@ # Trento docs -This repository contains the Trento project documentation that does not interest a specific service, such as ADR, coding standards and best practices, CI workflows, and templates. +This repository is the source of truth for the Trento project documentation. -- [Coding standards](./coding-standards) -- [Architecture Decision Records (ADR)](./adr) +- [Trento components documentation](./components/) +- [Developer Documentation](./developer/) - [Templates](./templates) -- [Guides](./guides) + +> **Note:** The content of user facing documentation is fetched from https://github.com/SUSE/doc-unversioned/tree/main/trento + +## How to contribute to development documentation? + + 1. **Create new documentation in `.adoc`**. + + 2. **Choose the appropriate location** for your new `.adoc` file: + - For **developer documentation**,like adr, rfc or architecture use `deveveloper/`. + - For **component-specific docs**, like web, wanda or the agent, use `components//`. + + 3. **Add new documentation to trento-docs site by enriching the Antora collector** + The content of the modules directory is responsible for the build of the documentation page. + + Open the appropriate `nav.adoc` file under `modules//nav.adoc` and add a new entry for your page, for example: + ```adoc + * xref:my-new-page.adoc[My New Page] + ``` + Make sure the file path is relative to modules//pages + + 4. Install Dependency + + ```bash + npm i -D -E antora + ``` + + 5. Build Antora page + + ```bash + npx antora --fetch antora-playbook.yml + ``` + + 6. Run page + + ```bash + xdg-open antora_public/index.html + ``` + diff --git a/antora-playbook.yml b/antora-playbook.yml new file mode 100644 index 00000000..57e8c352 --- /dev/null +++ b/antora-playbook.yml @@ -0,0 +1,26 @@ +site: + title: Trento Documentation + url: https://trento-project.github.io + start_page: docs::index.adoc + +content: + sources: + - url: . + branches: + - HEAD +ui: + bundle: + url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable + snapshot: true + +output: + dir: ./antora_public + +antora: + extensions: + - '@antora/collector-extension' + - require: '@antora/lunr-extension' + index_latest_only: true + + + \ No newline at end of file diff --git a/antora.yml b/antora.yml new file mode 100644 index 00000000..d3ff1161 --- /dev/null +++ b/antora.yml @@ -0,0 +1,66 @@ +name: docs +title: Trento Documentation +version: ~ +nav: +- modules/trento_doc_unversioned/nav.adoc +- modules/components/nav.adoc +- modules/api/nav.adoc +- modules/developer/nav.adoc + +ext: + collector: + # Fetch trento official documentation + - run: + command: git clone https://github.com/SUSE/doc-unversioned temp-doc-unversioned + # Get content from doc-unversioned + - scan: + dir: temp-doc-unversioned/trento/adoc + files: '**/*.adoc' + into: modules/trento_doc_unversioned/pages + # Get images from doc-unversioned + - scan: + dir: temp-doc-unversioned/trento/images/src/png + files: '**/*.png' + into: modules/trento_doc_unversioned/images + # Build asciidoc attributes from doc-unversioned + - run: + command: ./scripts/generate-attributes-yaml.sh temp-doc-unversioned/trento/adoc/generic-attributes.adoc ./antora_tmp/antora.yml + # Scan generated antora file to merge the asciidoc attributes + - scan: + dir: antora_tmp + files: 'antora.yml' + # Clean fetched doc-unversioned and remove generated antora file + - clean: + dir: temp-doc-unversioned + - clean: + dir: antora_tmp + +# Trento upstream projects components + - clean: + dir: modules/components/pages + - clean: + dir: modules/components/images + - scan: + dir: components/ + files: '**/*.adoc' + into: modules/components/pages + - scan: + dir: components/assets + files: '**/*.{png,jpg,jpeg,svg,gif}' + into: modules/components/images +# Trento developer documentation + - clean: + dir: modules/developer/pages + - scan: + dir: developer/ + files: '**/*.adoc' + into: modules/developer/pages +# Images developer documentation + - clean: + dir: modules/developer/images + - scan: + dir: developer/assets + files: '**/*.{png,jpg,jpeg,svg,gif}' + into: modules/developer/images + + diff --git a/components/agent/ci-cd-variables.adoc b/components/agent/ci-cd-variables.adoc new file mode 100644 index 00000000..9926af40 --- /dev/null +++ b/components/agent/ci-cd-variables.adoc @@ -0,0 +1,16 @@ +== CI/CD Variables expected by Trento GH Action Runner + +These are a list of the variables that our GitHub Workflow supports in +order to make a `+trento+` deployment. + +=== Secrets + +==== `+OBS_USER+` + +This the OBS user that will trigger build in `+opensuse.build.org+` +`+obsuser+` + +==== `+OBS_PASS+` + +This is the password for the OBS user that will trigger build in +`+opensuse.build.org+` `+obspassword+` diff --git a/components/agent/development/how-to-make-a-release.adoc b/components/agent/development/how-to-make-a-release.adoc new file mode 100644 index 00000000..23222dc6 --- /dev/null +++ b/components/agent/development/how-to-make-a-release.adoc @@ -0,0 +1,80 @@ +== How to release a new version of Trento Agent + +____ +Note: this document is a draft! +____ + +=== Pre-requisites + +Install +https://github.com/github-changelog-generator/github-changelog-generator[github-changelog-generator] +globally in your dev box: + +.... +gem install github_changelog_generator +.... + +=== Update the changelog and create a new tag + +The automatic changelog generation leverages GitHub labels very heavily +to produce a meaningful output following the +https://keepachangelog.com/en/1.0.0/[Keep a Changelog] specification, +grouping pull requests and issues in sections. + +Use the labels as follows: - `+enhancement+` or `+addition+` items go in +the `+Added+` section; - `+bug+` or `+fix+` items go in the `+Fixed+` +section; - `+removal+` items go in the `+Removed+` section; - unlabelled +pull requests go in the `+Other Changes+` section; - unlabelled closed +issues are ignored. + +You don’t have to label everything: the intent of the changelog is to +communicate highlights to end-users, while also being comprehensive; +this is why the `+Other changes+` section catches all the unlabelled +items and is rendered last. + +Once you do a quick round of issues/PR triaging to apply labels in a +meaningful way, follow these steps: + +[source,bash] +---- +# always create a dedicated release branch +git switch -c release-x.y.z + +# x1.y1.z1 is the previous release tag +github_changelog_generator --since-tag=x1.y1.z1 --future-release=x.y.z + +git add CHANGELOG.md +git commit -m "add x.y.z changelog entry" + +# maybe make some other last minute changes +# [...] + +# merge and tag, making sure the tag is on the merge commit +git switch main +git merge --no-ff release-x.y.z +git tag x.y.z + +# don't forget to force update the rolling tag! +git fetch --tags -f + +# push directly +git push --tags origin main +---- + +Optionally, open a pull request from the release branch instead of +tagging and pushing manually. + +=== GitHub release + +____ +Note: this step will soon be automated. +____ + +Go to the https://github.com/trento-project/agent/releases[project +releases page] and create a new release, then: + +* use the just created git tag as the release tag and title; +* copy-paste the last changelog entry from `+CHANGELOG.md+` as the +release body; +* hit the green button; +* profit! diff --git a/components/assets/keycloack_login.png b/components/assets/keycloack_login.png new file mode 100644 index 00000000..9f4db7c8 Binary files /dev/null and b/components/assets/keycloack_login.png differ diff --git a/components/assets/trento-monitoring.png b/components/assets/trento-monitoring.png new file mode 100644 index 00000000..7312cfb9 Binary files /dev/null and b/components/assets/trento-monitoring.png differ diff --git a/components/assets/trento-spa-login.png b/components/assets/trento-spa-login.png new file mode 100644 index 00000000..425551d6 Binary files /dev/null and b/components/assets/trento-spa-login.png differ diff --git a/components/assets/trento-spa-refresh-failed.png b/components/assets/trento-spa-refresh-failed.png new file mode 100644 index 00000000..ec28c1a2 Binary files /dev/null and b/components/assets/trento-spa-refresh-failed.png differ diff --git a/components/assets/trento-spa-refresh.png b/components/assets/trento-spa-refresh.png new file mode 100644 index 00000000..3011d479 Binary files /dev/null and b/components/assets/trento-spa-refresh.png differ diff --git a/components/assets/trento_single_sign_on_login.png b/components/assets/trento_single_sign_on_login.png new file mode 100644 index 00000000..08879601 Binary files /dev/null and b/components/assets/trento_single_sign_on_login.png differ diff --git a/components/helm_charts/trento-server.adoc b/components/helm_charts/trento-server.adoc new file mode 100644 index 00000000..c02d556c --- /dev/null +++ b/components/helm_charts/trento-server.adoc @@ -0,0 +1,135 @@ +== Trento Server Helm Chart + +=== Installation + +==== Requirements + +The _Trento Server_ is intended to run in many ways, depending on users’ +already existing infrastructure, but it’s designed to be cloud-native +and OS agnostic. As such, our default installation method provisions a +minimal, single node, [K3S] Kubernetes cluster to run its various +components in Linux containers. The suggested physical resources for +running all the _Trento Server_ components are 2GB of RAM and 2 CPU +cores. The _Trento Server_ needs to reach the target infrastructure. + +==== Quick-start installation + +An installation script is provided to quickly get you started by +automatically provisioning, installing and updating the latest version +of Trento. + +The script installs a single node K3s cluster and uses the +link:../charts/trento-server[trento-server Helm chart] to bootstrap a +complete Trento server component. + +You can `+curl | bash+` if you want to live on the edge. + +.... +curl -sfL https://raw.githubusercontent.com/trento-project/helm-charts/main/scripts/install-server.sh | bash +.... + +Or you can fetch the script, and then execute it manually. + +.... +curl -O https://raw.githubusercontent.com/trento-project/helm-charts/main/scripts/install-server.sh +chmod 700 install-server.sh +sudo ./install-server.sh +.... + +_Note: if a Trento server is already installed in the host, it will be +updated._ + +Please refer to the link:#helm-chart[Helm chart] section for more +information about the Helm chart. + +==== Manual installation + +===== Helm chart + +The charts/trento-server directory contains the Helm chart for +installing Trento Server in a Kubernetes cluster. + +===== Install K3S + +If installing as root: + +.... +# curl -sfL https://get.k3s.io | sh +.... + +If installing as non-root user: + +.... +curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644 +.... + +Export KUBECONFIG env variable: + +.... +export KUBECONFIG=/etc/rancher/k3s/k3s.yaml +.... + +Please refer to the +https://rancher.com/docs/k3s/latest/en/installation/[K3S official +documentation] for more information about the installation. + +===== Install Helm and chart dependencies + +Install Helm: + +.... +curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash +.... + +Please refer to the https://helm.sh/docs/intro/install/[Helm official +documentation] for more information about the installation. + +===== Install Trento Server + +Add third-party Helm repositories: + +.... +helm repo add bitnami https://charts.bitnami.com/bitnami +helm repo update +.... + +Install chart dependencies: + +.... +cd charts/trento-server/ +helm dependency update +.... + +Install the Trento Server chart: + +.... +helm install trento . +.... + +or perform a rolling update: + +.... +helm upgrade trento . --set trento-web.trentoWebOrigin="trento.example.com" +.... + +_Note: be sure to replace trento.example.com with a valid hostname that +points to the Trento server._ + +Now you can connect to the web server via `+http://localhost+` and point +the agents to the cluster IP address. + +===== Other Helm chart usage examples + +Use a different container image (e.g. the `+rolling+` one): + +.... +helm install trento . --set trento-web.image.tag="rolling" --set trento-wanda.image.tag="rolling" +.... + +Use a different container registry: + +.... +helm install trento . --set trento-web.image.repository="ghcr.io/myrepo/trento-web" --set trento-wanda.image.repository="ghcr.io/myrepo/trento-wanda" +.... + +Please refer to the the subcharts `+values.yaml+` for an advanced usage. diff --git a/components/wanda/development/demo.adoc b/components/wanda/development/demo.adoc new file mode 100644 index 00000000..f7e83979 --- /dev/null +++ b/components/wanda/development/demo.adoc @@ -0,0 +1,151 @@ +== Wanda Demo + +=== Introduction + +Wanda’s demo mode provides a simulated environment where targets’ +gathered facts are faked via a configuration yaml file. This empowers +showcasing Trento’s capabilities, and eases the development of the +platform. The user is able to run check executions from the web, and +wanda’s fake server returns faked gathered facts. + +=== How to setup Wanda in demo mode? + +First set up the link:./hack_on_wanda.md[local environment] and run the +following command to start Wanda in demo mode, instead of the usual +`+iex -S mix phx.server+` + +[source,bash] +---- +$ DATABASE_URL=ecto://postgres:postgres@localhost:5434/wanda_dev \ + AMQP_URL=amqp://wanda:wanda@localhost:5674 \ + SECRET_KEY_BASE=Tbp26GilFTZOXafb7FNVqt4dFQdeb7FAJ++am7ItYx2sMzYaPSB9SwUczdJu6AhQ \ + CORS_ORIGIN=http://localhost:4000 \ + ACCESS_TOKEN_ENC_SECRET=s2ZdE+3+ke1USHEJ5O45KT364KiXPYaB9cJPdH3p60t8yT0nkLexLBNw8TFSzC7k \ + PORT=4001 \ + MIX_ENV=demo \ + iex -S mix phx.server +---- + +==== Explanation of environment variables: + +* DATABASE_URL: The URL to connect to the Postgres database running in +the Docker container. +* AMQP_URL: The URL for RabbitMQ, used for message exchange. +* SECRET_KEY_BASE: The secret key for Wanda. +* CORS_ORIGIN: The origin URL from where API requests are allowed +(pointing to web). +* ACCESS_TOKEN_ENC_SECRET: Secret key for encrypting access tokens. +* PORT: The port on which Wanda will run (4001 in this case). +* MIX_ENV: The environment in which Wanda will run (set to "`demo`" for +the demo environment). + +=== Modify demo facts configuration + +In the Wanda demo environment, configure fake facts by editing the +https://github.com/trento-project/wanda/blob/main/priv/demo/fake_facts.yaml[fake_facts +configuration]. Define or modify custom facts for different targets and +checks. + +==== YAML Structure + +The Configuration is saved in +https://github.com/trento-project/wanda/blob/main/priv/demo/fake_facts.yaml[fake_facts.yaml] +file, which follows a specific structure with two main sections: +`+targets+` and `+facts+`. + +Example: + +[source,yaml] +---- +targets: + target1: 0a055c90-4cb6-54ce-ac9c-ae3fedaf40d4 + target2: 13e8c25c-3180-5a9a-95c8-51ec38e50cfc +facts: + check_1: + fact_name1: + target1: 2 + target2: 3 +---- + +==== Targets + +The targets section allows defining handles for targets’ UUIDs, +providing a reference that can be used through the configuration. + +Format: + +[source,yaml] +---- +targets: + : +---- + +`++`: A user-defined handle or reference for a target. + +`++`: The UUID or identifier of the target. + +Add as many new target entries as required: + +[source,yaml] +---- +targets: + target1: 0a055c90-4cb6-54ce-ac9c-ae3fedaf40d4 + target2: 13e8c25c-3180-5a9a-95c8-51ec38e50cfc + : +---- + +==== Facts: + +The facts section allows specifying what fact value a target should +return for a specific check. + +The format for the facts section is as follows: + +[source,yaml] +---- +facts: + : + : + : + : +---- + +* There can be as many as needed. +* Each there can be as many configured as needed. +* Each can be instrumented to have a specific on a . + +Example: + +[source,yaml] +---- +targets: + target1: 0a055c90-4cb6-54ce-ac9c-ae3fedaf40d4 + target2: 13e8c25c-3180-5a9a-95c8-51ec38e50cfc + target3: 3f16cb32-57fb-46cc-9bf1-cd8d72d7eb9a + +facts: +.......existing facts.......... + new_check_id: + new_fact_name: + target1: + target2: + target3: +---- + +=== FAQ + +==== What happens with unmapped targets, facts or checks? + +Wanda will still evaluate and return a default fallback fact value +"`some fact value`" for a check’s execution. This means when a new check +is added to wanda, changing the the configuration is optional. + +==== How to know what’s the correct return value of a fact? + +Every Fact Gatherer has specific return values. To get an overview of +all gatherers, refer to link:../gatherers.md[Wanda’s gatherers guide]. +Determining the correct return value of a fact requires inspecting the +specific check itself. Visit the +https://github.com/trento-project/wanda/blob/main/priv/catalog[Checks +Catalog] to find out which gatherer is used for the specific check and +which values are expected. diff --git a/components/wanda/development/hack_on_wanda.adoc b/components/wanda/development/hack_on_wanda.adoc new file mode 100644 index 00000000..6ba1ade0 --- /dev/null +++ b/components/wanda/development/hack_on_wanda.adoc @@ -0,0 +1,88 @@ +== Hack on Wanda + +=== Requirements + +In order to run Wanda, the following software must be installed: + +[arabic] +. https://elixir-lang.org/[Elixir] +. https://www.erlang.org/[Erlang OTP] +. https://www.rust-lang.org/tools/install[Rust] +. https://docs.docker.com/get-docker/[Docker] +. https://docs.docker.com/compose/install/[Docker Compose] + +==== Ensure Compatibility with asdf + +https://asdf-vm.com/guide/introduction.html[asdf] allows using specific +versions of programming language tools that are known to be compatible +with the project, rather than relying on the version that’s installed +globally on the host system. + +In order to use asdf, follow the official +https://asdf-vm.com/guide/getting-started.html[asdf getting started +guide]. + +Install all required asdf plugins from +link:/.tool-versions[.tool-versions] inside the web repository. + +.... +cut -d' ' -f1 .tool-versions|xargs -i asdf plugin add {} +.... + +Set up the asdf environment + +.... +asdf install +.... + +=== Development environment + +A `+docker-compose+` environment is provided for easy local development. +To start the environment, run the following command: + +.... +docker-compose up -d +.... + +This command starts a *Postgres* database and a *RabbitMq* instance, +which is used for storage and communication. + +=== Setup Wanda + +Before starting Wanda, some initial setup tasks are required. This can +be achieved by running the following command: + +.... +mix setup +.... + +This command performs necessary tasks such as installing dependencies, +creating the database schema and running migrations. + +==== Hint about Project setup + +Gain a deeper understanding of how Wanda is configured, reading the +https://github.com/trento-project/wanda/blob/main/mix.exs[mix.exs] file +located in the root directory of the project. This file contains +information on dependencies, configuration settings, and tasks that can +be run using the Mix build tool, providing a complete picture of the +project’s setup. + +=== Start Wanda in the REPL + +To start Wanda, you need to run the following command: + +.... +iex -S mix phx.server +.... + +=== Access Wanda Swaggerui + +Congratulations, Wanda is now running locally on your machine! You can +access its API documentation by visiting the following URL: +http://localhost:4001/swaggerui[localhost:4001/swaggerui]. + +The Swagger UI provides a user-friendly interface for exploring and +testing the various API endpoints of Wanda. + +Happy Hacking! diff --git a/components/wanda/expression_language.adoc b/components/wanda/expression_language.adoc new file mode 100644 index 00000000..abc5fea7 --- /dev/null +++ b/components/wanda/expression_language.adoc @@ -0,0 +1,327 @@ +== Expression Language + +A small, fast, easy-to-use scripting language and evaluation engine. + +=== Introduction + +An embedded scripting language and evaluation engine for Trento Checks +Expressions that gives a safe and easy way to script specific steps +during Checks Execution. + +=== Types + +[cols=",",options="header",] +|=== +|Type |Example +|*Nothing/void/nil/null/Unit* |`+()+` +|*Integer* |`+42+`, `+123+` +|*Float* |`+123.4567+` +|*Boolean* |`+true+` or `+false+` +|*String* |`+"hello"+` +|*Array* |`+[ 1, 2, 3, "foobar" ]+` +|*Map* |`+#{ "a": 1, "b": true }+` +|=== + +=== Logic Operators and Boolean + +[width="100%",cols="^10%,35%,^32%,^23%",options="header",] +|=== +|Operator |Description(`+x+` _operator_ `+y+`) |`+x+`, `+y+` same type +or are numeric |`+x+`, `+y+` different types +|`+==+` |`+x+` is equals to `+y+` |error if not defined |`+false+` if +not defined + +|`+!=+` |`+x+` is not equals to `+y+` |error if not defined |`+true+` if +not defined + +|`+>+` |`+x+` is greater than `+y+` |error if not defined |`+false+` if +not defined + +|`+>=+` |`+x+` is greater than or equals to `+y+` |error if not defined +|`+false+` if not defined + +|`+<+` |`+x+` is less than `+y+` |error if not defined |`+false+` if not +defined + +|`+<=+` |`+x+` is less than or equals to `+y+` |error if not defined +|`+false+` if not defined +|=== + +==== Comparing different types defaults to `+false+` + +Comparing two values of _different_ data types defaults to `+false+`. + +The exception is `+!=+` (not equals) which defaults to `+true+`. This is +in line with intuition. + +[source,ts] +---- +42 > "42"; // false: i64 cannot be compared with string +42 <= "42"; // false: i64 cannot be compared with string +ts == 42; // false: different types cannot be compared +ts != 42; // true: different types cannot be compared +---- + +==== Boolean Operators + +[cols="^,^,^,^",options="header",] +|=== +|Operator |Description |Arity |Short-circuits? +|`+!+` _(prefix)_ |_NOT_ |unary |no +|`+&&+` |_AND_ |binary |yes +|`+&+` |_AND_ |binary |no +|\|\| |_OR_ |binary |yes +|\| |_OR_ |binary |no +|=== + +Double boolean operators `+&&+` and `+||+` _short-circuit_ – meaning +that the second operand will not be evaluated if the first one already +proves the condition wrong. + +Single boolean operators `+&+` and `+|+` always evaluate both operands. + +[source,ts] +---- +a() || b(); // b() is not evaluated if a() is true +a() && b(); // b() is not evaluated if a() is false +a() | b(); // both a() and b() are evaluated +a() & b(); // both a() and b() are evaluated +---- + +=== If Statement + +`+if+` statements follow C syntax. + +[source,ts] +---- +if foo(x) { + print("It's true!"); +} else if bar == baz { + print("It's true again!"); +} else if baz.is_foo() { + print("Yet again true."); +} else if foo(bar - baz) { + print("True again... this is getting boring."); +} else { + print("It's finally false!"); +} +---- + +____ +Unlike C, the condition expression does _not_ need to be enclosed in +parentheses `+(+`…`+)+`, but all branches of the `+if+` statement must +be enclosed within braces `+{+`…`+}+`, even when there is only one +statement inside the branch. Like Rust, there is no ambiguity regarding +which `+if+` clause a branch belongs to. + +[source,ts] +---- +// not C! +if (decision) print(42); +// ^ syntax error, expecting '{' +---- +____ + +==== If Expression + +`+if+` statements can also be used as _expressions_, replacing the +`+? :+` conditional operators in other C-like languages. + +[source,ts] +---- +// The following is equivalent to C: int x = 1 + (decision ? 42 : 123) / 2; +let x = 1 + if decision { 42 } else { 123 } / 2; +x == 22; +let x = if decision { 42 }; // no else branch defaults to '()' +x == (); +---- + +=== Arrays + +All elements stored in an array are dynamic, and the array can freely +grow or shrink with elements added or removed. + +Array literals are built within square brackets `+[+` … `+]+` and +separated by commas `+,+`: + +____ +`+[+` _value_`+,+` _value_`+,+` … `+,+` _value_ `+]+` + +`+[+` _value_`+,+` _value_`+,+` … `+,+` _value_ `+,+` `+]+` +`+// trailing comma is OK+` +____ + +[source,ts] +---- +let some_list = [1, 2, 3]; + +let another_list = ["foo", "bar", 42]; +---- + +==== Access Element From beginning + +Like C, arrays are accessed with zero-based, non-negative integer +indices: + +____ +_array_ `+[+` _index position from 0 to length−1_ `+]+` +____ + +[source,ts] +---- +let some_list = ["foo", "bar", 42]; + +let second_element = some_list[1]; + +// second_element is "bar" +---- + +==== Access Element From end + +A _negative_ position accesses an element in the array counting from the +_end_, with −1 being the _last_ element. + +____ +_array_ `+[+` _index position from −1 to −length_ `+]+` +____ + +[source,ts] +---- +let some_list = ["foo", "bar", 42]; + +let second_element = some_list[-2]; +let last_element = some_list[-1]; + +// second_element is "bar" +// last_element is 42 +---- + +[width="100%",cols="4%,14%,82%",options="header",] +|=== +|Function |Parameter(s) |Description +|`+get+` |position, counting from end if < 0 |gets a copy of the element +at a certain position (`+()+` if the position is not valid) + +|`+len+` |_none_ |returns the number of elements + +|`+filter+` |predicate (usually a closure) |constructs a new array with +all items that return `+true+` when called with the predicate function +taking the following parameters: + +|`+all+` |predicate (usually a closure) |returns `+true+` if all items +return `+true+` when called with the predicate function taking the +following parameters: +|=== + +Examples + +[source,ts] +---- +let some_list = [1, 2, 3, 4, "foo", "bar"]; + +let foo = some_list.get(4); // "foo" + +let items_count = some_list.len(); // 6 + +let only_foo_and_bar = some_list.filter(|item| item == "foo" || item == "bar"); // ["foo", "bar"] +// let only_foo_and_bar = some_list.filter(|item, idex_in_array| item == "foo" || item == "bar"); + +let another_list = [3, 5, 7, 9, 10, 20, 30]; + +let all_greater_than_2 = another_list.all(|item| item > 2); // true +let all_greater_than_10 = another_list.all(|item| item > 10); // false +// let all_greater_than_10 = another_list.all(|item, idex_in_array| item > 10); +---- + +=== Maps + +Maps are hash dictionaries. Properties are all dynamic values and can be +freely added and retrieved. + +Map literals are built within braces `+#{+` … `+}+` with +_name_`+:+`_value_ pairs separated by commas `+,+`: + +____ +`+#{+` _property_ `+:+` _value_`+,+` … `+,+` _property_ `+:+` _value_ +`+}+` + +`+#{+` _property_ `+:+` _value_`+,+` … `+,+` _property_ `+:+` _value_ +`+,+` `+}+` `+// trailing comma is OK+` +____ + +[source,ts] +---- +let some_map = #{ // map literal with 2 properties + foo: 42, + bar: "hello", +}; +---- + +==== Dot notation + +The _dot notation_ allows to access properties by name. + +____ +_object_ `+.+` _property_ +____ + +[source,ts] +---- +let some_map = #{ // map literal with 2 properties + foo: 42, + bar: "hello", +}; + +some_map.foo // 42 +some_map.bar // "hello" +---- + +==== Non-existing property + +Trying to read a non-existing property returns an error. + +[source,ts] +---- +let some_map = #{ // map literal with 2 properties + foo: 42, + bar: "hello", +}; + +some_map.another_property // returns "Property not found: another_property (line X, position Y)" +---- + +==== A more complex example + +[source,ts] +---- +let some_map = #{ // map literal with 2 properties + foo: 42, + bar: "hello", + rabbits: [ + #{ + name: "wanda", + power: 9001 + }, + #{ + name: "tonio", + power: 9002 + }, + #{ + name: "weak_rabbit", + power: 8999 + } + ] +}; + +// Tell me how many strong rabbits are there +let strong_rabbits = some_map.rabbits.filter(|rabbit| rabbit.power > 9000).len() // 2 + +let rabbits = some_map.rabbits + +let all_rabbits_are_strong = rabbits.all(|rabbit| rabbit.power > 9000) // false, unfortunately +---- + +=== Rhai + +For extra information about the underlying scripting language see +https://rhai.rs/book/language/[Rhai]. diff --git a/components/wanda/gatherers.adoc b/components/wanda/gatherers.adoc new file mode 100644 index 00000000..17878f0b --- /dev/null +++ b/components/wanda/gatherers.adoc @@ -0,0 +1,1948 @@ +== Gatherers + +=== Introduction + +Gatherers can be thought of as functions: + +* they have a name +* they accept argument(s) +* they return a value, the gathered link:./specification.md#facts[Fact] + +Facts Gathering process in a nutshell + +.... +fact = gatherer(argument) +.... + +=== Gatherers versioning + +The gatherers implementation supports a versioning mechanism in order to +enable non-backwards compatibility changes in any of them. When an +update to the trento-agent includes a non-backwards compatible change in +a gatherer (e.g., changes to the Rhai output format), its version is +bumped by incrementing the @vN suffix that follows the gatherer’s name, +where '`N`' represents the new version of that gatherer. Example: + +* `+systemd@v1+` -> Represents the first version of the systemd gatherer +* `+systemd@v2+` -> Represents the second version of the systemd +gatherer + +Note that when writing a check, if no tag is specified +(e.g. `+systemd+`), the latest version is used. It is *strongly* +recommended to always pin your checks to a specific version of a +gatherer. + +Not all changes in a released gatherer get a new version tag. A new +version tag is released only for breaking changes, while non-breaking +changes such as additional fields in the Rhai output reuse the latest +existing tag. To use a check that relies on a newer field introduced +after an update, upgrade the agent to the latest version to ensure that +the required gatherers are also up-to-date. + +=== Available Gatherers + +Here’s a collection of built-in gatherers, with information about how to +use them. + +[width="100%",cols="<29%,<71%",options="header",] +|=== +|Name |Implementation +|link:#ascsers_clusterv1[`+ascsers_cluster@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/ascsers_cluster.go[trento-project/agent/../gatherers/ascsers_cluster.go] + +|link:#cibadminv1[`+cibadmin@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/cibadmin.go[trento-project/agent/../gatherers/cibadmin.go] + +|link:#corosync-cmapctlv1[`+corosync-cmapctl@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/corosynccmapctl.go[trento-project/agent/../gatherers/corosynccmapctl.go] + +|link:#corosyncconfv1[`+corosync.conf@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/corosyncconf.go[trento-project/agent/../gatherers/corosyncconf.go] + +|link:#dir_scanv1[`+dir_scan@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/dir_scan.go[trento-project/agent/../gatherers/dir_scan.go] + +|link:#dispworkv1[`+disp+work@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/dispwork.go[trento-project/agent/../gatherers/dispwork.go] + +|link:#fstabv1[`+fstab@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/fstab.go[trento-project/agent/../gatherers/fstab.go] + +|link:#groupsv1[`+groups@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/groups.go[trento-project/agent/../gatherers/groups.go] + +|link:#hostsv1[`+hosts@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/hostsfile.go[trento-project/agent/../gatherers/hostsfile.go] + +|link:#ini_filesv1[`+ini_files@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/ini_files.go[trento-project/agent/../gatherers/ini_files.go] + +|link:#mount_infov1[`+mount_info@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/mountinfo.go[trento-project/agent/../gatherers/mountinfo.go] + +|link:#os-releasev1[`+os-release@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/osrelease.go[trento-project/agent/../gatherers/osrelease.go] + +|link:#package_versionv1[`+package_version@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/packageversion.go[trento-project/agent/../gatherers/packageversion.go] + +|link:#passwdv1[`+passwd@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/passwd.go[trento-project/agent/../gatherers/passwd.go] + +|link:#productsv1[`+products@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/products.go[trento-project/agent/../gatherers/products.go] + +|link:#sap_profilesv1[`+sap_profiles@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/sapprofiles.go[trento-project/agent/../gatherers/sapprofiles.go] + +|link:#sapcontrolv1[`+sapcontrol@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/sapcontrol.go[trento-project/agent/../gatherers/sapcontrol.go] + +|link:#saphostctrlv1[`+saphostctrl@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/saphostctrl.go[trento-project/agent/../gatherers/saphostctrl.go] + +|link:#sapinstance_hostname_resolverv1[`+sapinstance_hostname_resolver@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/sapinstancehostnameresolver.go[trento-project/agent/../gatherers/sapinstancehostnameresolver.go] + +|link:#sapservicesv1[`+sapservices@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/sapservices.go[trento-project/agent/../gatherers/sapservices.go] + +|link:#saptunev1[`+saptune@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/saptune.go[trento-project/agent/../gatherers/saptune.go] + +|link:#sbd_configv1[`+sbd_config@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/sbd.go[trento-project/agent/../gatherers/sbd.go] + +|link:#sbd_dumpv1[`+sbd_dump@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/sbddump.go[trento-project/agent/../gatherers/sbddump.go] + +|link:#sudoersv1[`+sudoers@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/sudoers.go[trento-project/agent/../gatherers/sudoers.go] + +|link:#sysctlv1[`+sysctl@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/sysctl.go[trento-project/agent/../gatherers/sysctl.go] + +|link:#systemdv1[`+systemd@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/systemd.go[trento-project/agent/../gatherers/systemd.go] + +|link:#systemdv2[`+systemd@v2+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/systemd_v2.go[trento-project/agent/../gatherers/systemd_v2.go] + +|link:#verify_passwordv1[`+verify_password@v1+`] +|https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/verifypassword.go[trento-project/agent/../gatherers/verifypassword.go] +|=== + +[#ascsers_clusterv1]## + +==== ascsers_cluster@v1 + +*Argument required*: no. + +This gatherer allows accessing ASCS/ERS clusters managed systems most +relevant information, such as the ensa version and the installed +instances. It is really useful to check multi SID environments. It +doesn’t replace the need to use the `+cibadmin+` gatherer, as this +gatherer aims to facilitate complex data manipulations using data from +that gatherer. The ensa version is obtained running the +`+GetProcessList+` sapcontrol webcommand and parsing the `+name+` output +in each of the installed instances. + +Example specification: + +[source,yaml] +---- +facts: + - name: ascsers + gatherer: ascsers_cluster@v1 +---- + +Example output (in Rhai): + +[source,ts] +---- +#{ + "PRD": #{ + "ensa_version": "ensa1", // available values: ensa1/ensa2/unknown + "instances": [ + #{ + "resource_group": "grp_PRD_ASCS00", + "resource_instance": "rsc_sap_PRD_ASCS00", + "name": "ASCS00", + "instance_number": "00", + "virtual_hostname": "sapascs00", + "filesystem_based": true, // if the instance resource group has a FileSystem resource + "local": true // if the instance is running locally in this host (sapcontrol returns a positive response for GetProcessList) + }, + #{ + "resource_group": "grp_PRD_ERS10", + "resource_instance": "rsc_sap_PRD_ERS10", + "name": "ERS10", + ... + } + ] + }, + "QAS": #{ + "ensa_version": "ensa2", + ... + } +}; +---- + +[#cibadminv1]## + +==== cibadmin@v1 + +*Argument required*: no. + +This gatherer allows accessing Pacemaker’s CIB information, the output +of the `+cibadmin+` command more precisely. As the `+cibadmin+` command +output is in XML format, the gatherer converts it to map/dictionary type +format, so the fields are available with the normal dot access way. Some +specific fields, such as `+primitive+`, `+clone+`, `+master+`, etc (find +the complete list +https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/cibadmin.go#L48[here]) +are converted to lists, in order to avoid differences when the field +appears one or multiple times. + +Example arguments: + +[width="100%",cols="<49%,<51%",options="header",] +|=== +|Name |Return value +|`+cib.configuration+` |complete cib configuration entry as a map + +|`+cib.configuration.resources.primitive.0+` |first available primitive +resource + +|`+cib.configuration.crm_config.cluster_property_set.0.nvpair.1+` +|second nvpair value from the first element of cluster_property_set +|=== + +Example specification: + +[source,yaml] +---- +facts: + - name: cib_configuration + gatherer: cibadmin@v1 + argument: cib.configuration + + - name: first_primitive + gatherer: cibadmin@v1 + argument: cib.configuration.resources.primitive.0 + + - name: first_cluster_property_set_second_nvpair + gatherer: cibadmin@v1 + argument: cib.configuration.crm_config.cluster_property_set.0.nvpair.1 +---- + +Example output (in Rhai): + +[source,ts] +---- +// first_primitive +#{ + class: "stonith", + id: "stonith-sbd", + instance_attributes: #{ + id: "stonith-sbd-instance_attributes", + nvpair: [#{ + id: "stonith-sbd-instance_attributes-pcmk_delay_max", + name: "pcmk_delay_max", + value: "30s" + }] + }, + type: "external/sbd" +}; + +// first_cluster_property_set_second_nvpair +#{ + id: "cib-bootstrap-options-dc-version", + name: "dc-version", + value: "2.0.4+20200616.2deceaa3a-3.12.1-2.0.4+20200616.2deceaa3a" +}; +---- + +[#corosync-cmapctlv1]## + +==== corosync-cmapctl@v1 + +*Argument required*: yes. + +This gatherer allows accessing the output of the `+corosync-cmapctl+` +tool. It supports all of the keys returned by it to be queried. + +Example arguments: + +[width="100%",cols="<53%,<47%",options="header",] +|=== +|Name |Return value +|`+totem.token+` |extracted value from the command +|`+runtime.config.totem.token+` |extracted value from the command +|`+totem.transport+` |extracted value from the command +|`+runtime.config.totem.max_messages+` |extracted value from the command +|`+nodelist.node.0.ring0_addr+` |extracted value from the command +|`+nodelist.node+` |extracted value from the command +|`+nodelist.node.1+` |extracted value from the command +|=== + +Example specification: + +[source,yaml] +---- +facts: + - name: totem_token + gatherer: corosync-cmapctl@v1 + argument: totem.token + + - name: runtime_totem_token + gatherer: corosync-cmapctl@v1 + argument: runtime.config.totem.token + + - name: totem_transport + gatherer: corosync-cmapctl@v1 + argument: totem.transport + + - name: totem_max_messages + gatherer: corosync-cmapctl@v1 + argument: runtime.config.totem.max_messages + + - name: node_0_ring0addr + gatherer: corosync-cmapctl@v1 + argument: nodelist.node.0.ring0_addr + + - name: node_list + gatherer: corosync-cmapctl@v1 + argument: nodelist.node + + - name: second_node + gatherer: corosync-cmapctl@v1 + argument: nodelist.node.1 +---- + +Example output (in Rhai): + +[source,ts] +---- +// totem_token +30000; + +// runtime_totem_token +30000; + +// totem_transport +"udpu"; + +// totem_max_messages +20; + +// node_0_ring0addr +"10.80.1.11"; + +// node_list +#{ + "0": #{ + nodeid: 1, + ring0_addr: "10.80.1.11" + }, + "1": #{ + nodeid: 2, + ring0_addr: "10.80.1.12" + } +}; + +// second_node +#{ nodeid: 2, ring0_addr: "10.80.1.12" }; +---- + +[#corosyncconfv1]## + +==== corosync.conf@v1 + +*Argument required*: no. + +This gatherer allows accessing the information contained in +`+/etc/corosync/corosync.conf+` + +Example arguments: + +[width="100%",cols="<48%,52%",options="header",] +|=== +|Name |Return value +|`+totem.token+` |extracted value from the config +|`+totem.join+` |extracted value from the config +|`+nodelist.node..nodeid+` |extracted value from the config +|`+nodelist.node+` |list of objects representing the nodes +|=== + +Example specification: + +[source,yaml] +---- +facts: + - name: corosync_token_timeout + gatherer: corosync.conf@v1 + argument: totem.token + + - name: corosync_join + gatherer: corosync.conf@v1 + argument: totem.join + + - name: corosync_node_id_0 + gatherer: corosync.conf@v1 + argument: nodelist.node.0.nodeid + + - name: corosync_node_id_1 + gatherer: corosync.conf@v1 + argument: nodelist.node.1.nodeid + + - name: corosync_nodes + gatherer: corosync.conf@v1 + argument: nodelist.node +---- + +Example output (in Rhai): + +[source,ts] +---- +// corosync_token_timeout +30000; + +// corosync_join +60; + +// corosync_node_id_0 +1; + +// corosync_node_id_1 +2; + +// corosync_nodes +[#{nodeid: 1, ring0_addr: "192.168.157.10"}, #{nodeid: 2, ring0_addr: "192.168.157.11"}]; +---- + +For extra information refer to +https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/corosyncconf_test.go[trento-project/agent/../gatherers/corosyncconf_test.go] + +[#dir_scanv1]## + +==== dir_scan@v1 + +*Argument required*: Yes + +This gatherer allows to scan directories with a glob pattern provided as +argument. The gatherer returns a list of files matched by the pattern +with group/user information associated to each file. + +Example argument: + +* `+/usr/sap/[A-Z][A-Z0-9][A-Z0-9]/ERS[0-9][0-9]+` +* `+/etc/polkit-1/rules.d/[0-9][0-9]-SAP[A-Z][A-Z0-9][A-Z0-9]-[0-9][0-9].rules+` + +Example specification: + +[source,yaml] +---- +facts: + - name: dir_scan + gatherer: dir_scan@v1 + argument: "/usr/sap/[A-Z][A-Z0-9][A-Z0-9]/ERS[0-9][0-9]" +---- + +Example output (in Rhai): + +[source,ts] +---- + [ + #{ + "name": "/usr/sap/PRD/ERS01", + "owner": "trento", + "group": "trento" + }, + #{ + "name": "/usr/sap/QAS/ERS02", + "owner": "trento", + "group": "trento" + }, + ] +---- + +[#dispworkv1]## + +==== disp+work@v1 + +*Argument required*: No + +This gatherer allows access to the `+disp+work+` command output and +returns some fields available there. The command is executed for all +installed SAP systems, accessing it with the `+adm+` user. The +fields for each system are returned in a map using the SAP sid as key. + +If the `+disp+work+` command execution fails, the fields are returned +with an empty string value. + +The available fields are `+compilation_mode+`, `+kernel_release+` and +`+patch_number+`. + +Example specification: + +[source,yaml] +---- +facts: + - name: dispwork + gatherer: disp+work@v1 +---- + +Example output (in Rhai): + +[source,ts] +---- +#{ + "NWP": #{ + "compilation_mode": "UNICODE", + "kernel_release": "753", + "patch_number": "900" + }, + // failed execution + "NWQ": #{ + "compilation_mode": "", + "kernel_release": "", + "patch_number": "" + }, + "NWD": #{ + "compilation_mode": "UNICODE", + "kernel_release": "753", + "patch_number": "910" + } +} +---- + +[#fstabv1]## + +==== fstab@v1 + +*Argument required*: no. + +This gatherer allows access to the /etc/fstab file, returning all +entries available at the file. + +Example specification: + +[source,yaml] +---- +facts: + - name: fstab + gatherer: fstab@v1 +---- + +Example output (in Rhai): + +[source,ts] +---- +[ + #{ + "device": "/dev/system/root", + "mount_point": "/", + "file_system_type": "btrfs", + "options": [], + "backup": 0, + "check_order": 1, + }, + #{ + "device": "/dev/system/root", + "mount_point": "/home", + "file_system_type": "ext4", + "options": ["defaults"], + "backup": 0, + "check_order": 1, + }, + ... +]; +---- + +[#groupsv1]## + +==== groups@v1 + +*Argument required*: no. + +This gatherer allows access to the /etc/group file, returning all +entries available at the file. + +Example specification: + +[source,yaml] +---- +facts: + - name: groups + gatherer: groups@v1 +---- + +Example output (in Rhai): + +[source,ts] +---- +[ + #{ + "name": "root", + "gid": 0, + "users": [], + }, + #{ + "name": "adm", + "gid": 1, + "users": ["trento"], + } + ... +]; +---- + +[#hostsv1]## + +==== hosts@v1 + +*Argument required*: no. + +This gatherer allows accessing the hostnames that are resolvable through +`+/etc/hosts+`. It does *not* use domain resolution in any way but +instead directly parses the file. + +It allows one argument to be specified or none at all: + +* When a hostname is provided as an argument, the gatherer will return +an array of IPv4 and/or IPv6 addresses. +* When no argument is provided, the gatherer will return a map with +hostname as keys and arrays with IPv4 and/or IPv6 addresses. + +Example arguments: + +[cols="<,<",options="header",] +|=== +|Name |Return value +|`+localhost+` |list of IPs resolving +|`+node1+` |list of IPs resolving +|`+no argument provided+` |map with hostnames and IP addresses +|=== + +Example specification: + +[source,yaml] +---- +facts: + - name: hosts_node1 + gatherer: hosts@v1 + argument: node1 + + - name: hosts_node2 + gatherer: hosts@v1 + argument: node2 + + - name: hosts_all + gatherer: hosts@v1 +---- + +Example output (in Rhai): + +[source,ts] +---- +// hosts_node1 +["127.0.0.1", "::1"]; + +// hosts_node2 +["192.168.157.11"]; + +// hosts_all +#{ + "localhost": ["127.0.0.1", "::1"], + "node1": ["192.168.157.10"], + "node2": ["192.168.157.11"], + ... +}; +---- + +[#ini_filesv1]## + +==== ini_files@v1 + +*Argument required*: yes. + +This gatherer fetches the content from a configuration file in INI +format. The configuration file is specified as argument, chosen from a +list of allowed files. Currently whitelisted files are: + +* `+global.ini+` + +Each fact request can return one or more item, one for each found file; +multiple files can occur when the host has configured more than one SAP +system. Each item then has a `+sid+` field with the system id and a +`+value+` field with the actual content of the file. + +Example arguments: + +[width="100%",cols="<13%,<87%",options="header",] +|=== +|Name |Return value +|`+global.ini+` |Retrieved the content from +`+/usr/sap/>/SYS/global/hdb/custom/config/global.ini+` +|=== + +[source,yaml] +---- +facts: + - name: global_configuration + gatherer: ini_files@v1 + argument: global.ini +---- + +Example output (in Rhai): + +[source,ts] +---- +[ + #{ + "sid": "S01", + "value": #{ + "communication": #{ + "internal_network": "10.23.1.128/26", + "listeninterface": ".internal" + }, + "internal_hostname_resolution": #{ + "10.23.1.132": "hana-s1-db1", + "10.23.1.133": "hana-s1-db2", + "10.23.1.134": "hana-s1-db3" + } + } + } +] +---- + +[#mount_infov1]## + +==== mount_info@v1 + +*Argument required*: yes. + +This gatherer allows accessing the OS file system mount points. It +returns information about the mount point of a given path. Besides of +the mount information, if the mount is done in a local block device, it +returns the UUID of the block (coming from the `+blkid+` command). If +the given path is not mounted, all the fields are returned with empty +strings. + +Example specification: + +[source,yaml] +---- +facts: + - name: not_mounted + gatherer: mount_info@v1 + argument: /usr/sap + + - name: shared_nfs + gatherer: mount_info@v1 + argument: /sapmnt + + - name: mounted_locally + gatherer: mount_info@v1 + argument: /hana/data +---- + +Example output (in Rhai): + +[source,ts] +---- +// not_mounted +#{ + "block_uuid": "", + "fs_type": "", + "mount_point": "", + "options": "", + "source": "" +} + +// shared_nfs +#{ + "block_uuid": "", + "fs_type": "nfs4", + "mount_point": "/sapmnt", + "options": "rw,relatime", + "source": "10.1.1.10://sapmnt" +} + +// mounted_locally +#{ + "block_uuid": "f45cf408-efgh-abcd-88ec-2f9269a12f07", + "fs_type": "xfs", + "mount_point": "/hana/data", + "options": "rw,relatime", + "source": "/dev/mapper/vg_hana-lv_hana_data" +} +---- + +[#os-releasev1]## + +==== os-release@v1 + +*Argument required*: no. + +This gatherer allows access to the distribution details in +`+/etc/os-release+`. This file contains operating system identification +data, such as the vendor of the distribution, the name of the +distribution, the version and the ID of the distribution, as well as +many other details. + +Example specification: + +[source,yaml] +---- +facts: + - name: os_release + gatherer: os-release@v1 +---- + +Example output (in Rhai): + +[source,ts] +---- +// output from openSUSE Leap 15.2 +#{ + "ANSI_COLOR": "0;32", + "BUG_REPORT_URL": "https://bugs.opensuse.org", + "CPE_NAME": "cpe:/o:opensuse:leap:15.2", + "HOME_URL": "https://www.opensuse.org/", + "ID": "opensuse-leap", + "ID_LIKE": "suse opensuse", + "NAME": "openSUSE Leap", + "PRETTY_NAME": "openSUSE Leap 15.2", + "VERSION": "15.2", + "VERSION_ID": "15.2" +} +---- + +[#package_versionv1]## + +==== package_version@v1 + +*Argument required*: yes. + +This gatherer supports two usecases: + +* get information about the installed versions of the specified package. +* compare a given version string against the latest installed version of +a given package. + +In the first usecase a list of objects is returned, where each object +carries relevant information about an installed version of a package. + +____ +Note: + +* a list of one element is often expected since usually the installed +version would be only one +* detected installed versions list is ordered by descending installation +time: *latest installed versions come first* +* operating on the latest installed version requires accessing the first +element in the list via `+package_fact_name[0]+` or +`+package_fact_name.first()+` +____ + +In the second usecase, the return value is as follows (see additional +details +https://fedoraproject.org/wiki/Archive:Tools/RPM/VersionComparison#The_rpmvercmp_algorithm[here]): + +* A value of `+0+` if the provided version string matches the installed +package version for the requested package. +* A value of `+-1+` if the provided version string is older that what’s +currently installed. +* A value of `+1+` if the provided version string is newer than what’s +currently installed. + +____ +The latest detected installed version is used for comparison +____ + +Naming the facts / expectations accordingly is specially important here +to avoid confusion. + +* We suggest using a `+compare_+` prefix for package version comparisons +and `+package_+` to retrieve a package version + +Additionally, when using the version comparison, it increases +readability to explicitly mention the values to compare against: + +[source,yaml] +---- +facts: + - name: compare_package_corosync + gatherer: package_version@v1 + argument: corosync,2.4.5 + + - name: package_corosync + gatherer: package_version@v1 + argument: corosync + + - name: package_sbd + gatherer: package_version@v1 + argument: sbd + +values: + - name: greater_than_installed + default: 1 + - name: lesser_than_installed + default: -1 + - name: same_as_installed + default: 0 + - name: expected_corosync_version + default: "2.4.5" + +expectations: + - name: compare_package_corosync + expect: facts.compare_package_corosync == values.greater_than_installed + + - name: package_corosync_is_the_expected_one + expect: facts.package_corosync.first().version == values.expected_corosync_version + + - name: sbd_version_same_on_all_hosts + expect_same: facts.package_sbd.first().version +---- + +Example arguments: + +[width="100%",cols="<21%,<79%",options="header",] +|=== +|Name |Return value +|`+package_name+` |a list containing information about the installed +versions of the rpm package + +|`+package_name,2.4.5+` |an integer with a value of `+-1+`, `+0+` or +`+1+` (see above) +|=== + +Example specification: + +[source,yaml] +---- +facts: + - name: package_corosync + gatherer: package_version@v1 + argument: corosync + + - name: package_pacemaker + gatherer: package_version@v1 + argument: pacemaker + + - name: multiple_sbd_versions_installed + gatherer: package_version@v1 + argument: sbd + + - name: compare_package_corosync + gatherer: package_version@v1 + argument: corosync,2.4.5 + + ... +---- + +Example output (in Rhai): + +[source,ts] +---- +// package_corosync +[ + #{ + "version": "2.4.5" + } +] + +// package_pacemaker +[ + #{ + "version": "2.0.4+20200616.2deceaa3a" + } +] + +// multiple_sbd_versions_installed +[ + #{ + "version": "1.5.1" // latest installed version, not necessarily the newest one + }, + #{ + "version": "1.5.2" + } +] + +// compare_package_corosync +0 +---- + +[#passwdv1]## + +==== passwd@v1 + +*Argument required*: no. + +This gatherer allows access to the /etc/passwd file, returning all +entries available at the file. + +Example specification: + +[source,yaml] +---- +facts: + - name: passwd + gatherer: passwd@v1 +---- + +Example output (in Rhai): + +[source,ts] +---- +[ + #{ + "description": "bin", + "gid": 1, + "home": "/bin", + "shell": "/sbin/nologin", + "uid": 1, + "user": "bin" + }, + #{ + "description": "Chrony Daemon", + "gid": 475, + "home": "/var/lib/chrony", + "shell": "/bin/false", + "uid": 474, + "user": "chrony" + }, + #{ + "description": "Daemon", + "gid": 2, + "home": "/sbin", + "shell": "/sbin/nologin", + "uid": 2, + "user": "daemon" + }, + ... +]; +---- + +[#productsv1]## + +==== products@v1 + +*Argument required*: no. + +This gatherer allows access to the /etc/products.d/ folder files +content. It returns the file contents mapped using the file name. The +XML content is returned as-is, just converted to a rhai object. + +Example specification: + +[source,yaml] +---- +facts: + - name: products + gatherer: products@v1 +---- + +Example output (in Rhai): + +[source,ts] +---- +#{ + "Leap.prod": #{ + "product": #{ + "arch": "x86_64", + ... + "codestream": #{ + "endoflife": "2024-11-30", + "name": "openSUSE Leap 15" + }, + ... + "name": "Leap", + "productline": "Leap", + ... + "vendor": "openSUSE", + "version": "15.3" + } + }, + "baseproduct": #{ + "product": #{ + "arch": "x86_64", + ... + "codestream": #{ + "endoflife": "2024-11-30", + "name": "openSUSE Leap 15" + }, + ... + "name": "Leap", + "productline": "Leap", + ... + "vendor": "openSUSE", + "version": "15.3" + } + } +} +---- + +[#sap_profilesv1]## + +==== sap_profiles@v1 + +*Argument required*: no. + +This gatherer allows access to the latest SAP profile files content +stored in `+/sapmnt//profile+`. The "`latest`" profile means that +backed up files like `+DEFAULT.1.PFL+` or `+some_profile.1+` are +excluded. It returns the profile files and content grouped by SID in a +keyway. + +Example specification: + +[source,yaml] +---- +facts: + - name: sap_profiles + gatherer: sap_profiles@v1 +---- + +Example output (in Rhai): + +[source,ts] +---- +#{ + "NWP": #{ + "profiles": [ + #{ + "content": #{ + "SAPDBHOST": "10.80.1.13", + "SAPGLOBALHOST": "sapnwpas", + "SAPSYSTEMNAME": "NWP", + ... + }, + "name": "DEFAULT.PFL", + "path": "/sapmnt/NWP/profile/DEFAULT.PFL" + }, + #{ + "content": #{ + "DIR_CT_RUN": "$(DIR_EXE_ROOT)$(DIR_SEP)$(OS_UNICODE)$(DIR_SEP)linuxx86_64", + "DIR_EXECUTABLE": "$(DIR_INSTANCE)/exe", + "DIR_PROFILE": "$(DIR_INSTALL)$(DIR_SEP)profile", + ... + }, + "name": "NWP_ASCS00_sapnwpas", + "path": "/sapmnt/NWP/profile/NWP_ASCS00_sapnwpas" + }, + ... + ] + }, + "NWD": #{ + "profiles": [ + #{ + "content": #{ + "SAPDBHOST": "10.85.1.13", + "SAPGLOBALHOST": "sapnwdas", + "SAPSYSTEMNAME": "NWD", + ... + }, + "name": "DEFAULT.PFL", + "path": "/sapmnt/NWD/profile/DEFAULT.PFL" + }, + ... + ] + } +} +---- + +[#sapservicesv1]## + +==== sapservices@v1 + +*Argument required*: no. + +This gatherer allows access to the SAP services file content stored in +`+/usr/sap/sapservices+`. Each entry in the file is returned as a map, +containing the SID, the instance number, the raw line content of the +entry and the kind of system used for startup, `+systemctl+` or +`+sapstartsrv+`. + +Example specification: + +[source,yaml] +---- +facts: + - name: sapservices + gatherer: sapservices@v1 +---- + +Example output (in Rhai): + +[source,ts] +---- +[ + #{ + "sid": "HS1", + "kind": "sapstartsrv", + "content": "LD_LIBRARY_PATH=/usr/sap/HS1/HDB11/exe:$LD_LIBRARY_PATH;export LD_LIBRARY_PATH;/usr/sap/HS1/HDB11/exe/sapstartsrv pf=/usr/sap/HS1/SYS/profile/HS1_HDB11_s41db -D -u hs1adm", + "instance_nr": "11" + }, + #{ + "sid": "S41", + "kind": "systemctl", + "content": "systemctl --no-ask-password start SAPS41_40", + "instance_nr": "40" + }, +] +---- + +[#sapcontrolv1]## + +==== sapcontrol@v1 + +*Argument required*: yes. + +This gatherer allows access to certain webmethods that `+sapcontrol+` +implements. An argument is required to specify which webmethod should be +called. The communication with `+sapcontrol+` is created opening a unix +socket connection using the file `+/tmp/.sapstream5xx13+`. The +https://www.sap.com/documents/2016/09/0a40e60d-8b7c-0010-82c7-eda71af511fa.html[Sapcontrol +Web Service Interface] documents the SOAP API interface, including all +the possible values each of the fields could have, specifically helpful +for enumerators like `+dispstatus+` in `+GetProcessList+` and +`+state/category+` in `+HACheckConfig+` webmethod. + +The return value is grouped by discovered SIDs, which include the list +of command outputs for each instance in this system. + +Supported webmethods: + +* `+GetProcessList+` +* `+GetSystemInstanceList+` +* `+GetVersionInfo+` +* `+HACheckConfig+` +* `+HAGetFailoverConfig+` + +Example specification: + +[source,yaml] +---- +facts: + - name: processes + gatherer: sapcontrol@v1 + argument: GetProcessList + + - name: instances + gatherer: sapcontrol@v1 + argument: GetSystemInstanceList +---- + +Example output (in Rhai): + +[source,ts] +---- +// GetProcessList +#{ + "NWP": [ + #{ + "instance_nr": "10", + "name": "ERS10", + "output": [ + #{ + "description": "EnqueueReplicator", + "dispstatus": "SAPControl-GREEN", + "elapsedtime": "266:08:15", + "name": "enrepserver", + "pid": 7221, + "starttime": "2023 09 29 09:41:41", + "textstatus": "Running" + } + ] + } + ] +} + +// GetSystemInstanceList +#{ + "NWP": [ + #{ + "instance_nr": "10", + "name": "ERS10", + "output": [ + #{ + "dispstatus": "SAPControl-GREEN", + "features": "MESSAGESERVER|ENQUE", + "hostname": "sapnwpas", + "http_port": 50013, + "https_port": 50014, + "instance_nr": 0, + "start_priority": "1" + }, + #{ + "dispstatus": "SAPControl-GREEN", + "features": "ENQREP", + "hostname": "sapnwper", + "http_port": 51013, + "https_port": 51014, + "instance_nr": 10, + "start_priority": "0.5" + }, + ... + ] + } + ] +} + +// GetVersionInfo +#{ + "NWP": [ + #{ + "instance_nr": "10", + "name": "ERS10", + "output": [ + #{ + "architecture": "linuxx86_64", + "build": "optU (Oct 16 2021, 00:03:15)", + "changelist": "2094654", + "filename": "/usr/sap/NWP/ERS10/exe/sapstartsrv", + "patch": "900", + "rks_compatibility_level": "1", + "sap_kernel": "753", + "time": "2021 10 15 22:14:31" + }, + #{ + "architecture": "linuxx86_64", + "build": "optU (Oct 16 2021, 00:03:15)", + "changelist": "2094654", + "filename": "/usr/sap/NWP/ERS10/exe/gwrd", + "patch": "900", + "rks_compatibility_level": "1", + "sap_kernel": "753", + "time": "2021 10 15 22:04:14" + }, + ... + ] + } + ] +} + +// HACheckConfig +#{ + "NWP": [ + #{ + "instance_nr": "10", + "name": "ERS10", + "output": [ + #{ + "category": "SAPControl-SAP-CONFIGURATION", + "comment": "2 ABAP instances detected", + "description": "Redundant ABAP instance configuration", + "state": "SAPControl-HA-SUCCESS" + }, + #{ + "category": "SAPControl-SAP-CONFIGURATION", + "comment": "0 Java instances detected", + "description": "Redundant Java instance configuration", + "state": "SAPControl-HA-SUCCESS" + }, + ... + ] + } + ] +} + +//HAGetFailoverConfig +#{ + "NWP": [ + #{ + "instance_nr": "10", + "name": "ERS10", + "output": #{ + "ha_active": false, + "ha_active_nodes": "", + "ha_documentation": "", + "ha_nodes": [], + "ha_product_version": "", + "ha_sap_interface_version": "" + } + } + ] +} +---- + +[#saphostctrlv1]## + +==== saphostctrl@v1 + +*Argument required*: yes. + +This gatherer allows access to certain webmethods that `+saphostctrl+` +implements. An argument is required to specify which webmethod should be +called. This webmethod is passed to the `+saphostctrl+` command-line +tool through the `+-function+` argument. + +Supported webmethods: + +* `+Ping+` +* `+ListInstances+` + +A `+Ping+` call with a successful return should look like this: + +Example specification: + +[source,yaml] +---- +facts: + - name: ping + gatherer: saphostctrl@v1 + argument: Ping + + - name: list_instances + gatherer: saphostctrl@v1 + argument: ListInstances +---- + +Example output (in Rhai): + +[source,ts] +---- +// ping +#{elapsed: 579770.0, status: "SUCCESS"} + +// list_instances +[ + #{ + "changelist": 1908545, + "hostname": "vmhana01", + "instance": "00", + "patch": 410, + "sapkernel": 753, + "system": "PRD" + } +]; +---- + +[#sapinstance_hostname_resolverv1]## + +==== sapinstance_hostname_resolver@v1 + +*Argument required*: no. + +This gatherer uses the filesystem to search for SAP systems using the +discovered profile file names to get the virtual hostnames associated to +each instance of the sap system. It then attempts to resolve those +hostnames to confirm that they are resolvable and afterwards a ping is +attempted to those hostnames. Keep in mind that ping could be disallowed +through firewall rules so it should only be used for networks in which +we know this is not true. + +Example specification: + +[source,yaml] +---- +facts: + - name: resolvability_check + gatherer: sapinstance_hostname_resolver@v1 +---- + +Example output (in Rhai): + +[source,ts] +---- +// 2 resolvable & 1 non-resolvable hosts +#{ + "NWP": [ + #{ + "addresses": [ + "2.1.1.82" + ], + "hostname": "sapnwpas", + "instance_name": "ASCS00", + "reachability": true + } + ], + "QAS": [ + #{ + "addresses": [ + "1.1.1.82" + ], + "hostname": "sapqasas", + "instance_name": "ASCS00", + "reachability": true + }, + #{ + "addresses": (), + "hostname": "sapwaser", + "instance_name": "ERS00", + "reachability": false + } + ] +} +---- + +[#saptunev1]## + +==== saptune@v1 + +*Argument required*: yes. + +This gatherer allows access to certain commands that `+saptune+` +implements. An argument is required to specify which argument should be +used when calling `+saptune+`. + +____ +Note: the gatherer will return the same JSON objects returned by +saptune. The only transformation it applies is the snake casing of the +keys. +____ + +Supported arguments: + +* `+status+` (maps to +`+saptune --format json status --non-compliance-check+`) +* `+solution-verify+` (maps to +`+saptune --format json solution verify+`) +* `+solution-list+` (maps to `+saptune --format json solution list+`) +* `+note-verify+` (maps to `+saptune --format json note verify+`) +* `+note-list+` (maps to `+saptune --format json note list+`) + +A `+status+` call with a successful return should look like this: + +Example specification: + +[source,yaml] +---- +facts: + - name: status + gatherer: saptune@v1 + argument: status +---- + +Example output (in Rhai): + +[source,ts] +---- +// status +#{ + "$schema": "file:///usr/share/saptune/schemas/1.0/saptune_status.schema.json", + "argv": "saptune --format json status", + "command": "status", + "exit_code": 1, + "messages": [ + #{ + "message": "actions.go:85: ATTENTION: You are running a test version", + "priority": "NOTICE" + } + ], + "pid": 6593, + "publish_time": "2023-09-15 15:15:14.599", + "result": #{ + "configured_version": "3", + "notes_applied": [ + "1410736" + ], + "notes_applied_by_solution": [], + "notes_enabled": [ + "1410736" + ], + "notes_enabled_additionally": [ + "1410736" + ], + "notes_enabled_by_solution": [], + "package_version": "3.1.0", + "remember_message": "This is a reminder", + "services": #{ + "sapconf": [], + "saptune": [ + "disabled", + "inactive" + ], + "tuned": [] + }, + "solution_applied": [], + "solution_enabled": [], + "staging": #{ + "notes_staged": [], + "solutions_staged": [], + "staging_enabled": false + }, + "systemd_system_state": "degraded", + "tuning_state": "compliant", + "virtualization": "kvm" + } +} +---- + +[#sbd_configv1]## + +==== sbd_config@v1 + +*Argument required*: yes. + +This gatherer allows accessing the information contained in +`+/etc/sysconfig/sbd+` + +Example arguments: + +[cols="<,<",options="header",] +|=== +|Name |Return value +|`+SBD_PACEMAKER+` |extracted value from the config +|`+SBD_STARTMODE+` |extracted value from the config +|`+SBD_DEVICE+` |extracted value from the config +|=== + +Example specification: + +[source,yaml] +---- +facts: + - name: sbd_pacemaker + gatherer: sbd_config@v1 + argument: SBD_PACEMAKER + + - name: sbd_startmode + gatherer: sbd_config@v1 + argument: SBD_STARTMODE + + - name: sbd_device + gatherer: sbd_config@v1 + argument: SBD_DEVICE +---- + +Example output (in Rhai): + +[source,ts] +---- +// sbd_pacemaker +"yes"; + +// sbd_startmode +"always"; + +// sbd_device +"/dev/vdc;/dev/vdb"; +---- + +[#sbd_dumpv1]## + +==== sbd_dump@v1 + +*Argument required*: no. + +This gatherer allows accessing the sbd dump command output data. + +It executes the `+sbd -d dump+` command in all devices +configured in the `+SBD_DEVICE+` field on `+/etc/sysconfig/sbd+` and +aggregates results in only one fact. + +Note that: + +* no arguments are required +* if any of the dumps fail, a fact error is returned + +Dumped keys (`+Timeout (watchdog)+`, `+Timeout (msgwait)+`, +`+Number of slots+`, etc) are sanitized to simplify their access and +usage in the expression language. + +Example specification: + +[source,yaml] +---- +facts: + - name: sbd_devices_dump + gatherer: sbd_dump@v1 +---- + +Example output (in Rhai): + +[source,ts] +---- +// sbd_devices_dump +#{ + "/dev/vdc": #{ + header_version: 2.1, + number_of_slots: 255, + sector_size: 512, + timeout_allocate: 2, + timeout_loop: 1, + timeout_msgwait: 10, + timeout_watchdog: 5, + uuid: "69048391-c647-4b34-a03a-f704f5cc2258" + } +}; +---- + +For extra information refer to +https://github.com/trento-project/agent/blob/main/internal/factsengine/gatherers/sbddump_test.go[trento-project/agent/../gatherers/sbddump_test.go] + +[#sudoersv1]## + +==== sudoers@v1 + +*Argument required*: no. + +This gatherer fetches the sudoer information about a user. The output is +a list of objects representing the sudoer rules with the following +fields: + +* `+user+`: The name of the user to whom the rule applies; +* `+command+`: The command a sudoer rule has been specified for; +* `+run_as_user+`: The user privileges under which the command will be +executed; +* `+run_as_group+`: The group privileges under which the command will be +executed.; +* `+no_password+`: Whether the `+NOPASSWD+` tag is set for the rule. + +The gatherer operates in two modes: + +* _user discovery mode_: no argument is specified, thus the gatherer +fetches results for all the configured users for the SAP systems on the +host; +* _explicit user mode_: the target user is specified as the gatherer +argument, regardless if it’s a SAP-configured user. + +Example arguments: + +[width="100%",cols="<9%,<91%",options="header",] +|=== +|Name |Return value +|_empty_ |All sudoer rules for all users configured for the installed +SAP systems on the host + +|`+prdadm+` |All sudoer rules for the `+prdadm+` user +|=== + +Example output (in Rhai): + +[source,ts] +---- +[ + #{ + "command": "ALL", + "no_password": false, + "run_as_group": "", + "run_as_user": "ALL", + "user": "prdadm" + }, + #{ + "command": "/usr/sbin/crm_attribute -n hana_prd_site_srHook_Site1 -v SOK -t crm_config -s SAPHanaSR", + "no_password": true, + "run_as_group": "", + "run_as_user": "ALL", + "user": "prdadm" + }, + #{ + "command": "/usr/sbin/crm_attribute -n hana_prd_site_srHook_Site1 -v SFAIL -t crm_config -s SAPHanaSR", + "no_password": true, + "run_as_group": "", + "run_as_user": "ALL", + "user": "prdadm" + }, + #{ + "command": "/usr/sbin/crm_attribute -n hana_prd_site_srHook_Site2 -v SOK -t crm_config -s SAPHanaSR", + "no_password": true, + "run_as_group": "", + "run_as_user": "ALL", + "user": "prdadm" + }, + #{ + "command": "/usr/sbin/crm_attribute -n hana_prd_site_srHook_Site2 -v SFAIL -t crm_config -s SAPHanaSR", + "no_password": true, + "run_as_group": "", + "run_as_user": "ALL", + "user": "prdadm" + }, + #{ + "command": "/usr/sbin/SAPHanaSR-hookHelper --case checkTakeover --sid\\=prd", + "no_password": true, + "run_as_group": "", + "run_as_user": "ALL", + "user": "prdadm" + } +] +---- + +[#sysctlv1]## + +==== sysctl@v1 + +*Argument required*: yes. + +Gather sysctl output. It takes a sysctl key as argument and it returns +the value of the requested key or a map if a partial key is provided. + +Example arguments: + +[width="100%",cols="<23%,<77%",options="header",] +|=== +|Name |Return value +|`+vm.swappiness+` |corresponding value returned by sysctl +|`+debug+` |a map containing all the keys starting with `debug.`` +|=== + +[source,yaml] +---- +facts: + - name: vm_swappiness + gatherer: sysctl@v1 + argument: vm.swappiness + + - name: debug + gatherer: sysctl + argument: debug +---- + +Example output (in Rhai): + +[source,ts] +---- +// vm_swapiness +60; + +// debug +#{ + "exception-trace": 1, + "kprobes-optimization": 1 +}; +---- + +[#systemdv1]## + +==== systemd@v1 + +*Argument required*: yes. + +Gather systemd units state. It returns an `+active/inactive+` string. If +the service is disabled or it does not exist, `+inactive+` is returned. + +Example arguments: + +[cols="<,<",options="header",] +|=== +|Name |Return value +|`+trento-agent+` |state of the systemd unit +|=== + +[source,yaml] +---- +facts: + - name: sbd_state + gatherer: systemd@v1 + argument: sbd + + - name: corosync_state + gatherer: systemd@v1 + argument: corosync +---- + +Example output (in Rhai): + +[source,ts] +---- +// sbd_state +"active"; + +// corosync_state +"inactive"; +---- + +[#systemdv2]## + +==== systemd@v2 + +*Argument required*: yes. + +Gather systemd units information. It returns an object with multiple +fields about the systemd unit. + +The provided unit name must include the extension, such as `+.service+` +or `+.mount+`. + +Only a subset of properties are returned. Additional information about +these is available in the +https://www.man7.org/linux/man-pages/man5/org.freedesktop.systemd1.5.html#UNIT_OBJECTS[systemd] +man pages, with some detailed description in the `+Properties+` +sub-chapter. + +Example arguments: + +[cols="<,<",options="header",] +|=== +|Name |Return value +|`+trento-agent.service+` |systemd unit information +|=== + +[source,yaml] +---- +facts: + - name: corosync + gatherer: systemd@v2 + argument: corosync.service + + - name: not_found + gatherer: systemd@v2 + argument: unknown.service +---- + +Example output (in Rhai): + +[source,ts] +---- +// corosync +#{ + "active_state": "inactive", + "description": "Corosync Cluster Engine", + "id": "corosync.service", + "load_state": "loaded", + "need_daemon_reload": false, + "unit_file_preset": "disabled", + "unit_file_state": "disabled" +} + +// not_found +#{ + "active_state": "inactive", + "description": "unknown.service", + "id": "unknown.service", + "load_state": "not-found", + "need_daemon_reload": false, + "unit_file_preset": "", + "unit_file_state": "" +} +---- + +[#verify_passwordv1]## + +==== verify_password@v1 + +*Argument required*: yes. + +This gatherer determines whether a given user has its password still set +to an unsafe password. It returns `+true+` if the password matches a +password in the list of unsafe passwords, `+false+` otherwise. + +Specification examples: + +[source,yaml] +---- +facts: + - name: hacluster_has_default_password + gatherer: verify_password@v1 + argument: hacluster +---- + +For the argument, only whitelisted users are allowed. Currently +whitelisted usernames: + +* `+hacluster+` + +List of unsafe passwords: + +* `+linux+` + +Example output (in Rhai): + +[source,ts] +---- +// hacluster_has_default_password +true; +---- diff --git a/components/wanda/rhai_expressions_cheat_sheet.adoc b/components/wanda/rhai_expressions_cheat_sheet.adoc new file mode 100644 index 00000000..c7342597 --- /dev/null +++ b/components/wanda/rhai_expressions_cheat_sheet.adoc @@ -0,0 +1,91 @@ +== Rhai expressions cheatsheet + +In this cheatsheet are grouped the most frequent manipulations that can +be done through Rhai, for convenience. + +=== Cheatsheet + +\{: .col-2} + +==== Arrays + +===== Filtering an array + +[source,ts] +---- +[1, 2, 3].filter(|value| value % 2 == 0) +=> [2] +---- + +===== Finding the index of an element inside an array + +[source,ts] +---- +["wow", "check"].index_of(|value| value == "check") +=> 1 +---- + +===== Finding an element inside an array + +[source,ts] +---- +let nodelist = [ + #{resource_id: 5, name: "foo"}, + #{resource_id: 12, name: "bar"} +]; + +nodelist.find(|value| value.resource_id == 5) +=> #{"name": "foo", "resource_id": 5} +---- + +===== Checking if an expression is true for every element of an array + +[source,ts] +---- +[2, 4, 6].all(|value| value % 2 == 0) +=> true +---- + +==== Maps + +===== Get only keys (returns an array) + +[source,ts] +---- +#{a: 1, b: 2}.keys() +=> ["a", "b"] +---- + +===== Get only values (returns an array) + +[source,ts] +---- +#{a: 1, b: 2}.values() +=> [1, 2] +---- + +===== Check if a map has more than 5 keys which name starts with "`ring`" + +[source,ts] +---- +let map = #{ + ring_one: 12, + ring_two: 34, + ring_three: 90, + ring_four: 234, + ring_five: 908 +}; + +map.keys().filter(|prop| prop.starts_with("ring")).len() >= 5 +=> true +---- + +==== Strings + +===== Splitting a string + +[source,ts] +---- +"a;b;c".split(";") +=> ["a", "b", "c"] +---- diff --git a/components/wanda/specification.adoc b/components/wanda/specification.adoc new file mode 100644 index 00000000..2cbdcad3 --- /dev/null +++ b/components/wanda/specification.adoc @@ -0,0 +1,1154 @@ +== Checks Specification + +A language allowing to declare best practices to be adhered on target +SAP Infrastructures. + +=== Introduction + +The need this Specification aims to fulfill is to provide users a simple +way to declare what we (the Trento Team) often refer to as "`Checks`". + +Checks are, in Trento’s domain, the crystallization of SUSE’s best +practices when it comes to SAP workloads in a form that both a user +(link:#anatomy-of-a-check[Spec]) and a machine +(link:#checks-execution[Execution]) can read. + +=== Checks Execution + +_Checks Execution_ is the process that determines whether the best +practices defined in the link:#anatomy-of-a-check[Checks Specifications] +are being followed on a target infrastructure. + +____ +link:#requesting-an-execution[Requesting an Execution] -> +link:#facts-gathering[Facts Gathering] -> +link:#expectation-evaluation[Expectation Evaluation] +____ + +==== Requesting an Execution + +An Execution can be requested to start by providing Wanda the following +information: + +* an execution identifier +* an execution Group identifier +* the Checks Selection for the targets (a list of checks to be executed +on the targets) + +When the Execution starts running, its current state is stored in the +Database and the targets are notified - via the message broker - about +Facts to be gathered. + +Then the _Execution_ waits for the link:#facts-gathering[Facts +Gathering] to complete. + +==== Facts Gathering + +After an _Execution Request_ the targets are notified about the facts +they need to link:./gatherers.md[gather]. + +Whenever a target has gathered all the needed facts for an _Execution_, +it notifies Wanda - via the message broker - about the _Gathered Facts_. + +==== Expectation Evaluation + +_Expectation Evaluation_ is the process of +link:#expression-language[evaluating] the +link:#expectations[Expectations] using the received _Gathered Facts_ to +obtain the result of a check. + +This will only happen once _Gathered Facts_ are received *from all the +targets*. + +After the result has been determined, the currently `+running+` +Execution transitions to `+completed+` and its new state is tracked on +the Database. + +At this point the Execution is considered *Completed* and interested +parties are notified about the Execution Completion. + +==== Checks Results + +Once an execution is completed, a checks result should give feedback on +what aspects of a target infrastructure adhere to the best practices and +which don’t. + +Possible results: + +* `+passing+`, everything ok +* `+warning+`, best practice not followed, should fix +* `+critical+`, best practice not followed, must fix + +See also link:#severity[Check Severity]. + +=== Anatomy of a Check + +A Check declaration comes in the form of a `+yaml+` file and all the +Checks together build up the *Checks Catalog* + +Here’s an example: + +[source,yaml] +---- +id: "156F64" +name: Corosync `token` timeout +group: Corosync +description: Corosync `token` timeout is set to the correct value +remediation: | + ## Abstract + The value of the Corosync `token` timeout is not set as recommended. + ## Remediation + Adjust the corosync `token` timeout as recommended... + +severity: warning + +metadata: + target_type: cluster + cluster_type: hana_scale_up + hana_scenario: performance_optimized + provider: [azure, nutanix, kvm, vmware] + +facts: + - name: corosync_token_timeout + gatherer: corosync.conf + argument: totem.token + +customization_disabled: true + +values: + - name: expected_token_timeout + customization_disabled: true + default: 5000 + conditions: + - value: 30000 + when: env.provider == "azure" || env.provider == "aws" + - value: 20000 + when: env.provider == "gcp" + +expectations: + - name: token_timeout + expect: facts.corosync_token_timeout == values.expected_token_timeout +---- + +==== Filename Convention + +*Note* that a Check’s filename *MUST* be in the form `+.yaml+` +(ie: `+156F64.yaml+`) + +==== Structure + +Following are listed the top level properties of a Check definition +yaml. + +[width="100%",cols="31%,26%,43%",options="header",] +|=== +|Key |Required/Not Required |Details +|`+id+` |required |link:#id[see more] + +|`+name+` |required |link:#name[see more] + +|`+group+` |required |link:#group[see more] + +|`+description+` |required |link:#description[see more] + +|`+remediation+` |required |link:#remediation[see more] + +|`+severity+` |not required |link:#severity[see more] + +|`+metadata+` |not required |link:#metadata[see more] + +|`+facts+` |required |link:#facts[see more] + +|`+customization_disabled+` |not required +|link:#disable-customization[see more] + +|`+values+` |not required |link:#values[see more] + +|`+expectations+` |required |link:#expectations[see more] +|=== + +''''' + +===== id + +Uniquely identifies a Check in the Catalog. The value must be a +hexadecimal number formatted as string using quotes. + +ie: + +[source,yaml] +---- +id: "156F64" +id: "845CC9" +id: "B089BE" +---- + +===== name + +A, preferably one-line, string representing the name for the Check being +declared. + +ie: + +[source,yaml] +---- +name: Corosync `token` timeout +name: Corosync `consensus` timeout +name: SBD Startmode +---- + +===== group + +A, preferably one-line, string representing the group where the Check +being declared belongs. + +Example: + +[source,yaml] +---- +group: Corosync +group: Pacemaker +group: SBD +---- + +===== description + +A text providing a description for the Check being declared. + +can be a one-liner + +[source,yaml] +---- +description: Some plain description +---- + +can be a multiline text + +[source,yaml] +---- +description: | + Some plain multiline + description that carries a lot + of information +---- + +format is *markdown* + +[source,yaml] +---- +description: | + A `description` is a **markdown** +---- + +===== remediation + +A text providing an comprehensive description about the remediation to +apply for the Check being declared. + +It has the same properties of the `+description+` + +* can be a one-liner (it usually is not) +* can be a multiline (it usually is) +* format is *markdown* + +Example: + +[source,yaml] +---- +remediation: | + ## Abstract + The value of the Corosync `token` timeout is not set as recommended. + ## Remediation + Adjust the corosync `token` timeout as recommended on the best + ... + 2. Reload the corosync configuration: + ... +---- + +===== severity + +A string determining the severity of the Check being declared, in case +the check is not passing, so that the appropriate result is reported. + +Allowed values: `+warning+`, `+critical+` + +*Default:* if no severity is provided, the system would default to +`+critical+` + +Example: + +Reports a `+warning+` When the Check expectations do not pass + +[source,yaml] +---- +severity: warning +---- + +Reports a `+critical+` When the Check expectations do not pass + +[source,yaml] +---- +severity: critical +---- + +===== metadata + +A key-value map that enriches the Check being declared by providing +extra information about when to consider it as applicable given a +specific link:#env[env] + +* keys must be non empty strings (`+foo+`, `+bar+`, `+foo_bar+`, +`+qux1+`) +* values can be any of the following types `+string+`, `+number+`, +`+boolean+`, `+string[]+` (list of strings) +* `+target_type+` is a *required* key of the `+metadata+` map. It’s +value is a `+string+`. + +Example: + +[source,yaml] +---- +metadata: + target_type: example_target + foo: bar + bar: 42 + baz: true + qux: [foo, bar, baz] +---- + +Metadata is used when: - querying checks from the catalog - loading +relevant checks for an execution (when requesting an execution to start +either via the rest API or via a message through the message broker) + +===== How does the matching work? + +For each of the metadata key-value the system checks whether a matching +key is present in the current context (catalog or execution env) and if +so, whether the value matches the one declared in the check. + +For a check to be considered applicable all the metadata key-value pairs +should match something in the env. + +Any extra key in the env not having a corresponding one in the check +metadata is ignored. + +Notes: - a string in the env (ie `+env.qux+` being `+baz+`) can match +either a plain string as in `+qux: baz+` and a string contained in a +list as in `+qux: [foo, bar, baz]+` - an empty env always matches any +metadata - an empty metadata always matches any env + +*Matching example* + +[source,ts] +---- +let env = #{ + foo: "bar", + qux: "baz" +} +---- + +[source,yaml] +---- +metadata: + foo: bar + bar: 42 + baz: true + qux: baz +---- + +*Not matching example* + +[source,ts] +---- +let env = #{ + foo: "bar", + qux: "baz", + baz: false +} +---- + +[source,yaml] +---- +metadata: + foo: bar + bar: 42 + baz: true + qux: [foo, bar, baz] +---- + +===== Facts, Values, Expectations + +See main sections link:#facts[Facts], link:#values[Values], +link:#expectations[Expectations] + +=== Facts + +Facts are the core data on which the engine evaluates the state of the +target infrastructure. Examples include (but are not limited to) +installed packages, cluster state, and configuration files content. + +The process of determining the value of a declared fact during Check +execution is referred to as _Facts Gathering_ and it is the +responsibility of the link:./gatherers.md[_Gatherers_]. Gatherers could +be seen as functions that have a name and accept argument(s). + +That said, a fact declaration contains: + +* the fact name +* the gatherer used to retrieve the fact +* the argument(s) to be provided to the gatherer + +*Note:* + +* many facts can be declared +* all the declared facts would be registered in the +link:#facts-1[`+facts+`] namespaced evaluation scope. + +[source,yaml] +---- +facts: + - name: + gatherer: + argument: + + - name: + gatherer: + argument: +---- + +The following example declares a *fact* named +`+corosync_token_timeout+`, retrievable via the built-in +`+corosync.conf+` *gatherer* to which will be provided the *argument* +`+totem.token+` + +[source,yaml] +---- +facts: + - name: corosync_token_timeout + gatherer: corosync.conf + argument: totem.token + + # other facts maybe +---- + +Finally, gathered facts, are used in Check’s +link:#expectations[Expectations] to determine whether expected +conditions are met for the best practice to be adhered. + +=== Disable Customization + +Users can modify a check’s link:#values[expected values] to accommodate +specific system and environmental configurations. + +By default, built-in checks are *customizable*. The +`+customization_disabled+` flag provides a way to *disable* +customizability when needed. + +To disable customization for a check the following bit of specification +is required: + +[source,yaml] +---- +customization_disabled: true +---- + +Opting out from customizability at the root of a check’s specification +makes all the values of the given check not customizable. + +==== Notes: + +* Setting `+customization_disabled: false+` has no real effect as by +default a check is customizable +* The `+customization_disabled+` flag can be also applied to +link:#customizable-values[specific values] + +=== Values + +Values are named variables that may evaluate differently based on the +execution context and are used with Facts for _Contextual_ +link:#expectations[Expectations] Evaluation. + +____ +When contextual expectations is not needed, there’s the following +options available: + +* use link:#hardcoded-values[*hardcoded*] values +* define `+values+` as link:#constant-values[*constants*] + +Scenario: + +No matter what the context is, the fact `+awesome_fact+` MUST always be +`+wanda+` +____ + +==== Hardcoded Values + +Direct usage of a simple hardcoded value + +[source,yaml] +---- +expectations: + - name: awesome_expectation + expect: facts.awesome_fact == "wanda" +---- + +==== Constant Values + +Define a Value with only the `+default+` specified (*omitting* +`+conditions+`) for *constants* regardless of the context. + +[source,yaml] +---- +values: + - name: awesome_constant_value + default: "wanda" + +expectations: + - name: awesome_expectation + expect: facts.awesome_fact == values.awesome_constant_value +---- + +==== Contextual Values + +This is needed because the same check might expect facts to be treated +differently based on the context. + +____ +Let’s clarify with an example: + +A Check might define a fact named `+awesome_fact+` which is expected to +be different given the _color_ of the execution. + +* it has to be `+cat+` when the `+color+` in the execution context is +`+red+` +* it has to be `+dog+` when the `+color+` in the execution context is +`+blue+` +* it has to be `+rabbit+` in all other cases, regardless of the +execution context + +so we define a named variable `+awesome_expectation+` that resolves to +`+cat|dog|rabbit+` when proper conditions are met + +allowing us to have an expectation like this + +`+expect: facts.awesome_fact == values.awesome_expectation+` +____ + +A Value declaration contains: + +* the value name +* the default value +* a list of conditions that determine the value given the context +(optional, see link:#constant-values[constant values]) + +[source,yaml] +---- +values: + - name: + default: + conditions: + - value: + when: + - value: + when: +---- + +It could read as: + +the value named `++` resolves to + +* `++` when `++` is true +* `++` when `++` is true +* `++` in all other cases + +Example: + +____ +Check `+156F64 Corosync token timeout is set to expected value+` defines +a fact `+corosync_token_timeout+` which is expected to be different +given the platform (aws/azure/gcp), so we define a named variable +`+expected_token_timeout+` resolving to the appropriate value. + +`+expected_token_timeout+` resolves to: + +* `+30000+` when `+azure+`/`+aws+` are detected +* `+20000+` on `+gcp+` +* `+5000+` in all other cases (ie: bare metal, VMs…) +____ + +[source,yaml] +---- +values: + - name: expected_token_timeout + default: 5000 + conditions: + - value: 30000 + when: env.provider == "azure" || env.provider == "aws" + - value: 20000 + when: env.provider == "gcp" + +expectations: + - name: corosync_token_timeout_is_correct + expect: facts.corosync_token_timeout == values.expected_token_timeout +---- + +Note that `+conditions+` is a cascading chain of contextual inspection +to determine which is the resolved value. + +* there may be many conditions +* first condition that passes determines the value, following are not +evaluated +* `+when+` entry link:#expression-language[Expression] has +link:#evaluation-scope[access] to gathered link:#facts-1[facts] and +link:#env[env] evaluation scopes + +All the _resolved_ declared values would be registered in the +link:#values-1[`+values+`] namespaced evaluation scope. + +==== Customizable Values + +A check’s link:#values[expected values] are customizable by default, and +to provide finer control to the global-level +link:#disable-customization[customizability opt-out] it is possible to +opt-out customizability on a per-value basis. + +[source,yaml] +---- +values: + - name: non_customizable_check_value + customization_disabled: true + default: 5000 +---- + +Setting *customization_disabled*: `+false+` for a specific value +prevents the modification of the default value. + +=== Expectations + +Expectations are assertions on the state of a target infrastructure for +a given scenario. By using fact and values they are able to determine if +a check passes or not. + +An Expectation declaration contains: + +* the expectation name +* the expectation expression itself with link:#evaluation-scope[access] +to gathered link:#facts-1[facts] and link:#values-1[resolved values] +* an optional link:#failure_message[failure message] +* an optional link:#warning_message[warning message], only available in +link:#expect_enum[expect_enum] expectations + +[source,yaml] +---- +expectations: + - name: + expect: + + - name: + expect: + failure_message: + + - name: + expect_same: +---- + +Extra considerations: + +* there can be many expectations for a single Check +* an expectation can be one of three types: link:#expect[`+expect+`], +link:#expect_same[`+expect_same+`] or link:#expect_enum[`+expect_enum+`] +* a Check passes when all the expectations are satisfied + +Example + +[source,yaml] +---- +expectations: + - name: token_timeout + expect: facts.corosync_token_timeout == values.expected_token_timeout + + - name: awesome_expectation + expect: facts.awesome_fact == values.awesome_expected_value +---- + +In the previous example a Checks passes (is successful) if all +expectations are met, meaning that + +.... +facts.corosync_token_timeout == values.expected_token_timeout +AND +facts.awesome_fact == values.awesome_expected_value +.... + +==== expect + +This type of expectation is satisfied when, after facts gathering, the +expression is `+true+` for all the targets involved in the current +execution. + +____ +Execution Scenario: + +* 2 targets [`+A+`, `+B+`] +* selected Checks [`+corosync_check+`] +* some environment (context) + +[source,yaml] +---- +facts: + - name: corosync_token_timeout + gatherer: corosync.conf + argument: totem.token + +values: ... + +expectations: + - name: corosync_token_timeout_is_correct + expect: facts.corosync_token_timeout == values.expected_token_timeout +---- +____ + +Considering the previous scenario what happens is that: + +* the fact `+corosync_token_timeout+` is gathered on all targets (`+A+` +and `+B+` in this case) +* the expectation expression gets executed against the +`+corosync_token_timeout+` fact gathered on every targets. +** `+targetA.corosync_token_timeout == values.expected_token_timeout+` +** `+targetB.corosync_token_timeout == values.expected_token_timeout+` +* every evaluation has to be `+true+` + +==== expect_same + +This type of expectation is satisfied when, after facts gathering, the +expression’s return value is the same for all the targets involved in +the current execution, regardless of the value itself. + +____ +Execution Scenario: + +* 2 targets [`+A+`, `+B+`, `+C+`] +* selected Checks [`+some_check+`] +* some environment (context) + +[source,yaml] +---- +expectations: + - name: awesome_expectation + expect_same: facts.awesome_fact +---- +____ + +Considering the previous scenario what happens is that: + +* the fact `+awesome_fact+` is gathered on all targets (`+A+`, `+B+` and +`+C+` in this case) +* the expectation expression gets executed for every target involved. +** `+targetA.facts.awesome_fact+` +** `+targetB.facts.awesome_fact+` +** `+targetC.facts.awesome_fact+` +* the expressions results has to be the same for every target +** `+targetA.facts.awesome_fact == targetB.facts.awesome_fact == targetC.facts.awesome_fact+` + +____ +Example: + +RPM version must be the same on all the targets, regardless of what +version it is + +[source,yaml] +---- +facts: + - name: installed_rpm_version + gatherer: package_version + argument: rpm + +expectations: + - name: installed_rpm_version_must_be_the_same_on_all_targets + expect_same: facts.installed_rpm_version +---- +____ + +==== expect_enum + +This type of expectation is satisfied when, after facts gathering, the +expression returns `+passing+`, `+warning+` or `+critical+`. If no value +is returned, the result defaults to `+critical+`. The final result of +this expectation is the aggretation of all the expectation evaluations +gathered in all the involved targets. + +The aggregation returns: - `+passing+` if all the targets evaluation is +`+passing+` - `+warning+` if any of the evaluations is `+warning+` and +there is not any `+critical+` result - `+critical+` if any of the +evaluations is `+critical+` + +In this expectation type the link:#severity[severity] field of the check +is ignored. + +____ +Execution Scenario: + +* 2 targets [`+A+`, `+B+`] +* selected Checks [`+sbd_check+`] +* some environment (context) + +[source,yaml] +---- +facts: + - name: sbd_devices + gatherer: sbd_config@v1 + argument: SBD_DEVICE + +values: ... + +expectations: + - name: multiple_sbd_devices_configured + expect_enum: | + if facts.sbd_devices > values.passing_sbd_devices_count { + "passing" + } else if facts.sbd_devices == values.warning_sbd_devices_count { + "warning" + } else { + "critical" + } + + - name: multiple_sbd_devices_configured_simple + expect_enum: | + if facts.sbd_devices > values.passing_sbd_devices_count { + "passing" + } else if facts.sbd_devices == values.warning_sbd_devices_count { + "warning" + } +---- +____ + +Considering the previous scenario what happens is that: + +* the fact `+sbd_devices+` is gathered on all targets (`+A+` and `+B+` +in this case) +* the expectation expression gets executed against the `+sbd_devices+` +fact gathered on every targets. +* the evaluated value is exactly what the expression returns. If there +is not any returned value, `+critical+` is returned, as in the 2nd +expectation example. +* the evaluation result of all the targets is aggregated to compose the +final expectation result. + +==== failure_message + +An optional failure message can be declared for every expectation. + +In case of an `+expect+` one, the failure message can interpolate +`+facts+` and `+values+` present in the check definition to provide more +meaningful insights: + +[source,yaml] +---- +expectations: + - name: awesome_expectation + expect: values.awesome_constant_value == facts.awesome_fact + failure_message: The expectation did not match ${values.awesome_constant_value} +---- + +The outcome of the interpolation is available in +`+ExpectationEvaluation+` inside the API response. + +In case of an `+expect_same+` one, the failure message has to be a plain +string: + +[source,yaml] +---- +expectations: + - name: awesome_expectation + expect_same: facts.awesome_fact + failure_message: Boom! +---- + +This plain string is available in `+ExpectationResult+` inside the API +response. + +==== warning_message + +An optional warning message that works exactly as the previous +link:#failure_message[failure message]. This field is only available for +link:#expect_enum[expect_enum] expectations, and it is interpolated when +the expectation outcome is `+warning+`. + +[source,yaml] +---- +expectations: + - name: awesome_expectation + expect_enum: | + if values.passing_value == facts.awesome_fact { + "passing" + } else if values.warning_value == facts.awesome_fact { + "warning" + } + failure_message: Critical! + warning_message: Warning! +---- + +The outcome of the interpolation is available in +`+ExpectationEvaluation+` inside the API response, in the +`+failure_message+` field. + +=== Expression Language + +Different parts of the Check declaration are places where an evaluation +is needed. + +____ +Determine to what a link:#values[value] resolves during execution + +`+when: +` part of a Value’s condition +____ + +[source,yaml] +---- +values: + - name: expected_token_timeout + default: 5000 + conditions: + - value: 30000 + when: env.provider == "azure" || env.provider == "aws" + - value: 20000 + when: env.provider == "gcp" +---- + +____ +Defining the link:#expectations[Expectation] of a Check + +`+expect|expect_same: +` +____ + +[source,yaml] +---- +expectations: + - name: token_timeout + expect: facts.corosync_token_timeout == values.expected_token_timeout +---- + +See link:./expression_language.md[reference for the Expression +Language]. + +==== Evaluation Scope + +Every expression has access to an evaluation scope, allowing to access +relevant piece of information to run the expression. + +Scopes are namespaced and access to items in the scope is name based. + +===== *env* + +`+env+` is a map of information about the context of the running +execution, it is set by the system on each execution/check compilation. + +Examples of entries in the scope. What is actually available during the +execution depends on the scenario. Find the updated values in the +reference column link. + +[width="100%",cols="10%,24%,45%,21%",options="header",] +|=== +|name |Type |Reference |Applicable +|`+env.target_type+` |one of `+cluster+`, `+host+` |No enum available +|All + +|`+env.provider+` |one of `+azure+`, `+aws+`, +`+gcp+`,`+kvm+`,`+nutanix+`, `+vmware+`, `+unknown+` +|https://github.com/trento-project/web/blob/main/lib/trento/enums/provider.ex[Providers] +|All + +|`+env.cluster_type+` |one of `+hana_scale_up+`, `+hana_scale_out+`, +`+ascs_ers+`, `+unknown+` +|https://github.com/trento-project/web/blob/main/lib/trento/clusters/enums/cluster_type.ex[Cluster +types] |`+target_type+` is `+cluster+` + +|`+env.hana_scenario+` |one of `+performance_optimized+`, +`+cost_optimized+`, `+unknown+` +|https://github.com/trento-project/web/blob/main/lib/trento/clusters/enums/hana_scenario.ex[Hana +Scale Up Scenario] |`+cluster_type+` is `+hana_scale_up+` + +|`+env.architecture_type+` |one of `+classic+`, `+angi+` +|https://github.com/trento-project/web/blob/main/lib/trento/clusters/enums/hana_architecture_type.ex[Architecture +types] |`+cluster_type+` is one of `+hana_scale_up+`, `+hana_scale_out+` + +|`+env.ensa_version+` |one of `+ensa1+`, `+ensa2+`, `+mixed_versions+` +|https://github.com/trento-project/web/blob/main/lib/trento/clusters/enums/cluster_ensa_version.ex[ENSA +version] |`+cluster_type+` is `+ascs_ers+` + +|`+env.filesystem_type+` |one of `+resource_managed+`, `+simple_mount+`, +`+mixed_fs_types+` +|https://github.com/trento-project/web/blob/main/lib/trento/clusters/enums/filesystem_type.ex[Filesystem +type] |`+cluster_type+` is `+ascs_ers+` +|=== + +===== *facts* + +`+facts+` is the map of the gathered facts, thus the scope varies based +on which facts have been declared in the link:#facts[relative section], +and are accessible in other sections by fact name. + +[source,yaml] +---- +facts: + - name: an_interesting_fact + gatherer: + argument: + + - name: another_interesting_fact + gatherer: + argument: +---- + +Available entries in scope, the value is what has been gathered on the +targets | name + +| —————————– | `+facts.an_interesting_fact+` + +| `+facts.another_interesting_fact+` + +===== *values* + +`+values+` is the map of resolved variable names defined in the +link:#values[relative section] + +[source,yaml] +---- +values: + - name: expected_token_timeout + default: 5000 + conditions: + - value: 30000 + when: env.provider == "azure" || env.provider == "aws" + - value: 20000 + when: env.provider == "gcp" + + - name: another_variable_value + default: "blue" + conditions: + - value: "red" + when: env.should_be_red == true +---- + +Available entries in scope | name | Resolved to | | ——————————- | +———————————————— | | `+values.expected_token_timeout+` | `+5000+`, +`+30000+`, `+20000+` based on the conditions | | +`+values.another_variable_value+` | `+blue+`, `+red+` based on the +conditions | + +=== Best practices and conventions + +To have a standardized format for writing checks, follow the next best +practices and conventions as much as possible: + +* The `+id+` field must be wrapped in double quotes to avoid any type of +ambiguity, as this field must be of string format. +* The remaining `+name+`, `+description+`, `+group+`, and +`+remediation+` fields must not be wrapped in quotes, as they are +text-based values always. +* Take advantage of markdown tags in the `+name+`, `+description+`, and +`+remediation+` fields to make the text easy and compelling to read. +* The `+name+` field of `+facts+`, `+values+`, and `+expectations+` must +follow `+camel_case+` format. + +For example: ++ +.... +facts: + - name: some_fact +... +values: + - name: expected_some_fact +... +expectations: + - name: some_expectation +... +.... +* Use 2 spaces to indent multiline expectation expressions. +* Naming hardcoded values in the `+values+` section with the `+default+` +field is encouraged instead of putting hardcoded values in the +expectation expression itself. This gives some meaning to the expected +value and improves potential interaction with the Wanda API. + +So this: ++ +.... +expectations: + - name: some_expectation + expect: facts.foo == 30 +.... ++ +would be: ++ +.... +values: + - name: expected_foo + default: 30 + +expectations: + - name: some_expectation + expect: facts.foo == values.expected_foo +.... +* If the gathered fact is compared to a value, using `+value+` and +`+expected_value+` names for facts and values respectively is +recommended, as it improves the meaning of the comparison. + +For example: ++ +.... +facts: + - name: some_fact +... +values: + - name: expected_some_fact +... +.... +* Avoid adding prefixes such as `+facts+` or `+values+` to the entries +of these sections, as they already use this as a namespace. For example, +the next example should be avoided, as the `+facts+` prefix would be +redundant in the expectation expression: ++ +.... +facts: + - name: facts_some_fact +.... +* If the implemented expectation expression contains any kind of `+&&+` +to combine multiple operations, consider adding them as individual +expectations, as the final result is the combination of all of them. + +So this: ++ +.... +expectations: + - name: some_expectation + expect: facts.foo == values.expected_foo && facts.bar == values.expected_bar +.... ++ +would be: ++ +.... +expectations: + - name: foo_expectation + expect: facts.foo == values.expected_foo + - name: bar_expectation + expect: facts.bar == values.expected_bar +.... +* Pipe the expression language functions vertically in order to provide +a better visual output of the code. + +So this: ++ +.... +expectations: + - name: some_expectation + expect: facts.foo.find(|item| item.id == "super").properties.find(|prop| prop.name == "good").value +.... ++ +would be: ++ +.... +expectations: + - name: some_expectation + expect: | + facts.foo + .find(|item| item.id == "super").properties + .find(|prop| prop.name == "good").value +.... ++ +____ +Note: Keep in mind that some functions such as `+sort+` and `+drain+` +run in-place modifications, so they cannot be piped. +____ diff --git a/components/web/alerting/alerting.adoc b/components/web/alerting/alerting.adoc new file mode 100644 index 00000000..d6f97826 --- /dev/null +++ b/components/web/alerting/alerting.adoc @@ -0,0 +1,55 @@ +== Alerting + +The Alerting feature notifies the SAP Administrator about important +events detected in the Landscape being monitored/observed by Trento. + +Some of the notified events: - *Host Health detected critical* - +*Cluster Health detected critical* - *Database Health detected critical* +- *SAP System Health detected critical* - … + +=== Enabling Alerting + +This feature is *disabled by default*. + +Provide `+ENABLE_ALERTING=true+` as an environment variable when +starting Trento. + +=== Delivery and Recipient + +A notification needs to be _delivered to someone_ in _some way_. + +With alerting enabled some extra configuration is needed to define the +recipient and the delivery mechanism. + +Currently *authenticated SMTP* is the *only supported delivery +mechanism* for alert notifications. + +.... +ENABLE_ALERTING=true +ALERT_SENDER=sender@yourmail.com +ALERT_RECIPIENT=recipient@yourmail.com + +SMTP_SERVER=your.smtp-server.com +SMTP_PORT=2525 +SMTP_USER=user +SMTP_PASSWORD=password +.... + +=== Enabling Alerting at a later stage + +If your current Trento installation has Alerting disabled, you can +enable it by upgrading the helm deployment. + +.... +helm upgrade + --install + --set trento-web.adminUser.password= + --set-file trento-runner.privateKey= + --set trento-web.alerting.enabled=true + --set trento-web.alerting.smtpServer= + --set trento-web.alerting.smtpPort= + --set trento-web.alerting.smtpUser= + --set trento-web.alerting.smtpPassword= + --set trento-web.alerting.sender= + --set trento-web.alerting.recipient= +.... diff --git a/components/web/authentication/jwt_specification.adoc b/components/web/authentication/jwt_specification.adoc new file mode 100644 index 00000000..b90027d5 --- /dev/null +++ b/components/web/authentication/jwt_specification.adoc @@ -0,0 +1,119 @@ +== JWT + +The `+web+` dashboard is the identity provider and authentication +manager of the Trento stack. The API endpoints of the Trento project are +protected with a JWT token authentication. + +To retrieve and refresh an access token, you should always refer to the +web dashboard, with dedicated endpoints. + +=== Login + +Endpoint: `+/api/session+` Method: POST Content-Type: +`+application/json+` + +Body + +[source,json] +---- +{ + "username": "yourusername", + "password": "yourpassword" +} +---- + +Returns 401 if the credentials are invalid. + +*Curl Example* + +[source,bash] +---- +curl 'http:///api/session' \ + -H 'Accept: application/json' \ + -H 'Content-Type: application/json' \ + --data-raw '{"username":"your_username","password":"your_password"}' \ +---- + +The login endpoint returns a pair of JWT tokens, an `+access_token+`, +used as `+Bearer+` token for all the API requests, and a +`+refresh_token+` used to regenerate an `+access_token+` when the token +expires. + +==== JWT anatomy + +*Access token* + +[source,json] +---- +{ + "aud": "trento-project", + "exp": 1673882986, + "iat": 1673882386, + "iss": "https://github.com/trento-project/web", + "jti": "2std6abj9nni0s3kp8000lv2", + "nbf": 1673882386, + "sub": 1, + "typ": "Bearer" +} +---- + +*Refresh Token* + +[source,json] +---- +{ + "aud": "trento-project", + "exp": 1673886911, + "iat": 1673865311, + "iss": "https://github.com/trento-project/web", + "jti": "2stc78e75h9sgvrc9s0003f2", + "nbf": 1673865311, + "sub": 1, + "typ": "Refresh" +} +---- + +You can distinguish the `+access_token+` from the `+refresh_token+` +using the claim `+typ+` of the JWT. + +The `+access_token+` has a lifespan of *10 minutes*, the +`+refresh_token+` has a lifespan of *6 hours*. + +The `+sub+` claim, contains the identifier of the user, in the example +JWT `+1+`. + +=== Refresh an access token + +To refresh an `+access_token+` when expires, you should use the +`+refresh+` endpoint. + +Endpoint: `+/api/session/refresh+` Method: POST Content-Type: +`+application/json+` + +Body + +[source,json] +---- +{ + "refresh_token": "YOUREFRESHTOKENJWT", +} +---- + +Returns 401 if the refresh token is invalid or expired. + +*Curl Example* + +[source,bash] +---- +curl 'http:///api/session/refresh' \ + -H 'Accept: application/json' \ + -H 'Content-Type: application/json' \ + --data-raw '{"refresh_token":"YOUR_REFRESH_TOKEN"}' \ +---- + +The endpoint will return a new `+access_token+` with the same lifespan +as all the other `+access_token+`. + +Please refer to the +https://www.trento-project.io/web/swaggerui/#/Platform/TrentoWeb.SessionController.create[OpenAPI] +spec for further details and client generation. diff --git a/components/web/authentication/spa_flow.adoc b/components/web/authentication/spa_flow.adoc new file mode 100644 index 00000000..16f294dc --- /dev/null +++ b/components/web/authentication/spa_flow.adoc @@ -0,0 +1,23 @@ +== Trento Single Page Application + +The trento single page application, leverages the JWT authentication +mechanism of `+trento+` API, using the refresh token flow when the +`+access_token+` expires. + +=== Login Diagram + +.Login diagram +image::trento-spa-login.png[Login diagram] + +=== Refresh Token Success Diagram + +.Refresh token success diagram +image::trento-spa-refresh.png[Refresh token success diagram] + +=== Refresh Token Failure Diagram + +.Refresh token failure diagram +image::trento-spa-refresh-failed.png[Refresh token failure diagram] + +All the login/logout procedures are handled by the `+SPA+` using route +guards and authentication providers hooked into the network calls. diff --git a/components/web/development/environment_variables.adoc b/components/web/development/environment_variables.adoc new file mode 100644 index 00000000..dd5e55ee --- /dev/null +++ b/components/web/development/environment_variables.adoc @@ -0,0 +1,29 @@ +== Environment Variables + +A possibly non comprehensive list of the environment variables needed by +the control plane to work. + +Dig into +https://github.com/trento-project/web/blob/main/config/[./config] +directory for mode details. + +''''' + +*Persistence* - `+DATABASE_URL+` - `+DATABASE_POOL_SIZE+` - +`+EVENTSTORE_URL+` - `+EVENTSTORE_POOL_SIZE+` + +*Basic encryption* - `+SECRET_KEY_BASE+` + +*Server* - `+PORT+` - `+TRENTO_WEB_ORIGIN+` + +*Runner integration* - `+RUNNER_URL+` + +*Monitoring* - `+PROMETHEUS_URL+` - `+CHARTS_ENABLED+` + +*Alerting* - `+ENABLE_ALERTING+` - `+ALERT_RECIPIENT+` + +*SMTP* - `+SMTP_SERVER+` - `+SMTP_PORT+` - `+SMTP_USER+` - +`+SMTP_PASSWORD+` + +*AUTHENTICATION* - `+ACCESS_TOKEN_ENC_SECRET+` - +`+REFRESH_TOKEN_ENC_SECRET+` diff --git a/components/web/development/hack_on_the_trento.adoc b/components/web/development/hack_on_the_trento.adoc new file mode 100644 index 00000000..c40a28b4 --- /dev/null +++ b/components/web/development/hack_on_the_trento.adoc @@ -0,0 +1,152 @@ +== Hack on the Trento Web + +=== Requirements + +In order to run the Trento Web application, the following software must +be installed: + +[arabic] +. https://elixir-lang.org/[Elixir] - 1.15.7 preferred +. https://www.erlang.org/[Erlang OTP] - 26.1.2 preferred +. https://nodejs.org/en/[Node.js] - 20.14.0 preferred +. https://docs.docker.com/get-docker/[Docker] +. https://docs.docker.com/compose/install/[Docker Compose] + +==== Additional requirements + +Some platforms might not be able to use pre-built versions of some +dependencies. Therefore, some additional dependencies might be required. +This does not effect most users and can be referred to, when +installation issues come up. For these dependencies, the distro packaged +version is usually sufficient. + +[arabic] +. https://www.python.org/[Python3] +. https://setuptools.pypa.io/en/latest/index.html[setuptools] +. https://gcc.gnu.org/[gcc] +. https://www.freedesktop.org/wiki/Software/pkg-config/[pkg-config] + +==== Ensure Compatibility with asdf + +https://asdf-vm.com/guide/introduction.html[asdf] allows to use specific +versions of programming language tools that are known to be compatible +with the project, rather than relying on the version that’s installed +globally on the host system. + +In order to use asdf, follow the official +https://asdf-vm.com/guide/getting-started.html[asdf getting started +guide]. + +Install all required asdf plugins from +link:/.tool-versions[.tool-versions] inside the web repository. + +.... +cut -d' ' -f1 .tool-versions|xargs -i asdf plugin add {} +.... + +Set up the asdf environment + +.... +asdf install +.... + +=== Development environment + +The Trento project provides a docker-compose development environment +that is used to start a Postgres database and a prometheus instance for +storage and monitoring. To start the development environment, navigate +to the root directory of the Trento project and run the following +command: + +.... +docker-compose up -d +.... + +=== Setup Trento + +Before starting Trento Web, some initial setup tasks, like installing +dependencies and creating the database, are required. Execute following +command: + +.... +mix setup +.... + +=== Connect Trento Web with https://github.com/trento-project/wanda[Wanda] + +By default, Wanda can be accessed on port 4001. + +The wanda url is provided with the configuration parameter +`+:trento, :checks_service, :base_url+`. + +*Guide* how to set up +https://github.com/trento-project/wanda/blob/main/guides/development/hack_on_wanda.md[Wanda]. + +Note: If the Wanda service is running on a different port, change the +default 4001 port in the .env file. + +=== Install the JavaScript frontend packages + +Install frontend packages: + +.... +npm --prefix ./assets/ install ./assets +.... + +=== Start Trento Web server in the REPL + +Start Trento web: + +.... +iex -S mix phx.server +.... + +=== Access the Trento Web + +Access the Trento Web by navigating to http://localhost:4000 in the web +browser. + +=== Login + +The default login credentials are: + +Username: + +.... +admin +.... + +Password: + +.... +adminpassword +.... + +=== Environment Variables + +The Trento application uses several environment variables to configure +its behavior. Information about these variables’ +link:./environment_variables.md[environment_variables]. + +=== Scenario loading with Photofinish + +The Trento project includes a tool called +https://github.com/trento-project/photofinish[photofinish], which is +used to load different scenarios for development and debugging purposes. + +.... +photofinish run --url "http://localhost:4000/api/collect" healthy-27-node-SAP-cluster +.... + +It’s possible to use Photofinish’ docker image too: + +.... +docker run -v "$PWD":/data --network host ghcr.io/trento-project/photofinish run healthy-27-node-SAP-cluster -u http://localhost:4000/api/collect +.... + +Several useful scenario fixtures are available in +https://github.com/trento-project/web/tree/main/test/fixtures/scenarios[./test/fixtures/scenarios], +the same ones used in e2e tests. + +See also +https://github.com/trento-project/web/blob/main/.photofinish.toml[.photofinish.toml]. diff --git a/components/web/integration/oidc.adoc b/components/web/integration/oidc.adoc new file mode 100644 index 00000000..14304f29 --- /dev/null +++ b/components/web/integration/oidc.adoc @@ -0,0 +1,131 @@ +== OpenID Connect + +Trento integrates with an identity provider (IDP) that use the OpenID +Connect (OIDC) protocol to authenticate users accessing the console. +Authorization for specific abilities/permissions is managed by Trento, +which means that only basic user information is retrieved from the +external IDP. + +=== Enabling OIDC + +The OIDC authentication is *disabled by default*. + +Provide the following environment variables to enable OIDC feature when +starting Trento. + +.... +# Required: +ENABLE_OIDC=true +OIDC_CLIENT_ID=<> +OIDC_CLIENT_SECRET=<> +OIDC_BASE_URL=<> + +# Optional: +OIDC_CALLBACK_URL=<> +.... + +=== Enabling OIDC in Development + +Enable OIDC in the development environment using Docker and +https://github.com/keycloak/keycloak[Keycloak] as IDP. + +==== Starting Keycloak Identity Provider + +Use a custom Docker profile to start Keycloak as IDP for local +development. + +Start the Docker containers with the `+idp+` profile: + +.... +docker compose --profile idp up +.... + +Keycloak server can be accessed at http://localhost:8081 + +==== Create OIDC configuration + +[arabic] +. Create a new local development configuration `+dev.local.exs+` in +`+config+` directory. +. Enable OIDC in `+dev.local.exs+` config: ++ +[source,elixir] +---- +import Config + +config :trento, :oidc, enabled: true +---- +. Start Trento web as usual ++ +`+iex -S mix phx.server+` + +==== Login into Trento web console using Single Sign-on with Keycloak + +[arabic] +. Navigate to the http://localhost:4000/[Trento web console]. +. Click on `+Login with Single Sign-on+`: + +.trento_single_sign_on_login +image::trento_single_sign_on_login.png[trento_single_sign_on_login] + +You will be redirected to the Keycloak login page: +image:keycloack_login.png[keycloack_login] + +==== Login as Trento user through keycloak IDP + +The default Trento login credentials are: + +Username: + +.... +trentoidp +.... + +Password: + +.... +password +.... + +After successfully entering user login data, the user is redirected to +Trento web console. + +==== Login into the Keycloak Admin Console + +Username: + +.... +keycloak +.... + +Password: + +.... +admin +.... + +==== Assigning Admin Rights to the Trento User + +Grant admin rights to the `+trentoidp+` user, update the +`+config/dev.local.exs+` file as follows , then restart the application + +.... +import Config + +config :trento, :oidc, enabled: true + +config :trento, + admin_user: "trentoidp" +.... + +==== Run OIDC integration E2E tests + +Running OIDC e2e tests, requires a running IDP provider. + +Run docker compose with the `+--profile idp+` flag, to use our +https://github.com/keycloak/keycloak[Keycloak] deployment for testing. + +==== Run OIDC tests in the GitHub CI + +Add the `+integration+` label to the PR, otherwise CI is executed +without OIDC integration tests. diff --git a/components/web/monitoring/monitoring.adoc b/components/web/monitoring/monitoring.adoc new file mode 100644 index 00000000..632317ab --- /dev/null +++ b/components/web/monitoring/monitoring.adoc @@ -0,0 +1,22 @@ +== Monitoring + +Currently Trento provides a basic integration with +https://github.com/prometheus/prometheus[Prometheus] that gives realtime +information of the following metrics: + +* Host CPU usage +* Host Memory usage + +Current integration strategy: Custom charts Trento UI (_Host Details_). + +In order for monitoring to properly work here’s the required environment +variables - `+PROMETHEUS_URL+` -> prometheus URL, should be accessible +from the web backend, it’s not mandatory to expose on the internet. + +On a full Trento installation monitoring is enabled by default and the +configuration is handled by the helm-charts. + +''''' + +.Monitoring Architecture +image::trento-monitoring.png[Monitoring Architecture] diff --git a/adr/.adr-dir b/dev_docs_legacy/adr/.adr-dir similarity index 100% rename from adr/.adr-dir rename to dev_docs_legacy/adr/.adr-dir diff --git a/adr/.tool-versions b/dev_docs_legacy/adr/.tool-versions similarity index 100% rename from adr/.tool-versions rename to dev_docs_legacy/adr/.tool-versions diff --git a/adr/0001-record-architecture-decisions.md b/dev_docs_legacy/adr/0001-record-architecture-decisions.md similarity index 100% rename from adr/0001-record-architecture-decisions.md rename to dev_docs_legacy/adr/0001-record-architecture-decisions.md diff --git a/adr/0002-build-a-dsl-to-declare-checks.md b/dev_docs_legacy/adr/0002-build-a-dsl-to-declare-checks.md similarity index 100% rename from adr/0002-build-a-dsl-to-declare-checks.md rename to dev_docs_legacy/adr/0002-build-a-dsl-to-declare-checks.md diff --git a/adr/0003-build-a-check-execution-orchestrator.md b/dev_docs_legacy/adr/0003-build-a-check-execution-orchestrator.md similarity index 100% rename from adr/0003-build-a-check-execution-orchestrator.md rename to dev_docs_legacy/adr/0003-build-a-check-execution-orchestrator.md diff --git a/adr/0004-add-facts-gathering-capabilities-to-the-agent.md b/dev_docs_legacy/adr/0004-add-facts-gathering-capabilities-to-the-agent.md similarity index 100% rename from adr/0004-add-facts-gathering-capabilities-to-the-agent.md rename to dev_docs_legacy/adr/0004-add-facts-gathering-capabilities-to-the-agent.md diff --git a/adr/0005-use-protobuf-to-define-and-generate-contracts.md b/dev_docs_legacy/adr/0005-use-protobuf-to-define-and-generate-contracts.md similarity index 100% rename from adr/0005-use-protobuf-to-define-and-generate-contracts.md rename to dev_docs_legacy/adr/0005-use-protobuf-to-define-and-generate-contracts.md diff --git a/adr/0006-use-cloudevents-to-describe-event-data.md b/dev_docs_legacy/adr/0006-use-cloudevents-to-describe-event-data.md similarity index 100% rename from adr/0006-use-cloudevents-to-describe-event-data.md rename to dev_docs_legacy/adr/0006-use-cloudevents-to-describe-event-data.md diff --git a/adr/0007-use-jwt-tokens-as-authentication-mechanism.md b/dev_docs_legacy/adr/0007-use-jwt-tokens-as-authentication-mechanism.md similarity index 100% rename from adr/0007-use-jwt-tokens-as-authentication-mechanism.md rename to dev_docs_legacy/adr/0007-use-jwt-tokens-as-authentication-mechanism.md diff --git a/adr/0008-rest-api-versioning-strategy.md b/dev_docs_legacy/adr/0008-rest-api-versioning-strategy.md similarity index 100% rename from adr/0008-rest-api-versioning-strategy.md rename to dev_docs_legacy/adr/0008-rest-api-versioning-strategy.md diff --git a/adr/0009-frontend-directory-structure-and-architecture.md b/dev_docs_legacy/adr/0009-frontend-directory-structure-and-architecture.md similarity index 100% rename from adr/0009-frontend-directory-structure-and-architecture.md rename to dev_docs_legacy/adr/0009-frontend-directory-structure-and-architecture.md diff --git a/adr/0010-web-dashboard-directory-structure-and-contexts.md b/dev_docs_legacy/adr/0010-web-dashboard-directory-structure-and-contexts.md similarity index 100% rename from adr/0010-web-dashboard-directory-structure-and-contexts.md rename to dev_docs_legacy/adr/0010-web-dashboard-directory-structure-and-contexts.md diff --git a/adr/0011-sap-system-database-aggregate-split.md b/dev_docs_legacy/adr/0011-sap-system-database-aggregate-split.md similarity index 100% rename from adr/0011-sap-system-database-aggregate-split.md rename to dev_docs_legacy/adr/0011-sap-system-database-aggregate-split.md diff --git a/adr/0012-reactiveness.md b/dev_docs_legacy/adr/0012-reactiveness.md similarity index 100% rename from adr/0012-reactiveness.md rename to dev_docs_legacy/adr/0012-reactiveness.md diff --git a/adr/0013-suma-integration.md b/dev_docs_legacy/adr/0013-suma-integration.md similarity index 100% rename from adr/0013-suma-integration.md rename to dev_docs_legacy/adr/0013-suma-integration.md diff --git a/adr/0014-decoupling-of-trento-checks-from-wanda.md b/dev_docs_legacy/adr/0014-decoupling-of-trento-checks-from-wanda.md similarity index 100% rename from adr/0014-decoupling-of-trento-checks-from-wanda.md rename to dev_docs_legacy/adr/0014-decoupling-of-trento-checks-from-wanda.md diff --git a/adr/0015-activity-logging.md b/dev_docs_legacy/adr/0015-activity-logging.md similarity index 100% rename from adr/0015-activity-logging.md rename to dev_docs_legacy/adr/0015-activity-logging.md diff --git a/adr/0016-discarded-discovery-events.md b/dev_docs_legacy/adr/0016-discarded-discovery-events.md similarity index 100% rename from adr/0016-discarded-discovery-events.md rename to dev_docs_legacy/adr/0016-discarded-discovery-events.md diff --git a/adr/0018-agent-operations-orchestration.md b/dev_docs_legacy/adr/0018-agent-operations-orchestration.md similarity index 100% rename from adr/0018-agent-operations-orchestration.md rename to dev_docs_legacy/adr/0018-agent-operations-orchestration.md diff --git a/adr/0019-e2e-testing-practices.md b/dev_docs_legacy/adr/0019-e2e-testing-practices.md similarity index 100% rename from adr/0019-e2e-testing-practices.md rename to dev_docs_legacy/adr/0019-e2e-testing-practices.md diff --git a/adr/0020-checks-customization.md b/dev_docs_legacy/adr/0020-checks-customization.md similarity index 100% rename from adr/0020-checks-customization.md rename to dev_docs_legacy/adr/0020-checks-customization.md diff --git a/adr/README.md b/dev_docs_legacy/adr/README.md similarity index 100% rename from adr/README.md rename to dev_docs_legacy/adr/README.md diff --git a/coding-standards/README.md b/dev_docs_legacy/coding-standards/README.md similarity index 100% rename from coding-standards/README.md rename to dev_docs_legacy/coding-standards/README.md diff --git a/coding-standards/elixir.md b/dev_docs_legacy/coding-standards/elixir.md similarity index 100% rename from coding-standards/elixir.md rename to dev_docs_legacy/coding-standards/elixir.md diff --git a/coding-standards/go.md b/dev_docs_legacy/coding-standards/go.md similarity index 100% rename from coding-standards/go.md rename to dev_docs_legacy/coding-standards/go.md diff --git a/coding-standards/javascript.md b/dev_docs_legacy/coding-standards/javascript.md similarity index 100% rename from coding-standards/javascript.md rename to dev_docs_legacy/coding-standards/javascript.md diff --git a/guides/architecture/trento-architecture.md b/dev_docs_legacy/guides/architecture/trento-architecture.md similarity index 100% rename from guides/architecture/trento-architecture.md rename to dev_docs_legacy/guides/architecture/trento-architecture.md diff --git a/guides/assets/event-sourcing-cqrs.png b/dev_docs_legacy/guides/assets/event-sourcing-cqrs.png similarity index 100% rename from guides/assets/event-sourcing-cqrs.png rename to dev_docs_legacy/guides/assets/event-sourcing-cqrs.png diff --git a/guides/assets/trento-architecture.drawio b/dev_docs_legacy/guides/assets/trento-architecture.drawio similarity index 100% rename from guides/assets/trento-architecture.drawio rename to dev_docs_legacy/guides/assets/trento-architecture.drawio diff --git a/guides/assets/trento-architecture.png b/dev_docs_legacy/guides/assets/trento-architecture.png similarity index 100% rename from guides/assets/trento-architecture.png rename to dev_docs_legacy/guides/assets/trento-architecture.png diff --git a/guides/development/pr-env-ssl-certificate-setup.md b/dev_docs_legacy/guides/development/pr-env-ssl-certificate-setup.md similarity index 100% rename from guides/development/pr-env-ssl-certificate-setup.md rename to dev_docs_legacy/guides/development/pr-env-ssl-certificate-setup.md diff --git a/guides/development/release.md b/dev_docs_legacy/guides/development/release.md similarity index 100% rename from guides/development/release.md rename to dev_docs_legacy/guides/development/release.md diff --git a/guides/development/test-manual-installation-with-agents.md b/dev_docs_legacy/guides/development/test-manual-installation-with-agents.md similarity index 100% rename from guides/development/test-manual-installation-with-agents.md rename to dev_docs_legacy/guides/development/test-manual-installation-with-agents.md diff --git a/guides/manual-installation.adoc b/dev_docs_legacy/guides/manual-installation.adoc similarity index 100% rename from guides/manual-installation.adoc rename to dev_docs_legacy/guides/manual-installation.adoc diff --git a/rfc/0001-checks-customization.md b/dev_docs_legacy/rfc/0001-checks-customization.md similarity index 100% rename from rfc/0001-checks-customization.md rename to dev_docs_legacy/rfc/0001-checks-customization.md diff --git a/developer/adr/0001-record-architecture-decisions.adoc b/developer/adr/0001-record-architecture-decisions.adoc new file mode 100644 index 00000000..dda19d3e --- /dev/null +++ b/developer/adr/0001-record-architecture-decisions.adoc @@ -0,0 +1,24 @@ +== 1. Record architecture decisions + +Date: 2022-07-27 + +=== Status + +Accepted + +=== Context + +We need to record the architectural decisions made on this project. + +=== Decision + +We will use Architecture Decision Records, as +http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions[described +by Michael Nygard]. We will add to the standard template a "`Considered +Options`" paragraph when to track the alternatives we considered in the +decision-making process. + +=== Consequences + +See Michael Nygard’s article, linked above. For a lightweight ADR +toolset, see Nat Pryce’s https://github.com/npryce/adr-tools[adr-tools]. diff --git a/developer/adr/0002-build-a-dsl-to-declare-checks.adoc b/developer/adr/0002-build-a-dsl-to-declare-checks.adoc new file mode 100644 index 00000000..f8670815 --- /dev/null +++ b/developer/adr/0002-build-a-dsl-to-declare-checks.adoc @@ -0,0 +1,39 @@ +== 2. Build a DSL to declare checks + +Date: 2022-07-27 + +=== Status + +Accepted + +=== Context + +We want to build a Domain Specific Language to declare checks, +superseding the current architecture which is based on Ansible. The need +this DSL aims to fulfill is to provide users a simple way to declare +what we (the Trento Core Team) often refer to as "`checks`". Checks are, +in Trento’s domain, the crystallization of SUSE’s best practices when it +comes to SAP clusters configuration in a form that both a man and a +machine can read. + +=== Decision + +We will declare checks by using a YAML file with three main sections. +The "`metadata`" section will contain the id, name, description and +other relevant information. The "`facts`" section will declare which +facts should be extracted from the hosts. The "`expectations`" section +will declare the assertions that need to be true for the check to pass. + +=== Consequences + +By rolling our DSL, we can describe checks in a declarative way. We get +several benefits from this approach: + +* Humans can formalize best practices with no space for ambiguity; +* Machines can assert systems’ states, automatically, with no space for +ambiguity; +* The development of new best practices gets streamlined through a +common definition that allows to bootstrap shared efforts; +* The custom YAML will improve the DX, by superseding Ansible playbooks +which are not tailored around checks execution; +* Linting and validation tools can be provided and added to the CI. diff --git a/developer/adr/0003-build-a-check-execution-orchestrator.adoc b/developer/adr/0003-build-a-check-execution-orchestrator.adoc new file mode 100644 index 00000000..653f41b2 --- /dev/null +++ b/developer/adr/0003-build-a-check-execution-orchestrator.adoc @@ -0,0 +1,44 @@ +== 3. Build a check execution orchestrator + +Date: 2022-11-17 + +=== Status + +Accepted + +=== Context + +We need to orchestrate the execution of the checks, namely the +"`facts-gathering`" and the "`expectations evaluation`" steps. As we +moved the facts-gathering capabilities from the runner to the agent, we +need to implement a way to orchestrate facts-gathering processes and +evaluations of the expectations defined by the DSL. + +=== Decision + +We decided to create a new service, which will supersede the current +runner and will be responsible for the orchestration of the execution of +the check. The service will be able to collect the facts from the agents +and evaluate the expectations of the checks by using a scripting +language. Once every fact is collected and every expectation is +evaluated, the service will send a message to the server with the +results of the execution, in a best-effort fashion. An execution is +completed when all the checks are evaluated, and the results are sent as +a message to the other components, with detailed information about the +execution, such as evaluation results, facts gathered, and so on. The +executions will be stored in the database and will be exposed via an API +to the consumers. + +=== Consequences + +The runner will be superseded by a new service, which will be +responsible for the orchestration of the execution of the checks. The +dashboard will be able to consume the results of the execution via the +API and will be able to display the results of the checks. The +responsibility of the dashboard will be limited to the presentation of +the results of the checks, and the orchestration of the execution will +be delegated to the new service, simplifying the cluster aggregate and +reducing the number of events stored in the Event Store. The UX in the +dashboard will be improved, as the results of the checks are not limited +to an "`OK`" or "`KO`" status, but can be more detailed and can include +the results of facts and expectations. diff --git a/developer/adr/0004-add-facts-gathering-capabilities-to-the-agent.adoc b/developer/adr/0004-add-facts-gathering-capabilities-to-the-agent.adoc new file mode 100644 index 00000000..34a2a8d1 --- /dev/null +++ b/developer/adr/0004-add-facts-gathering-capabilities-to-the-agent.adoc @@ -0,0 +1,44 @@ +== 4. Add facts-gathering capabilities to the agent + +Date: 2022-11-17 + +=== Status + +Accepted + +=== Context + +We want to gather facts from the target infrastructure, to be able to +use them in the expectations section of the checks or for other +purposes. A "`gatherer`" is a piece of code that knows how to extract a +specific fact from a host. We need to implement a way to run the +gatherers and collect the facts, namely the "`gathering engine`". Also, +advanced users might want to develop custom gatherers, we can refer to +them as "`gatherers plugins`". + +=== Decision + +The agent should be able to gather facts from the hosts and send them to +the server. A request is sent from the server to the agent to gather +facts, the agent runs the gatherers and sends the facts to the server. +The communication between the agent and the server is asynchronously +done via AMQP. If the agent is not able to gather one or more facts, it +should send a message to the server with the list of the failed +gatherers alongside the successfully gathered facts, in a best-effort +fashion. + +=== Consequences + +By moving the facts-gathering capabilities from the runner to the agent, +we provide a clear separation of concerns. The agent is agnostic of the +checks and the evaluation of the expectations. This reduces the +complexity of the runner, gives clear responsibilities to the +components, and removes the need for an SSH connection from the runner +to the hosts. Also by introducing the concept of gatherers, we limit the +execution of arbitrary code on the hosts, by providing a well-defined +set of actions that the agent can perform. A specific gatherer is +executed just once for each gathering request, this reduces the load on +the hosts and the network. Gatherers could be used in the future for +other purposes than the evaluation of the expectations, for example, to +generate a report of the infrastructure, or in place of the current +discovery mechanism. diff --git a/developer/adr/0005-use-protobuf-to-define-and-generate-contracts.adoc b/developer/adr/0005-use-protobuf-to-define-and-generate-contracts.adoc new file mode 100644 index 00000000..0217b5a0 --- /dev/null +++ b/developer/adr/0005-use-protobuf-to-define-and-generate-contracts.adoc @@ -0,0 +1,31 @@ +== 5. Use Protocol Buffers to define and generate contracts + +Date: 2022-11-17 + +=== Status + +Accepted + +=== Context + +We need to define contracts between the components of Trento to +communicate with each other. We want to use a well-known and widely +adopted format to define these contracts. We want to use a format that +allows us to generate code from the contracts, avoid boilerplate code +and reduce the risk of errors. + +=== Decision + +Protocol Buffers (Protobuf) is a good candidate for this task, as it is +a language-neutral, platform-neutral, extensible mechanism for +serializing structured data. A new repository will be created to host +the Protocol Buffers contracts, and the generated code will be published +as a Go and Elixir package to be used by the components. + +=== Consequences + +Each component will have to import the generated code to be able to +communicate with the other components. A mapping between the Protocol +Buffers contracts and the internal data structures will be needed to +avoid exposing the Protocol Buffers contracts to the rest of the +codebase. diff --git a/developer/adr/0006-use-cloudevents-to-describe-event-data.adoc b/developer/adr/0006-use-cloudevents-to-describe-event-data.adoc new file mode 100644 index 00000000..85713e08 --- /dev/null +++ b/developer/adr/0006-use-cloudevents-to-describe-event-data.adoc @@ -0,0 +1,28 @@ +== 6. Use CloudEvents to describe event data + +Date: 2022-11-17 + +=== Status + +Accepted + +=== Context + +We need to define a common way to describe the data of the events that +are exchanged between the components of Trento. An envelope format is +needed to carry the event payload and metadata and to easily serialize +the data for the transport layer. Ideally, we want to use a format that +allows us to open the door for third-party components to easily +integrate with Trento. + +=== Decision + +Decided on https://cloudevents.io/[CloudEvents]. + +=== Consequences + +The event data will be wrapped in a CloudEvent envelope. The envelope +will be completely opaque to the components, and the components will +only interact with the event payload. The envelope will be serialized +for the transport layer. A facade will be provided in the contracts +packages to easily create CloudEvents from the internal data structures. diff --git a/developer/adr/0007-use-jwt-tokens-as-authentication-mechanism.adoc b/developer/adr/0007-use-jwt-tokens-as-authentication-mechanism.adoc new file mode 100644 index 00000000..af2210a2 --- /dev/null +++ b/developer/adr/0007-use-jwt-tokens-as-authentication-mechanism.adoc @@ -0,0 +1,59 @@ +== 7. Use JWT tokens as authentication mechanism + +Date: 2022-12-06 + +=== Status + +Accepted + +=== Context + +We have cookie based authentication in place on the trento-web project. +The decision was made based on previous requirements but with the +evolution of the `+trento platform+` we need a different authentication +method. The authentication method should be cookie-less, stateless and +capable of authenticating more services with a single authentication +token. + +The new service, `+wanda+`, responsible for the new check engine +execution, needs an authentication mechanism. + +The new authentication solution should be based on standards and should +be easy and appropriate to integrate into both Single Page Applications +and raw API consumption. + +=== Decision + +Decided on https://jwt.io[JWT] token-based authentication. + +We don’t want to use an API gateway and an external identity provider +because the effort of deploying and maintaining these new pieces of +architecture does not make sense right now. + +We want to use the web dashboard as an identity provider and +authentication coordinator, this is a good compromise between the level +of security we need and the type of application we are building right +now. The choice of JWT as a token standard implies that a future switch +to an external identity provider and/or API gateway will be a painless +and easy procedure for our applications. + +We considered using an external identity provider, like +https://www.keycloak.org/[Keycloak] or +https://www.ory.sh/kratos/[Kratos], in conjunction with a +Kubernetes-compatible api gateway, the cost of that solution was too +heavy and not appropriate for the current context. + +The authentication procedure will be handled by +https://github.com/danschultzer/pow[Pow], the authentication library we +*already* use on the web dashboard. We change the underlying credentials +dispatcher, to send the user a JWT access token, with a refresh JWT +token for the refresh procedure. + +=== Consequences + +We remove the old authentication eex templates, served directly by +Phoenix. The single page application will include a login page. The +single page application will handle the access token authentication with +the refresh flow. All the new services will use the JWT authentication +mechanism The web dashboard will be the identity provider of the trento +users. diff --git a/developer/adr/0008-rest-api-versioning-strategy.adoc b/developer/adr/0008-rest-api-versioning-strategy.adoc new file mode 100644 index 00000000..3b25e1d8 --- /dev/null +++ b/developer/adr/0008-rest-api-versioning-strategy.adoc @@ -0,0 +1,61 @@ +== 8. Rest API versioning strategy + +Date: 2023-02-08 + +=== Status + +Accepted + +=== Context + +With the growing complexity of our services, we need to stabilize and +version our external rest API. We need a standard, easy and maintainable +way to deal with versioning and this guideline will affect all the +present and future rest API. This decision affects the purely technical +side, involving the HTTP rest API and the policy about when API should +be upgraded. + +We want a clear API versioning strategy for the API consumer and also a +way to express unambiguously the "`latest`" version of our API. + +=== Decision + +We will implement a URL-based API versioning. Our routes will be +prefixed with `+/vX+`, where "`X`" is a progressive number that +indicates the current version of the consumed API. This means that in +our projects, we need to namespace the handlers, requests and responses +of our http routes. For example in an elixir project, we want to +namespace the controllers, the views and the OpenApi schemas with the +version, using a module called VX (V1, V2 etc..). + +This means that a consumer could choose also the right Schemas for the +OpenApi spec consuming, generating a client according to a specific +version. + +The chosen approach, of URL-based API versioning, comes after a review +of the most used approaches, including HTTP header-based versioning and +mime-type versioning. + +You can read +https://elixirforum.com/t/how-do-you-handle-api-versioning/18898[here] +and +https://www.troyhunt.com/your-api-versioning-is-wrong-which-is/[here] to +have a more comprehensive overview of what we are talking about. + +We will have a "`special`" route, without any prefix, only `+/api+`. +This route will perform a temporary redirect to the latest API version +of our services. + +For API upgrade policy we refer to +https://learn.microsoft.com/en-us/linkedin/shared/breaking-change-policy[LinkedIn +API Breaking Change Policy]. + +=== Consequences + +The rest API will have a /vX prefix, where X is the API version number. +Our OpenAPI specs will be updated to benefit from this type of API +versioning. The rest API will have the special `+/api+` prefix, with a +temporary redirect to the latest API version. + +API version bump policy can be found +https://learn.microsoft.com/en-us/linkedin/shared/breaking-change-policy[here]. diff --git a/developer/adr/0009-frontend-directory-structure-and-architecture.adoc b/developer/adr/0009-frontend-directory-structure-and-architecture.adoc new file mode 100644 index 00000000..abde15fa --- /dev/null +++ b/developer/adr/0009-frontend-directory-structure-and-architecture.adoc @@ -0,0 +1,87 @@ +== 9. Frontend directory structure and architecture + +Date: 2023-05-16 + +=== Status + +Accepted + +=== Context + +Our software project has a frontend that needs to be organized to +facilitate maintenance, scalability, modularity, and ease of navigation. +The current directory structure is outdated and hard to manage. As +software engineers, we propose a new directory structure based on some +criteria. + +We studied https://redux.js.org/faq/code-structure/[Redux’ +documentation] about common patterns for project structures. + +Most of the discussion happened reviewing +https://github.com/trento-project/web/pull/1355[web/#1355]. + +=== Decision + +We propose the following new directory structure for the frontend of our +software project: + +.... +assets/js +├── app.js +├── common +│   ├── Banners +│   ├── Button +│ │   ├── Button.js +│ │   ├── Button.stories.js +│ │   ├── Button.test.js +│   │ └── index.js +│   ├── ListView +│   ├── Modal +│   ├── ObjectTree +│   ├── Table +│   ├── Tags +│   └── Tooltip +├── pages +│   ├── AboutPage +│   ├── ChecksCatalog +│   ├── ClusterDetails +│   ├── DatabaseDetails +│   ├── DatabasesOverview +│   ├── ExecutionResults +│   ├── Health +│   ├── HealthSummary +│   ├── Home +│   └── HostDetails +├── hooks +├── lib +├── state +│   ├── sagas +│ │   ├── clusters.js +│ │   ├── clusters.test.js +│   │ └── index.js +│   ├── selectors +│ │   ├── clusters.js +│ │   ├── clusters.test.js +│   │ └── index.js +│   ├── clusters.js +│   ├── clusters.test.js +└── trento.jsx +.... + +Note that `+/common+` contains truly generic and reusable utilities and +components. + +=== Consequences + +There may be some overhead associated with restructuring the frontend, +such as updating dependencies, fixing references, or rewriting some +parts of the application. However, in the long term, this new directory +structure will ensure an organized and efficient system that can be +easily maintained, scaled, and navigated. The new structure will also +facilitate collaboration between developers as they will have a better +understanding of where everything is located. + +We have one drawback: action creators related to sagas now live along +with slices and action creators generated by redux-toolkit. We found +reasonable to have action creators all stuffed in one specific place, +but it is well understood that it might not be the case for everyone. diff --git a/developer/adr/0010-web-dashboard-directory-structure-and-contexts.adoc b/developer/adr/0010-web-dashboard-directory-structure-and-contexts.adoc new file mode 100644 index 00000000..92dd077b --- /dev/null +++ b/developer/adr/0010-web-dashboard-directory-structure-and-contexts.adoc @@ -0,0 +1,106 @@ +== 10. Web repository directory structure and contexts + +Date: 2023-08-24 + +=== Status + +Accepted + +=== Context + +The web dashboard repository, is a `+phoenix+` application. Right now +the directory structure and the overall `+phoenix+` framework convention +regarding modules and code organization are not respected. With the +increasing complexity of the project and the codebase, we need to switch +to the `+phoenix+` framework standards, to be compatible with code +generators and framework conventions in general. + +The current directory structure is too complex in a lot of aspects, +given the fact that it’s really difficult to follow where the moving +parts are and deciding where to put them, which is becoming even more +difficult with the increasing codebase complexity. + +The web project is also a `+commanded+` application. `+commanded+` has +examples and guidelines on how to structure the projects, the +directories and the modules. This documentation with the official +`+phoenix+` guide will be the base principles for this refactoring. As +software engineers, we propose a new structure/architecture to increase +developers’ productivity, confidence, and obtain adherence to +framework/language standards. + +As a side effect, we will align Wanda and Web to the same standard, +decreasing onboarding length. + +REF + +* https://hexdocs.pm/phoenix/contexts.html +* https://github.com/slashdotdash/segment-challenge/tree/master/lib/segment_challenge/challenges +* https://github.com/slashdotdash/segment-challenge/tree/master/lib/segment_challenge/athletes +* https://github.com/slashdotdash/segment-challenge/tree/master/lib/segment_challenge/infrastructure +* https://github.com/slashdotdash/segment-challenge/tree/master/lib/segment_challenge/stages + +=== Decision + +We propose a directory structure with these characteristics + +* One context for each "`domain entity`" of our application (clusters, +sap_systems, hosts, tags etc..) +* Everything that is tailored to the context in the same directory, +(policies, services, etc..) +* "`integration`"/"`infrastructure`" context, containing all the +interaction outside the domain like rabbitmq, checks engines etc.. (what +we have now in application directory) and commanded cross-aggregate +interactions + +*Example with sap_system context* + +.... +lib + trento + sap_system + commands + events + projections // projectors + readmodels + queries // custom queries + services // like the health service + sap_system.ex // the aggregate + // all other domain entities + sap_system.ex // the context "entrypoint" the "usecase" +.... + +*Example of integration context* + +.... +lib + trento + integration + commanded + event_handlers + process_managers + checks + discovery + grafana + prometheus + telemetry + auth +.... + +The `+trento_web+` directory should contain all the files and contexts +dictated by the `+phoenix+` guide, including mailer and mail templates. + +The module names should be refactored to reflect the directory +structure. This means that we will need an ecto migration, to change +module names in the commanded persistence, to maintain compatibility +between old/new installations + +=== Consequences + +The new directory structure will make easier for current engineers and +new hires, to navigate the codebase and get familiar with it, searching +for support documentation will be easier due to the respected community +standards. + +The phoenix generators will work without manual intervention and future +phoenix updates will be easier, due to the already standard directory +structure. Following the upgrade guide, without translating the +structure to our custom directory structure, will be a sufficient step. diff --git a/developer/adr/0011-sap-system-database-aggregate-split.adoc b/developer/adr/0011-sap-system-database-aggregate-split.adoc new file mode 100644 index 00000000..175d97b0 --- /dev/null +++ b/developer/adr/0011-sap-system-database-aggregate-split.adoc @@ -0,0 +1,83 @@ +== 11. SAP system and database aggregate split + +Date: 2024-04-04 + +=== Status + +Accepted + +=== Context + +In the initial implementation of Trento, the SAP system and database +states are stored in the same `+sap_system.ex+` aggregate. This makes +the assumption that the relationship between a SAP system and a database +is 1 to 1, meaning that a database is only used by one SAP system. This +is not an accurate description of the reality, as a database can handle +multiple SAP systems in a multi-tenant environment. + +In this scenario, when a multi-tenant database is discovered, it only +can manage a unique SAP system and its application instances. Therefore, +a multi-tenant scenario is not supported. + +This limitation must be overcome, as a multi-tenant scenario is pretty +common, and many deployments have them. + +=== Decision + +The currently existing `+sap_system.ex+` aggregate will be split into +seperate modules, creating a new `+database.ex+` aggregate that will +handle databases individually. This way, the lifecycle of a database is +extracted from the lifecycle of the SAP system, making them independent. +Having this, a new 1 to N relationship will be created between the +database and SAP system aggregates. Now the read models are more +precise. From now on, the SAP system ID will be calculated by combining +the database ID and its tenant name, creating a new and unique ID for +each SAP system. + +The relationship between database and SAP systems will continue existing +in many senses, and most of the related events will be handled by event +handlers. For example, when a database is deregistered, or its health +updated, these events will be handled, and once they happen, they will +affect the SAP system aggregate via commands. At the end, both +aggregates continue being pure and connected through this kind of +mechanism. + +All of this forces us to handle previously existing events and aggregate +roll-up snapshots, so most of the events related to a database will be +upcasted. Besides this, some additional exceptions might arise, as SAP +system and database don’t share their ID anymore, but they did in the +past, so previously stored data (as the deregistration process manager) +must be taken into consideration. + +In order to deal with all of these old events, besides upcasting, we +have considered a couple of options when old events and IDs are mixed +up. Initially, +https://hexdocs.pm/commanded/Commanded.Commands.Router.html#module-define-aggregate-identity[prefixing] +the `+sap_system.ex+` aggregate and routing it with a `+sap-system-+` +prefix looked a good option. It removes issues that could arise when SAP +system commands with old IDs, now belonging to databases, are +dispatched, as they are routed with the prefix together with the ID. We +have discarded this option, as it would make the SAP system event stream +the only one different from the others, and it would hide a bit what’s +going on in these scenarios. We decided against this solution, as it +realistically only occours when the `+DeregisterApplicationInstance+` is +dispatched by the deregistration process manager. Therefore, we will +just handle this particular command. The command will be enriched in a +middleware protocol, so commands with IDs, now belonging to databases, +will be ignored. + +=== Consequences + +The new database aggregate will make the handling of a multi-tenant +scenario possible, as the relationship between a database and a SAP +system becomes a 1 to N relationship. Besides this specific scenario, +the code will represent a more precise view of the reality. + +Once the already existing database and SAP systems events are upcasted, +the application should continue working as it was before, and a unique +tenant environment wouldn’t have any difference compared with the old +code structure. + +Additionally, splitting the code makes it easier to read and understand, +as the database logic is separated completely. Finding database related +code will be much easier now on. diff --git a/developer/adr/0012-reactiveness.adoc b/developer/adr/0012-reactiveness.adoc new file mode 100644 index 00000000..7f04eede --- /dev/null +++ b/developer/adr/0012-reactiveness.adoc @@ -0,0 +1,51 @@ +== 12. Reactiveness + +Date: 2024-04-24 + +=== Status + +Accepted + +=== Context + +A strong selling point of Trento is its reactiveness as we mention in +the +https://github.com/trento-project/web?tab=readme-ov-file#reactive-control-plane[main +project’s README]. + +____ +By leveraging modern approaches to software architecture and engineering +and top-notch technologies, we built a reactive system that provides +real-time feedback about the changes in the target infrastructure. +____ + +While this is the underlying nature of the system and how it gets +presented to users, it is also true that not every part of the system +might have the strong need to be reactive. + +=== Decision + +As a general rule of thumb, we decide to make reactiveness of certain +sections of the system a first class citizen when that section is meant +to be used or will very likely be used in +https://en.wikipedia.org/wiki/Kiosk_software[Kiosk mode]. + +=== Consequences + +Not every part of the system will need full blown reactiveness, so +implementation and maintenance is simplified where possible. + +Examples of not totally reactive sections could be: - `+Checks Catalog+` +(ie its content is not expected, at least currently, to change real-time +based on new checks made available on wanda) - `+Settings+` (ie a user +operating in the settings, is not expected to see changes made by +another user in real time) + +Examples of sections where reactiveness is necessary: - `+Dashboard+` - +`+Overviews+` (_Hosts_, _Clusters_, _SAP Systems_, _HANA Databases_) - +`+Details+` (_Host Details_, _Cluster Details_, _SAP System Details_, +_HANA Database Details_) + +In any case, actions having effects on parts of the system where +reactiveness is desired still need to keep supporting the desired +behavior, regardless of the generating point of the action. diff --git a/developer/adr/0013-suma-integration.adoc b/developer/adr/0013-suma-integration.adoc new file mode 100644 index 00000000..4120cf4d --- /dev/null +++ b/developer/adr/0013-suma-integration.adoc @@ -0,0 +1,73 @@ +== 13. Tracking Software updates discovery results + +Date: 2024-05-07 + +=== Status + +Accepted + +=== Context + +Trento features currently building on top of +https://www.suse.com/products/suse-manager/[SUSE Manager] (*SUMA* from +here onwards) integration mainly consist in: - running software updates +discoveries - exposing relevant patches and upgradable packages + +At the moment there is no known reactiveness in SUMA allowing Trento to +be notified about state changes, so Trento proactively triggers software +updates discoveries at relevant moments (ie Host registration, change in +SUMA settings, in a scheduled fashion), potentially changing host +health. + +Information used to determine a discovery outcome is the result of +several requests to SUMA and is the same data useful to be exposed as +relevant patches and upgradable packages. + +At discovery time SUMA is always queried directly in order to get a +fresh representation of its state, however since the same kind +information retrieved during discovery is what at the end has to be +exposed as relevant patches and upgradable packages, we need to decide +how the exposing side of the integration is treated. + +=== Considered Options + +* *Proxied Results Retrieval*: Retrieval of software updates is proxied +to SUMA +* *Tracking Software Updates Discovery results*: Software updates +discovery results are tracked in Trento at discovery time and exposed +via API + +=== Decision + +At the time of writing the following was taken into account as a driver +for the integration + +____ +_Trento must be consistent with itself_: ie if there are patches +affecting a hosts’s health, Trento must provide a consistent information +from different application sections, even if it means being out of sync +with the actual state in SUMA +____ + +As mentioned, software updates discovery results is what we rely on in +order to changing the host’s health, and is also the relevant +information to be exposed by Trento. Retrieving this information from +SUMA in a proxied fashion does not support required consistency within +Trento, because the information gotten at discovery time (the one +affecting host’s health) may differ from the one retrieved later. + +For this reasons we decided to: > track software updates discovery +results at discovery time and expose the tracked results via Trento API + +=== Consequences + +The depicted decision has the following effects + +* we keep consistency within Trento as what is tracked at discovery time +is also what is exposed as hosts’ available software updates +* we achieve a higher degree of decoupling from SUMA by having Trento +API deal with tracked software updates results +* we avoid introducing side effects in read operations +* we limit the amount of potentially unnecessary requests to SUMA +* relevant patches and upgradable packages overviews also benefit, by +relying on the same data tracked at discovery time diff --git a/developer/adr/0014-decoupling-of-trento-checks-from-wanda.adoc b/developer/adr/0014-decoupling-of-trento-checks-from-wanda.adoc new file mode 100644 index 00000000..a77ba2d4 --- /dev/null +++ b/developer/adr/0014-decoupling-of-trento-checks-from-wanda.adoc @@ -0,0 +1,53 @@ +== 14. Decoupling of Trento checks from Wanda + +Date: 2024-08-06 + +=== Status + +Accepted + +=== Context + +Trento includes a component called Wanda that is responsible for +performing various configuration checks on the system. + +Currently, the configuration checks used by Trento are bundled with the +Wanda component, which means that any updates to the checks require a +full update of the Trento product. This can lead to delays in users +receiving the latest checks, as they have to wait for a new Trento +release. + +=== Decision + +We have decided to modify the Trento architecture to read the +configuration checks from a directory on the filesystem instead of +bundling them with the Wanda component. This will allow the checks to be +updated independently of the core Trento system, enabling users to +consume check updates faster. + +On traditional setups the checks will be shipped through an RPM package. +On Kubernetes-based setups (and generally setups that leverage +containers) the checks will be shipped through a container image that +will be instantiated as a sidecar container: the sidecar container +itself will copy the checks onto a shared volume from which Wanda will +be able to read them. + +=== Consequences + +* Users will be able to receive updates to the configuration checks more +quickly, without waiting for a full Trento release. +* The development and maintenance of the checks can be decoupled from +the core Trento system. +* Users (as in system administrators) will be able to write their own +checks and load them into Trento. + +=== Alternatives Considered + +* Continue Bundling Checks with Wanda: This would maintain the current +architecture, but would not address the issue of delays in users +receiving check updates. +* Ship the checks as an OCI artifact directly downloaded by Wanda using +an embedded OCI client library: this would lead to additional complexity +in Wanda’s codebase. It’s feasible, but the team decided for a lower +cost solution for the moment to address this need. The cost of this +additional complexity would have been way bigger than the return. diff --git a/developer/adr/0015-activity-logging.adoc b/developer/adr/0015-activity-logging.adoc new file mode 100644 index 00000000..ce1adeb1 --- /dev/null +++ b/developer/adr/0015-activity-logging.adoc @@ -0,0 +1,77 @@ +== 15. Activity Logging + +Date: 2024-10-02 + +=== Status + +Accepted + +=== Context + +Trento feature-set is growing and not limited to only reading operations +but expanding to applying state changes of different natures. + +Such activities may be of huge impact and visibility on them is a great +asset to troubleshooting incidents, determining root causes of issues +and it improving transparency of the system in general. + +Additionally the possibility to correlate actions/events in order to +build a history carrying cause-effects links, enhances visibility by +supporting a sense making about chain of events and their outcomes. + +With this in mind logging activities and their cause-effect relationship +becomes a much valuable feature for Trento users. + +=== Considered Options + +* *Change Data Capture/Write ahead Log (CDC/WAL)*: listen to the +Postgres streaming replication log for certain tables, and do something +when a relevant db operation happens +* *https://opentelemetry.io/[Open Telemetry] integration*: leverage OTEL +libraries for automatic data collection and a +https://github.com/quickwit-oss/quickwit[third party search engine] for +searching on collected data +* *Nimble bespoke instrumentation*: instrumenting the code to capture +only what of interest and appending entries into a new local postgres +table + +=== Decision + +A CDC/WAL approach requires by definition that the relevant activities +hit the database with a _write_, however that is not the case for all +the possibly interesting activities (ie. a login attempt, requesting +checks execution), hence this is not a viable option unless we introduce +changes forcing such database writes to happen. + +Integration of OTEL and its almost automatic instrumentation turned out +to be quite convenient with regards to data collection, however it +demands the introduction of a third party tool to enable searching +capabilities on collected data. The overhead the introduction of this +extra piece of software adds to the release process led us to deciding +not to pursue this approach. + +For these reasons we decided to implement a bespoke yet nimble mechanism +to log relevant activities into a dedicated table in the already present +postgres instance. + +Currently, interesting loggable activities are: - cartain http requests +made to relevant trento’s APIs - all domain events emitted by the system + +and we will use specific mechanism to capture them with regards to their +nature - a plug/middleware will forward all http requests to an activity +logger that will decide whether to log something or not - an event +listener will listen to all emitted events and delegate to an activity +logger appending the log entry to the persistence layer + +=== Consequences + +The depicted decision has the following effects: - the footprint of the +feature rollout is kept at minimum. No extra deps are needed - changes +to already existent modules/code is near to zero - we can leverage +postgres searching capabilities + +Tradeoffs: - number of writes to the database are amplified for relevant +activities because of insertions and reindexing - the size of the +activity log table depends on the number of logged activities combined +with the retention time set by the user - Activity log context +references web related modules in order to match controller/action pairs diff --git a/developer/adr/0016-discarded-discovery-events.adoc b/developer/adr/0016-discarded-discovery-events.adoc new file mode 100644 index 00000000..e4527e05 --- /dev/null +++ b/developer/adr/0016-discarded-discovery-events.adoc @@ -0,0 +1,50 @@ +== 16. Discarded Discovery Events + +Date: 2024-10-15 + +=== Status + +Accepted + +=== Context + +The discovery workflow allows Trento Agent instances to publish their +host system’s information to the Trento Web’s `+collect+` API operation. +Once accepted, the event results in one or more commands that are +dispatched to the ES/CQRS system. Responsibilities are separated between +the publisher (Trento Agent) and the collector (Trento Web): Trento +Agent’s job is done once the payload has been received and stored. + +There are cases in which a discovery sent from a Trento Agent is not +processed; they can be categorized as: 1. The Trento Agent fails to +connect to the Trento Web instance (network issues); 2. The Trento Agent +fails to authenticate with the Trento Web instance (authentication +issues); 3. The request payload does not match the expected shape, thus +it cannot be mapped to a command (API contract issues); 4. The discovery +is rejected because of the current state in Trento. + +Currently all these scenarios result in an error being returned by the +`+collect+` endpoint and subsequent log in trento agent. + +=== Decision + +Failures that depend on the current Trento data (4) are not reported as +errors to the publishing Trento Agent; instead, it will receive a +success status code indicating that the event has been correctly +ingested by Trento Web, even if it won’t be processed. This is +legitimated by the periodicity of the discoveries that would lead Trento +to eventually converge to a consistent state. + +Such events, alongside the ones that fall into (3), are traced into the +`+discarded_discovery_events+` database table for inspection and +troubleshooting. + +The decision of whether API contract issues (3) should be treated the +same way, has been postponed. + +=== Consequences + +* The noise in the application logs of Trento Agents is reduced as there +will be fewer entries that have little value for the host administrator. +* Users will have knowledge of the discarded event only by looking at +the `+discarded_discovery_events+` database table. diff --git a/developer/adr/0018-agent-operations-orchestration.adoc b/developer/adr/0018-agent-operations-orchestration.adoc new file mode 100644 index 00000000..cb626d6d --- /dev/null +++ b/developer/adr/0018-agent-operations-orchestration.adoc @@ -0,0 +1,178 @@ +== 18. Agent operations orchestration + +Date: 2025-01-28 + +=== Status + +Accepted + +=== Context + +Follow up of ADR +https://github.com/trento-project/docs/blob/main/adr/0017-agent-operations-framework.md[17. +Agent Operations Framework] which describes the actions to implement +write operations in Trento agents. In this case, this ADR describes the +orchestration layer, which takes care of dispatching all the operation +commands to the agents. + +The framework must be able to orchestrate multi-agent and multi-step +operations, as many operations are complex, including multiple clustered +targets and actions that must be sequential in time. It must be able of +sending the operator actions to the agents and wait for the report. Once +the report is received, decide to move on with the next operation steps +or stop the operation as failed. + +=== Decisions + +The operations framework orchestrator will be implemented in Wanda, as +it already has direct connection to the agents using RabbitMQ. Likewise, +it already includes some "`must have`" features like access to a +persistent database, HTTP endpoints to expose stored information and +additional code utilities which makes this feature easy to add. + +We have chosen to use Elixir +https://hexdocs.pm/elixir/1.13/DynamicSupervisor.html[Dynamic +Supervisors] to start and handle the operation requests dynamically. +This means that the operations are short-lived processes responsible for +handling individual operation requests. We already do the same with +check executions with a successful outcome. + +The process is able to: - Decide which agent will receive the operation +(from now on: target agents) - Dispatch the individual operator requests +to the target agents - Wait for the reports and compute the overall step +result after receiving the feedback from all the targets - Move to the +next operation step once the previous one is successfully completed - +Save the intermediate and final state of the operation as an historic +record + +At the end, all the orchestration is defined as a state machine that +moves the current state through different stages. + +==== Operation + +An operation is a multi-step process, where each step defines the +operation to be executed in the agents. The steps are executed +sequentially, but the step execution is requested in parallel to all +involved agents. Besides this, it includes some additional metadata to +describe the operation itself. Find here a dummy example: + +.... +%Wanda.Operations.Catalog.Operation{ + id: "testoperation@v1", + name: "Test operation", + description: """ + A test operation. + """, + required_args: ["arg"], + steps: [ + %Wanda.Operations.Catalog.Step{ + name: "First step", + operator: "operatior1@v1", + predicate: "*" + }, + %Wanda.Operations.Catalog.Step{ + name: "Second step", + operator: "operatior2@v1", + predicate: "isDC == true" + } + ] +} +.... + +As commented, the steps define the operator to be executed in the +agents. Check the +https://github.com/trento-project/docs/blob/main/adr/0017-agent-operations-framework.md[Agent +related ADR] for more information. The `+required_args+` field defines +the arguments that must be provided from the outside with a key/value +format. For example: `+saptune_solution=HANA+`. They will be passed to +the agent operators so they can be executed. + +==== Orchestration + +The operations orchestration is composed only by 2 states: - Dispatch +the step operations to target agents - Wait until all the target agents +have reported back + +This image represents the states and transitions: + +[source,mermaid] +---- + graph TD; + A[Start operation]-->B{Steps left to run?}; + B--Yes-->C[Start operation step]-->H[Select target agents with predicate] + H-->J[Dispatch step operation request to target agents]; + B--No-->D[Evaluate reports and finish]; + J-->K[Wait for target agent reports]-->E[Receive target agent report]; + E-->F{All target agents reported?} + F--No-->K + F--Yes-->G{Some target agent operation failed in step?} + G--No-->B + G--Yes-->D +---- + +==== Predicate + +Deciding if a step operation must be executed in some agents or not is +something really useful. This will let us define some multi-agent +operations where some step operations are only executed in certain +agents. Some examples could be: - Run cluster operations only in DC +nodes - Run SAP operations only in nodes with specific features, like +ASCS or PAS nodes - Exclude running operations in nodes like majority +makers + +The predicate will be a simple RHAI expression that returns a boolean. +We will pass some agent specific values in the requests, so each agent +has its own characteristics. With these values, we will run the RHAI +evaluation in Wanda and decide if the operation step must be executed in +this agent or not. + +==== Operations registry + +The operations to be executed will be stored in a registry, in Wanda +codebase. Unlike the checks executions, where the checks are implemented +using YAML files and a specific DSL, in this case the operations will be +hardcoded in the codebase, and there won’t be any capability to upload +them after releasing the code. We want to have control over want to +implement, as the operations are really sensitive actions that could +lead to malfunctions in the system if implemented incorrectly. The +registry simply implements a basic map with some unique operation +identifiers pointing to operations described in the +link:#operation[Operation] chapter. + +==== Out of scope in the first implementation + +To make the delivery of this feature easier and faster, we have decided +to let out some of the initial features we wanted to implement: - +Multi-step operations rollback. When the operation implements a lot of +steps, returning to the original state in case of failure has some +challenges as all intermediate steps must be rolled back in reverse +other. - Restart of operations with abnormal or unknown exit due the +code crashes. We will "`let it fail`" by now, as in the majority of the +cases, the error will be caused for things that will hardly be fixed in +a subsequent execution. These can be things like: database access error, +error publishing or reading from RabbitMQ, agent reporting an incorrect +payload, internal state got corrupted, etc Additional rationale about +postponing this implementation is that the first few operations we will +implement (saptune solution apply, cluster maintenance on/off, SAP +application layer instance start/stop) won’t harm the target agents if +the operation is not resumed. If the operation request arrives to the +agent, it will perform it in an atomic transactional change, so no +matter what, the initial request done by the user will be effective. The +worst thing that can happen is that the internal saved state of the +operation might not match which what happened in the agent, but we are +fine accepting this as an initial trade-off. As an effect, this makes +the orchestrator not 100% reliable between what Wanda saved in its +database and what happened in the target agents. Either way, once a we +start implementing a multi-step rollback feature we will resume the task +of implementing proper restarts. + +=== Consequences + +Implementing the agent operations framework with this design enables us +the following, so we can…: - Implement multi-agent and multi-step +operations - Use a reliable self-implement Agent operators that we are +sure they do exactly what we want and are tested - Decide if the +operation step is executed only in certain targets using a deterministic +predicate - Have a controlled and consistent operations registry - Save +all the executed operations and have a historical record - In the +future, rollback complex multi-step operations to the initial state diff --git a/developer/adr/0019-e2e-testing-practices.adoc b/developer/adr/0019-e2e-testing-practices.adoc new file mode 100644 index 00000000..8d4914e8 --- /dev/null +++ b/developer/adr/0019-e2e-testing-practices.adoc @@ -0,0 +1,119 @@ +== 19. E2E Testing Practices & Page Object Implementation + +Date: 2025-04-25 + +=== Status: + +Accepted + +=== Context + +This document outlines the identified challenges within our current +Cypress End-to-End (E2E) test suite and the strategic decisions made to +address these issues. The goal is to improve the maintainability, +readability, robustness, and overall efficiency of our E2E testing +efforts. + +Our existing E2E test suite, while providing valuable coverage, suffers +from several key limitations: + +* *Significant code duplication*: Repetitive code across test files and +scenarios complicates maintenance and increases the test suite size. +* *Lack of test organization and readability*: The absence of a clear +organizational structure and natural language-based naming conventions +makes it difficult for team members to understand the test suite’s scope +and purpose. +* *Limited code reusability*: Inefficient reuse of test logic, helper +functions, and interaction patterns contributes to code duplication and +increases test creation effort. +* *Suboptimal element interaction*: Over-reliance on generic Cypress +utilities for element selection can lead to less resilient and +harder-to-understand selectors. +* *Embeded UI interaction logic in test files*: This tightly couples +tests to the UI, reducing maintainability and readability, and hindering +the isolation of UI interactions and business logic. +* *Reduced test isolation*: Lack of true test independence makes +debugging challenging as failures can cascade and produce misleading +results. +* *Reduced test robustness*: Using `+contains+` assertions can lead to +false positives and provides limited debugging information upon failure, +making it harder to pinpoint the actual issue. + +=== Decisions + +To overcome these challenges, we have decided to implement the following +strategies: + +* *Adoption of the Page object model (POM)*: We will implement the POM +to encapsulate all UI interaction logic and data within dedicated page +object modules. Test files will then utilize concise, descriptive +methods from these page objects, focusing solely on the test steps. + +This implementation will use JS modules (instead of classes) as distinct +object instances are not required. Each module will represent a specific +application page. Common utility methods will reside in a base page +object module, accessible to all other page-specific modules through +import/export. + +Refs: - +https://dev.to/aswani25/implementing-the-page-object-model-pom-with-cypress-a-step-by-step-guide-5c2i[Page +Object Model with Cypress] Although this explanation uses classes +instead of modules, the underlying concept remains the same. + +* *Implementation of custom selectors*: We will prioritize the +development of custom, robust selectors over exclusive reliance on +Cypress’s built-in tools. This will lead to more resilient and readable +test methods, often reducing the need for complex DOM traversal within +the tests. ++ +Examples: +[arabic] +. To get the number of subscriptions in the "`About`" page. Instead of: +`+.leading-5+` use `+div:contains('SAP subscriptions') + div+` In this +case `+++` works as cypress `+.next()+`. By using this the selector is +more informative about what is going to be checked and removes the usage +of cypress built it commands to navigate the DOM +. To get the passing hosts in the "`Hosts`" page. Instead of: +`+[data-testid='health-box-passing-not-selected'] > .rounded > .flex > .font-semibold+` +use `+p:contains('Passing') + p+` The same happens in this case and also +we avoid using data-testid, which is something agreed to stop using. +. To get the first data row of a table. Instead of +`+cy.get('td').eq(1)+` use `+cy.get('td:eq(1)')+` +. To get some cell based on some data in the row and its index. + +Use `+tr:contains('hana_cluster_1') td:eq(1)+` we could even create a +function here to get the index of the header we want by its text and +then use that index to get the data cell we want. That way if at some +point that column is placed somewhere else the test would be resilient +to that change and still would be testing what is needed. +. To get SUSE Multilinux Manager Settings button in settings page. + +We can use the title of the div section to use it as a reference for the +element we want. +`+div[class*='container max']:contains('SUSE Multi-Linux Manbager Config') button:contains('Edit Settings')+` +* *Ensuring test isolation*: Each test scenario will be provided with +the necessary pre-conditions to ensure its independence. This will +prevent failures in one test from affecting others, significantly +simplifying the debugging process. +* *Utilizing precise assertions*: We will favor specific assertions +(when possible) like `+have.text+` over more general ones like +`+contains+`. These precise assertions provide detailed feedback +(expected vs. actual values) in failure logs, enabling faster and more +accurate identification of the root cause. + +=== Consequences + +The implementation of these decisions is expected to yield the following +benefits: + +* *Improved maintainability*: Reduced code duplication and a +well-defined structure will make the test suite easier to update and +manage. +* *Enhanced readability*: Clear separation of concerns and descriptive +test methods will improve the understandability of the tests for all +team members. +* *Increased reusability*: The POM and custom selectors will promote the +reuse of code components, saving development time and effort. +* *Greater robustness*: More specific selectors and assertion strategies +will make the tests less susceptible to minor UI changes and provide +more accurate feedback. +* *Simplified debugging*: Test isolation will significantly streamline +the process of identifying and resolving test failures. diff --git a/developer/adr/0020-checks-customization.adoc b/developer/adr/0020-checks-customization.adoc new file mode 100644 index 00000000..480fd89d --- /dev/null +++ b/developer/adr/0020-checks-customization.adoc @@ -0,0 +1,126 @@ +== 20. Checks Customization + +Date: 2025-04-30 + +=== Status + +Accepted + +=== Context + +Trento provides a catalog of checks with predefined expected values, +often based on SUSE’s best practices. However, specific customer +environments may necessitate deviations from these defaults for certain +checks to accurately reflect their operational reality or specific +configurations. + +Therefore, a mechanism is needed to - allow users to override the +built-in expected values of specific checks for particular targets - +have these overrides used during check executions - track changes in the +activity log - allow to opt-out from customizability for either entire +checks or specific values within a check + +=== Considered Options + +* *Storing Customizations in Web* +** places check-related data outside the Check Engine, making standalone +engine usage clunky +** requires passing custom values for every execution, relieving wanda +from customization state, yet complicating the execution request process +(message contracts/API endpoints) +** managing consistency between check definitions (in Wanda) and +customizations (in Web) becomes more complex, especially when check +specifications change or checks are added/removed +** logging customizations is simple as the activity logging subsystem is +embedded in web +* *Storing Customizations in Wanda* +** keeps check-related logic and data consolidated within the Check +Engine +** allows for a more straightforward execution process, as Wanda can +directly access its state for custom values +** managing consistency upon check specifications change or checks are +added/removed is simpler because all the needed information is in the +same place +** logging customizations needs the activity logging subsystem to be +extended to support message based activities + +Additional APIs and work to ensure that custom values are correctly +applied during execution is needed in any case. + +=== Decision + +We will introduce the checks customization capabilities to Wanda in +order to - keep cohesive and consistent responsibility about check +related actions and data - keep execution process simple and +straightforward also in a standalone engine usage - allow for simpler +management of check specifications changes and check additions/removals + +==== Checks Customizability + +A check is deemed "`customizable`" if its definition includes a +non-empty `+values+` section. + +Checks using hardcoded expectation values will need refactoring to use +the `+values+` structure to become customizable. + +For enhanced flexibility, certain checks may be explicitly excluded from +customization as well as specific values within a generally customizable +check. It can be because of a strong policy about some values or because +the nature of the check itself does not allow for customization. + +This will be defined in the check specification via the following entry +`+customization_disabled: true+` + +==== Operations + +Wanda will expose API endpoints to manage custom values for a given +check and target: - retrieving the selectable checks for a given target. +This will expose relevant check information in addition to the +customization status - applying, changing, or resetting custom values +for a specific check and target + +Notes: - Wanda will need to enforce authorization for customization +operations by checking abilities embedded within the JWT provided by +Web, which currently is also the auth server - the catalog endpoint +remains unchanged. It will not expose the customization status of +checks, although it will expose the customizability status of the check +itself as it’s defined in the specification + +==== Execution + +During checks execution, Wanda will query its state for any custom +values applicable to the check and the specific target being evaluated. + +If custom values exist, they will be used instead of the built-in values +defined in the specification. + +Additionally, the specific custom values used during a check execution +must be snapshotted and stored alongside the execution results to ensure +accurate reporting. + +==== Activity Logging + +Customization actions are significant events and need to be logged in +the central Activity Logging subsystem embedded in web. + +Since these actions are exposed by wanda we will have it expose relevant +messages carrying also the information about the user performing the +operations. + +On the other hand the activity logging subsystem in web will be extended +to support message based activities. + +=== Consequences + +The depicted decision has the following effects: - *Increased +Flexibility:* users gain the ability to tailor check expectations to +their specific environment needs, improving the relevance and accuracy +of compliance results - *Consolidated Logic:* keeping customization +logic and data within Wanda aligns with its responsibility as the Check +Engine - *Simpler sync management*: check specifications changes and +check additions/removals can be responded to in order to keep the +customization data in sync - *Enhanced Activity Logging*: the activity +logging subsystem supports cross components actions logging - *Enhanced +Contracts*: messages will support the ability to carry the information +about the user performing the operation, allowing for better tracking +and auditing of actions taken within the system diff --git a/developer/adr/README.adoc b/developer/adr/README.adoc new file mode 100644 index 00000000..6b3555b8 --- /dev/null +++ b/developer/adr/README.adoc @@ -0,0 +1,40 @@ +== ADR + +Trento’s ADR repository contains the Architecture Decision Records (ADR) +for the project. Please refer to the ADR GitHub page and this article +for more info about Architecture Decision Records. + +=== How to contribute + +==== Install ASDF and the ADR plugin + +Follow the instructions in the +https://asdf-vm.com/#/core-manage-asdf-vm[ASDF documentation] to install +ASDF. + +Then, install the https://github.com/npryce/adr-tools[adr-tools plugin] + +==== Create a new ADR + +Run the following command to create a new ADR: + +[source,bash] +---- +adr new "Wanda is the best rabbit pet" +---- + +==== Supersede an ADR + +Run the following command to supersede an ADR: + +[source,bash] +---- +# supersede the ADR 0009 +adr new -s 9 new "Tonio is the best rabbit pet" +---- + +=== Resources + +* https://adr.github.io/[Architecture Decision Records] +* https://engineering.atspotify.com/2020/04/when-should-i-write-an-architecture-decision-record/[Spotify +Blog post about ADR] diff --git a/developer/architecture/trento-architecture.adoc b/developer/architecture/trento-architecture.adoc new file mode 100644 index 00000000..0af240fe --- /dev/null +++ b/developer/architecture/trento-architecture.adoc @@ -0,0 +1,96 @@ +== Trento Architecture + +Here’s a high level view of what we do to achieve the result of +providing a reactive event driven system. + +.Trento Architecture +image::trento-architecture.png[Trento Architecture] + +=== Discovery + +https://github.com/trento-project/agent[Trento Agent] extracts relevant +information on the target infrastructure and publishes those to the +control plane. + +_(cluster discoveries, host discoveries, cloud discoveries…)_ + +The control plane Discovery integration securely accepts published +discoveries, stores them and triggers Scenario Detection. + +''''' + +=== Scenario Detection + +The https://github.com/trento-project/web[control plane] discovery +integration leverages specific policies to determine the scenario to be +triggered based on the discovered information. + +That means we need to be able to determine which command to dispatch in +the application. _(RegisterHost, RegisterDatabaseInstance and so +forth…)_ + +''''' + +=== Business rule validation + +The detected scenario dispatches the needed command(s) which trigger +validation of the requested action(s) against the current state and the +proper business rules for the usecase. + +''''' + +=== Events & State change + +Going through the previous steps of dispatching and action in the system +and applying business rules to the process, translates in things +actually happening (or not) and changing some state (or not). + +When things happen we represent them as *events* +(_SAPSystemHealthChanged, ChecksExecutioncompleted_), we store them in +an append only *Event Store* and use them as the source of truth of the +system (or part of it). + +See Event Sourcing +https://martinfowler.com/eaaDev/EventSourcing.html[here] + +''''' + +=== State Propagation & Reaction + +Once things happen the system notifies the world about it by emitting +the recorded events so that interested components can listen for those +and react accordingly. + +Reaction may be any orthogonal listener responsible to deliver part of +the feature being served. + +Some of possible _reactions_ - projecting read optimized models for +specific usecases (_Clusters, Hosts, Heartbeats_) maybe later served via +APIs - Sending and email _whenever a SAP System’s health goes critical_ +- Broadcasting changes to the Reactive UI via websockets - Third party +software integration - … — + +=== Checks Execution + +The Checks Execution is managed by the +https://github.com/trento-project/wanda[Checks Engine]. It uses the +https://github.com/trento-project/checks[Trento configuration checks +catalog], which consists of `+yaml+` files. Each check includes a script +written in https://rhai.rs/[Rhai] which is what the check tries to +evaluate with some facts coming from the targets. + +This component is responsible of: - Listening for checks execution +requests coming from the control plane (or any other external user) - +Sending the fact gathering requests to the appropriate targets - +Evaluating check results using the gathered facts - Publishing the +evaluated results - Storing and exposing the executed executions + +=== General Considerations + +https://github.com/commanded/commanded[CQRS with Commanded] supports +complex domain modeling, however, whenever possible we use simpler +implementations for non-critical aspects (_Tagging_ for instance, is a +basic CRUD operation). + +Find here a simple diagram to understand how CQRS works: + +image::event-sourcing-cqrs.png[ES+CQRS] diff --git a/developer/assets/event-sourcing-cqrs.png b/developer/assets/event-sourcing-cqrs.png new file mode 100644 index 00000000..a3a94e46 Binary files /dev/null and b/developer/assets/event-sourcing-cqrs.png differ diff --git a/developer/assets/trento-architecture.drawio b/developer/assets/trento-architecture.drawio new file mode 100644 index 00000000..b5dbe548 --- /dev/null +++ b/developer/assets/trento-architecture.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/developer/assets/trento-architecture.png b/developer/assets/trento-architecture.png new file mode 100644 index 00000000..592d8d5f Binary files /dev/null and b/developer/assets/trento-architecture.png differ diff --git a/developer/coding-standarts/README.adoc b/developer/coding-standarts/README.adoc new file mode 100644 index 00000000..20e52df9 --- /dev/null +++ b/developer/coding-standarts/README.adoc @@ -0,0 +1,26 @@ +== Coding standards and best practices + +=== Pull requests + +* Organize your work in small, self-contained, and well-documented +commits. +* Rewrite your branch history to make it easy to review your changes. +This includes squashing fixup commits, rebasing on top of the latest +master, and rewriting commits to make them self-contained. +* Use https://github.com/tummychow/git-absorb[git absorb] to +automatically create and squash fixup commits. +* Use the +https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-request-merges#squash-and-merge-your-pull-request-commits[squash +and merge] option to merge your PR. + +==== Reviews + +Please refer to Google’s +https://google.github.io/eng-practices/review/[Code Review Developer +Guide] on how to write/receive a code review. + +=== Language-specific guidelines + +* link:./elixir.md[Elixir] +* link:./go.md[Go] +* link:./javascript.md[JS] diff --git a/developer/coding-standarts/elixir.adoc b/developer/coding-standarts/elixir.adoc new file mode 100644 index 00000000..45dc9c5a --- /dev/null +++ b/developer/coding-standarts/elixir.adoc @@ -0,0 +1,148 @@ +== Elixir coding standards + +=== Style Guide + +Trento follows the https://github.com/rrrene/elixir-style-guide[Credo +style guide]. + +==== Linting + +The linting is enforced by the https://github.com/rrrene/credo[credo +tool] locally and in the CI. + +Use the link:../templates/.credo.exs[.credo.exs template] as a starting +point for your project. + +==== Additional rules and exceptions + +* Private functions must appear after public functions in order of +usage. + +=== Static analysis + +https://github.com/jeremyjh/dialyxir[Dialyzer] must be used to check for +type correctness. + +Please write specs `+@spec+` tags for all public functions and typespecs +for defined types, to help Dialyzer doings its job. + +=== Code smells + +The https://github.com/lucasvegi/Elixir-Code-Smells[Catalog of +Elixir-specific Code Smells] is a good reference for common code smells +in Elixir. + +=== CI + +Please use the link:../templates/elixir-ci.yaml[CI template] as a +starting point for your project. + +=== Documentation + +Use https://github.com/elixir-lang/ex_doc[ExDoc] to generate the +documentation for the project. + +Please add relevant documentation to `+@moduledoc+` and `+@doc+` +attributes and make sure to run `+mix docs+` to check the generated +documentation before submitting a PR. + +Repositories that don’t publish a package to Hex, should publish the +generated documentation to GitHub pages using the +link:../templates/elixir-ci.yaml[generate-elixir-docs action]. + +=== Testing + +Testing guidelines: + +* Read https://pragprog.com/titles/lmelixir/testing-elixir/[Testing +Elixir]. +* Prefer https://github.com/thoughtbot/ex_machina[factories] over +fixtures to create test data. +* Hook to https://hexdocs.pm/telemetry/readme.html[Telemetry] events of +3rd party packages to synchronize the test code. Find +https://elixirforum.com/t/testing-and-telemetry-events-how-to-test-if-they-are-sent/28273/5[here] +a generic example and +https://github.com/trento-project/wanda/pull/180[here] another example +based on AMQP connections. +* https://dashbit.co/blog/mocks-and-explicit-contracts[Mocks and +explicit contracts] +* https://groups.google.com/g/elixir-ecto/c/BKpLf092dWs/m/VaCvfZpEBQAJ[Controller +tests as integration tests] (_pre-Phoenix Context_ but still relevant) + +==== Test coverage + +Test coverage is enforced by https://coveralls.io/[coveralls.io]. Please +add https://github.com/parroty/excoveralls[excoveralls] to the list of +dependencies and setup `+mix.exs+` file as shown here: + +[source,elixir] +---- +def project do + [ + app: :yourapp, + version: "1.0.0", + elixir: "~> 1.0.0", + deps: deps(), + test_coverage: [tool: ExCoveralls], + preferred_cli_env: [ + coveralls: :test, + "coveralls.github": :test + ] + ] +end + +defp deps do + [ + {:excoveralls, "~> 0.10", only: :test} + ] +end + +# If you have a custom mix task you can override the coveralls.github task +defp aliases do + [ + "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], + "ecto.reset": ["ecto.drop", "ecto.setup"], + test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], + "coveralls.github": ["ecto.create --quiet", "ecto.migrate --quiet", "coveralls.github"] + ] +end +---- + +The link:../templates/elixir-ci.yaml#131[CI template] includes a step to +upload the coverage report to coveralls.io. + +Please refer to the +https://docs.coveralls.io/coveralls-notifications[coveralls notification +documentation] to allow coveralls to post comments with the coverage +report in the PR. + +=== Phoenix + +Guidelines for applications using Phoenix: + +* Use the https://hexdocs.pm/phoenix/overview.html[Phoenix +documentation] as a starting point for your project. +* Always refer to the https://hexdocs.pm/ecto/Ecto.html[Ecto +documentation]. +* Start with +https://hexdocs.pm/phoenix/contexts.html#starting-with-generators[generators] +when possible, as they give a reference for the directory structure and +naming. +* Use +https://github.com/gothinkster/elixir-phoenix-realworld-example-app[realworld +example app] as a reference for the directory structure, naming and code +organization in general. +* Instead of https://hexdocs.pm/phoenix/routing.html#path-helpers[Router +Path helpers], prefer using the full path in the tests +(e.g. `+/api/rabbits+`) to test that the route is correct. +* Document APIs using +https://github.com/open-api-spex/open_api_spex[OpenAPI], cast and +validate operations in controllers using the provided +https://github.com/open-api-spex/open_api_spex#validating-and-casting-params[plug], +and test controllers using +https://github.com/open-api-spex/open_api_spex#validate-responses[OpenAPISpex.TestAssertions]. +* The link:../templates/elixir-ci.yaml[CI template] includes a step to +generate the Swagger UI and publish it to GitHub pages. Please refer to +https://github.com/open-api-spex/open_api_spex/pull/489[this pr] to +configure the `+ApiSpec+` module so that it does not depend on a running +`+Endpoint+` when generating the `+openapi.json+` file. diff --git a/developer/coding-standarts/go.adoc b/developer/coding-standarts/go.adoc new file mode 100644 index 00000000..e926f1c4 --- /dev/null +++ b/developer/coding-standarts/go.adoc @@ -0,0 +1,71 @@ +== Golang coding standards + +=== Linting + +Linting is enforced by https://golangci-lint.run[golangci-lint] locally +and in the CI pipeline. Use the +link:../templates/.golangci.yaml[.golangci.yaml template] as a starting +point for your project. + +=== Documentation + +Golang packages hosted in Github are automatically published in +https://pkg.go.dev/[pkg.go.dev] page. Include +https://github.com/marketplace/actions/ek-godoc-action[essentialkaos/godoc-action] +job usage in the CI in order to speed up the publishing time. + +Please add relevant documentation to the implemented functions (at +least, the public ones) adding a docstring on top of the function as +shown here: + +.... +// MyFunction runs some desired action +func MyFunction(value string) (string, error) { +... +.... + +=== Testing + +The testing is done using the standard +https://pkg.go.dev/testing[testing] library. + +As generic guidelines: + +* Use https://pkg.go.dev/github.com/stretchr/testify[testify] package +methods as assertions. +* Use “_test” package name for the test file. This enforces the explicit +import of the package under test making it usage closer to a real user. +* Prefer test +https://pkg.go.dev/github.com/stretchr/testify/suite[suite] usage as it +groups multiples tests, enabling setup and teardown functions and +improving the test output logs. +* Avoid if possible global variables usage to enable mocking in tests. +Implement dependency injection as much as possible, as this enforces +good code design decisions and improves testability. + +==== Test coverage + +Test coverage is enforced by https://coveralls.io/[coveralls.io]. Please +use link:github.com/mattn/goveralls[goveralls] util in the CI as shown +here: + +.... + test: + runs-on: ubuntu-24.04 + ... + + steps: + ... + - name: test + run: make test-coverage + - name: install goveralls + run: go install github.com/mattn/goveralls@latest + - name: send coverage + env: + COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: goveralls -coverprofile=covprofile -service=github +.... + +To enable the `+covprofile+` file creation, add +`+-covermode atomic -coverprofile=covprofile+` to the `+go test+` +command execution. diff --git a/developer/coding-standarts/javascript.adoc b/developer/coding-standarts/javascript.adoc new file mode 100644 index 00000000..5b27a9a7 --- /dev/null +++ b/developer/coding-standarts/javascript.adoc @@ -0,0 +1,76 @@ +== JavaScript coding standards + +=== Style Guide + +Trento follows the https://github.com/airbnb/javascript[Airbnb style +guide], wherever possible. + +=== Linting + +The linting is enforced through https://eslint.org/[ESLint] locally and +in the CI. + +Use the link:../templates/.eslintrc.js[.eslintrc.js template] as a +starting point for a new project. + +=== Formatting + +Code formatting is applied using https://prettier.io/[Prettier], and +enforced with a CI check. + +Use the link:../templates/.prettierrc.js[.prettierrc.js template] as a +starting point for a new project. + +==== Additional rules and exceptions + +* Always prefer an arrow function (`+() => {}+`) to a regular +`+function(){}+`. It has lower memory footprint and it’s more concise. +* PropTypes are being avoided for the moment when writing a new React +component. + +==== `+npm+`, `+yarn+`, `+pnpm+`? + +`+npm+`. + +=== Testing + +End-to-end tests in production-like environments are performed using +https://www.cypress.io/[Cypress]. + +Unit testing of JavaScript code and React components is performed using +https://jestjs.io/[Jest] and +https://testing-library.com/docs/react-testing-library/intro[React +Testing Library]. + +==== Testing guidelines + +* Prefer factories created with +https://github.com/thoughtbot/fishery[Fishery] and +https://fakerjs.dev/[Faker] over fixtures to craft test data. +* As a rule of thumb one should avoid mocking and creating spies +wherever possible. + +==== Storybook + +https://storybook.js.org/[Storybook] is used to develop UI components in +isolation. + +When writing UI components, every PR should have new Storybook stories +attached or have existing ones updated. + +=== Books and guides + +* https://developer.mozilla.org/en-US/docs/Web/JavaScript[MDN’s +JavaScript documentation] _is_ the only reliable source of truth about +JavaScript on the web. +* https://reactjs.org/docs/getting-started.html[React documentation] +* https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/README.md[You +Don’t Know JS] +* A bit old but gold: +https://www.oreilly.com/library/view/javascript-the-good/9780596517748/[JavaScript: +The Good Parts] +* https://www.nodejsdesignpatterns.com[Node.JS Design Patterns]: a good +book if you want to write a full-baked JS application. +* https://redux.js.org/usage/[Redux usage guidelines] +* https://redux.js.org/usage/writing-tests[Redux: writing tests] +* https://redux-saga.js.org/docs/advanced/Testing/[Testing Redux sagas] diff --git a/developer/development/pr-env-ssl-certificate-setup.adoc b/developer/development/pr-env-ssl-certificate-setup.adoc new file mode 100644 index 00000000..0d75ce71 --- /dev/null +++ b/developer/development/pr-env-ssl-certificate-setup.adoc @@ -0,0 +1,79 @@ +== SSL Certificate creation and setup Guide for Pull Request Environments + +This guide outlines the process for creating and setting up SSL +certificates for pull request environments using Let’s Encrypt and AWS +Route 53. + +This also is the process we currently apply upon expiration in order to +generate new valid ones. + +=== Generating the Certificate + +Run the following command to start the certificate generation process: + +[source,bash] +---- +certbot certonly --manual --preferred-challenges dns -d "*." +---- + +The command prompts to create a DNS TXT record for domain verification +and it waits for action to proceed: + +.... +Please deploy a DNS TXT record under the name +_acme-challenge. with the following value: + + + +Before continuing, verify the record is deployed. +.... + +=== Updating DNS Records in AWS Route 53 + +[arabic] +. Log in to the AWS Management Console +. Navigate to the Route 53 service +. Select the hosted zone matching `++` +. Find the TXT record for `+_acme-challenge.+` +. Update its value with the `++` provided +by certbot +. Wait a few moments for DNS propagation to complete +. Return to the terminal and press Enter to continue the certificate +generation process + +=== Certificate Files + +After successful verification, the certificates and keys will be +available at: + +.... +/etc/letsencrypt/live// +.... + +=== Updating GitHub Action Secrets + +The final step is to update the CI secrets in GitHub: + +[arabic] +. Navigate to +https://github.com/trento-project/web/settings/secrets/actions[Web’s +Actions secrets and variables] +. Update the `+PR_ENV_SSL_CERT+` secret: ++ +[source,bash] +---- +cat /etc/letsencrypt/live//fullchain.pem | base64 +---- ++ +Copy the output and paste it as the value for `+PR_ENV_SSL_CERT+` +. Update the `+PR_ENV_SSL_CERT_KEY+` secret: ++ +[source,bash] +---- +cat /etc/letsencrypt/live//privkey.pem | base64 +---- ++ +Copy the output and paste it as the value for `+PR_ENV_SSL_CERT_KEY+` + +Once completed, the pull request environments will use the new SSL +certificate. diff --git a/developer/development/release.adoc b/developer/development/release.adoc new file mode 100644 index 00000000..62f4dc67 --- /dev/null +++ b/developer/development/release.adoc @@ -0,0 +1,31 @@ +== Release Process + +To issue a new version of any Trento component (e.g. Web, Agent, Wanda, +etc.), a new GitHub release needs to be published in the respective +repository. + +Most of this process is automated via GitHub Actions. + +=== Overview + +These are the main steps, to be repeated for each relevant repository: + +* Bump the version by changing the file holding the most recent version +number, named `+VERSION+` and located at the root of the repository. +* Push this change as a commit on the `+main+` branch (admins only), or +open a pull-request as usual. This will trigger the following automated +steps: +** update the changelog; +** add a tag to the repository; +** publish the release on GitHub; +** build container images hosted on ghcr.io; +** update SUSE distribution packages in OBS. + +____ +⚠️ *Note* + +The Continuous Integration test phase is not executed during this +release process (i.e. changing just the `+VERSION+` file won’t trigger +any test job), so developers should rely on the fact that the `+main+` +branch is expected to always be in a green build status. + +If it’s not, one should work to achieve a green build on `+main+` +_before_ bumping the version. +____ diff --git a/developer/development/test-manual-installation-with-agents.adoc b/developer/development/test-manual-installation-with-agents.adoc new file mode 100644 index 00000000..4367c9f2 --- /dev/null +++ b/developer/development/test-manual-installation-with-agents.adoc @@ -0,0 +1,59 @@ +== Testing the setup + +To test the +https://github.com/trento-project/docs/blob/main/guides/manual-installation.md[manual +installation locally] deploy an agent using the available +`+trento-agent+` package. This package is already available in SLES 15 +SP5, so we can install it using zypper: + +[source,bash] +---- +zypper install trento-agent +---- + +=== Configuring the Agent host with the Self-Signed Certificate + +*Step 1*: On the Trento agent, copy the self-signed certificate +`+trento.crt+` from the Trento server to the agent machine with `+scp+` +to transfer the certificate to `+/etc/pki/trust/anchors/+`. + +[source,bash] +---- +scp <>@<>:/etc/ssl/certs/trento.crt /etc/pki/trust/anchors/ +---- + +*Step 2*: Update the certificate store: + +[source,bash] +---- +update-ca-certificates +---- + +*Step 3*: Configure the Trento agent using the +`+/etc/trento/agent.yaml+` file. + +Make sure to use `+https+` for the `+server-url+` parameter. Refer to +https://documentation.suse.com/sles-sap/trento/html/SLES-SAP-trento/index.html#sec-trento-installing-trentoagent +for more details. + +Example agent.yaml content: + +[source,bash] +---- +server-url: https://trento.example.com +facts-service-url: amqp://trento_user:trento_user_password@trento.example.com:5672/vhost +api-key: <> +---- + +____ +*Note:* Depending on your setup, adjust the configuration of +`+/etc/hosts+` to point the server url https://trento.example.com. +____ + +Example of `+/etc/hosts+`: + +[source,bash] +---- +127.0.0.1 localhost +<> trento.example.com +---- diff --git a/developer/rfc/0001-checks-customization.adoc b/developer/rfc/0001-checks-customization.adoc new file mode 100644 index 00000000..4b15ddf7 --- /dev/null +++ b/developer/rfc/0001-checks-customization.adoc @@ -0,0 +1,439 @@ +[width="100%",cols="<18%,<82%",] +|=== +|Feature Name |Checks Customization +|Start Date |Jan 15th, 2025 +|Category |Architecture +|PR |https://github.com/trento-project/docs/pull/62[#62] +|=== + +== Summary + +Certain checks in the Trento checks catalog should be customized by +customers on a target basis to adjust the corresponding expected values +to ones that work better in their environment. + +=== Use Cases outline + +* As a user, I want to override a Check’s expected values on a target +basis (host or cluster at the moment) so that it matches my specific +environment needs +* As a user, I want the overridden values to be used in a checks +execution +* As a user, I want to reset a customized check to default so that I can +return to using SUSE’s suggested best practices +* As a user, I want check customization attempts to be tracked in the +Activity Log + +== Motivation + +The purpose of this RFC is to define what Checks customization entails +in overall Trento Architecture: - where data and operations belong - the +interactions involved between the components (mainly server and checks +engine) - authorizing customization operations - tracking customization +activities + +== Detailed design + +Considering the outlined link:#use-cases-outline[use cases] we need to: +- define link:#customizable-check[when a check can be customized] - +persist custom values (basically CRUD-ish operations) on a check+target +basis - use custom values in the checks execution by the Check Engine +(Wanda) + +Since check engine is meant to deal with Checks, it looks naturally +reasonable considering it as the component who’s responsible to deal +with link:#operations-on-custom-values[custom values operations and +data]. + +This makes sure also that +link:#custom-values-usage-during-checks-execution[during checks +execution] the engine already has the information needed to override +built-in values. + +=== Customizable check + +A Check can be considered _customizable_, meaning a customer is allowed +to modify its expected values, if it’s specification contains the +`+values+` entry and if that entry is not empty. + +*Customizable Check excerpt* + +[source,yaml] +---- +id: "156F64" +name: Check Corosync token_timeout value +# ... +values: + - name: expected_token_timeout + default: 5000 + conditions: + - value: 30000 + when: env.provider == "azure" || env.provider == "aws" + - value: 20000 + when: env.provider == "gcp" +---- + +In order to allow customization of checks where hardcoded values are +used in expectation rhai scripts, those checks need to be adjusted by +moving hardcoded values to DSL specified values. + +*Not Customizable Check excerpt* + +[source,yaml] +---- +# not customizable check +id: "AAA000" +name: Lorem ipsum dolor sit amet +# ... +# missing `values` entry +expectations: + - name: foo_expectation + expect: facts.some_fact == "yes" +---- + +Need to be refactored to something like the following + +[source,yaml] +---- +# refactored to be customizable +id: "AAA000" +name: Lorem ipsum dolor sit amet +# ... +values: + - name: expected_yes_value + default: "yes" + +expectations: + - name: foo_expectation + expect: facts.some_fact == values.expected_yes_value +---- + +This is necessary because operating on the rhai script is currently not +an option. + +==== Caveats + +After some deeper research there are some caveates to be take into +account for check customizability - check +https://github.com/trento-project/checks/blob/main/checks/3A59DC.yaml[3A59DC] +will be not allowed for customization because of its nature (what it is +attempting to do and how it is written) - if one of the values of a +check is a list, that specific value won’t be allowed for modification +(deferred implementation) + +=== Operations on custom values + +In order to support applying custom values, changing them after they’ve +been set and also reset to SUSE’s default ones, the following new +operations would be introduced: - link:#apply-custom-values[Apply custom +values] - link:#change-custom-values[Change custom values] - +link:#reset-check-to-defaults[Reset custom values] - +link:#reading-customization[Reading custom values] + +*Disclaimer:* all following endpoints, methods, paths, query strings, +parameters are indicative at this point and up for further refinement. +The intent is to give the general feeling of the moving parts and what +is needed. + +==== Apply custom values + +This operation allows to apply custom values for a given customizable +check that has not been customized yet. The operation allows to apply a +custom value for at least one customizable value, meaning that if a +check defines more than one *expected value*, this operation allows to +customize one, many or all of them (ie. if a check has 3 expected values +defined, a user might be interested in customizing only one of them and +use the default ones for the others) + +===== Endpoint + +`+POST /checks/:check_id/values+` + +[source,json] +---- +{ + "target_id": "target-uuid", + "group_id": "group-uuid", + "values": [ + { + "name": "expected_token_timeout", + "value": 42 + }, + // possibly other entries + ] +} +---- + +==== Change custom values + +This operation allows to change custom values on an already customized +check. The operation allows to change custom values for at least one +customizable value, meaning that if a check defines more than one +*expected value*, this operation allows to change customization for one, +many or all of them, independently from the fact that they’ve been +previously customized or not (ie. if a check has 3 expected values +defined, and a user has customized one of them, with this operation a +user might be interested in customizing only one of them and use the +default ones for the others) + +===== Endpoint + +`+PATCH /checks/:check_id/values+` + +[source,json] +---- +{ + "target_id": "target-uuid", + "group_id": "group-uuid", + "values": [ + { + "name": "expected_token_timeout", + "value": 42 + }, + // possibly other entries + ] +} +---- + +==== Reset check to defaults + +This operation clears any previously set custom value, effectively +resulting in checks execution considering built-in values defined in +check’s specification. + +===== Endpoint + +`+DELETE /checks/:check_id/values+` + +[source,json] +---- +{ + "target_id": "target-uuid", + "group_id": "group-uuid" +} +---- + +Whether such operations require both `+target_id+` and `+group_id+` as +input for the operations will be defined at due time. + +==== Reading customization + +A target’s checks selection workflow gets extended with customization +capabilities, hence the following extra information is needed: - whether +a check is customizable - whether a value of a customizable check can be +customized (ie a value which is a list cannot be customized, yet) - +identify which is the value being used based on the context (requires +evaluating `+when+` conditions) so that the user know what actually is +going to be overridden - whether a check has been already customized - +which are the custom values that have been applied + +Since reading the checks catalog alone wouldn’t be enough anymore, +options are: - the current read operation on the catalog is extended to +carry the customization data - a read operation is added specifically +targeting custom values - a read operation is added to fulfill the +overall Checks Selection (meaning the read catalog operation remains as +such while an _extended catalog_ representation, as depicted in the +first option, becomes an operation on its own) + +===== Option 1: enriching the catalog + +This option, besides requiring the addition of extra field to the +catalog’s representation, also demands for the target identifier, at +least, to be part of the operation input so that the correct overriding +values are retrieved. + +`+GET /checks/catalog?provider=...&target_type=...&target_id=uuid+` + +Sample response would be what the catalog currently exposes plus the +extra information + +[source,json] +---- +[ + { + "id": "check1", + // other check fields + "values": [ + //... + ], + "customizable": true, + "customized": true, + "custom_values": [ + //... + ] + }, + // other checks +] + +// Note: customization related new fields could be grouped into their own entry, rather than adding all of them at the root level +---- + +This option adds perhaps too much responsibilities to the catalog +operation which is also used when non in a checks selection workflow. + +===== Option 2: exposing a read operation for target based customized values of a check + +This option entails the introduction of a new operation to get all +customized values information for a specific target. Better not do it on +a per check basis as we usually need info for a bulck of checks. + +`+GET /checks/:target_id/values+` + +[source,json] +---- +[ + { + "id": "check1", + "default_values": [ + //... + ], + "custom_values": [ // having both original ones + overriding ones allows exposing the difference + //... + ] + }, + // other checks customized values +] +---- + +Such an options requires to delegate to a client aggregating information +from the catalog and this new data to get the full picture. + +===== Option 3: exposing a read operation for target based catalog with customization information + +This option proposes a new operation which effectively is a narrowed +version of the catalog specific for the target containing also +customization information, as in option 1, keeping the actual catalog +operation untouched and scoped only for generic consultation. + +`+GET /checks/:target_id/catalog?qs...+` + +[source,json] +---- +[ + { + "id": "check1", + // other check fields + "values": [ + //... + ], + "customizable": true, + "customized": true, + "custom_values": [ // having both original ones + overriding ones allows exposing the difference + //... + ] + }, + { + "id": "check2", + "values": [ + //... + ], + "customizable": true, + "customized": false, + "custom_values": [] // empty list | null | absent + }, + { + "id": "check3", + // missing or empty values entry + "customizable": false, + "customized": false, + "custom_values": [] // empty list | null | absent + } +] + +// Notes: +// - delegating detection of whether a check is customizable (ie it has values) to a client sounds like logic leakage +// - delegating detection of whether a check is customized (ie it has custom values) to a client sounds less of a leakage but if we decide to expose `customizable` it is just trivial exposing also a `customized` entry +---- + +Somewhat related to https://github.com/trento-project/web/pull/3160[this +hackweek exploration]. + +=== Custom values usage during checks execution + +When there are custom values for a check on a specific target, those +need to be used instead of the built-in ones during a +https://github.com/trento-project/wanda/blob/main/guides/specification.md#checks-execution[checks +execution]. + +By having the custom values available in its state, wanda can simply +query and use them instead of the built-in ones. + +==== Notes on execution results + +Since custom values at a certain point in time might differ from custom +values used during an execution it becomes necessary snapshotting the +specific custom values used during a specific execution, that is storing +the custom values along with the execution result they’re being used in. + +Then, to get a proper overview of a checks execution results, data from +the catalog and from the extended checks execution results keep being +aggregated together as we already do. + +=== Authorizing and Logging customization activities + +==== Authorization + +Currently Wanda only supports checking for an authenticated token, it +does not check whether user is authorized to perform an action. + +We can make sure that the JWT generated by the auth server (aka web) +also contains abilities, so that a service provider (like wanda in this +case) can allow/disallow certain operations. + +==== Logging + +Currently, +https://github.com/trento-project/docs/blob/main/adr/0015-activity-logging.md[Activity +Logging] is pretty much scoped to server component tracking activities +of the following nature: - API based operations, that is calls to +specific http requests - domain events emitted by the system + +Having checks customization operations in wanda adds a challenge since +those interesting actions are not passing through the Activity Logging +subsystem. + +Options available to get a valuable outcome are: - checks customization +operations are proxied via server component (meaning we have twin +operations exposed by trento web that actually just call wanda) - wanda +emits messages after it applies customization so that interested parties +- web - acknowledges the operation was successfully completed and tracks +relevant entries in the activity log + +Activity Logging needs to evolve to support logging actions in a +distributed system like Trento, however this is out of scope for this +specific RFC. + +== Drawbacks + +== Alternatives + +The main alternative point is storing custom values in Web rather than +in Wanda. + +Generally speaking all the considerations made previously keep their +validity. + +Here’s the main points considered about storing custom values in web: - +having web responsible for checks customization data and operations +means leaking a responsibility where it does not naturally belong - in +the context of a standalone Compliance Check Engine, Checks +Customization feature would be clunky to use because custom values would +not be organically part of the check engine, but would need to be +provided every time even if they did not change - using the overriding +values in a checks execution requires sending those from web to wanda +via the `+ExecutionRequested+` message hence either: - we change the +message contract - we inappropriately send the overriding values in +`+ExecutionRequested+` env entry - also the start endpoint in Wanda +needs to be changed - having checks and their customization in different +places makes it harder to operate when checks change (meaning that if a +check changes its spec we need to react to that and possibly invalidate +a previously made customization for instance. We have a similar +situation with selected checks stored in web and the possibility that a +selected check is removed) + +== Unresolved questions + +* consider the difference between customizing checks for a host vs +customizing checks for a cluster (host checks execution vs cluster +checks execution) target_id might not be sufficient, we might need to +take into account the group id as well diff --git a/modules/ROOT/pages/index.adoc b/modules/ROOT/pages/index.adoc new file mode 100644 index 00000000..6f2b50df --- /dev/null +++ b/modules/ROOT/pages/index.adoc @@ -0,0 +1,4 @@ += Trento Documentation + + +== What is Trento? \ No newline at end of file diff --git a/modules/api/nav.adoc b/modules/api/nav.adoc new file mode 100644 index 00000000..581246bd --- /dev/null +++ b/modules/api/nav.adoc @@ -0,0 +1,5 @@ +.Trento Open Api +* https://www.trento-project.io/wanda/swaggerui/[Wanda Api] +* https://www.trento-project.io/wanda/readme.html[Wanda Guides and Modules] +* https://www.trento-project.io/web/swaggerui/[Web Api] +* https://www.trento-project.io/web/readme.html[Web Modules and Mix Tasks] diff --git a/modules/components/nav.adoc b/modules/components/nav.adoc new file mode 100644 index 00000000..b0b92017 --- /dev/null +++ b/modules/components/nav.adoc @@ -0,0 +1,38 @@ +.Trento Components + +* Agent + +** xref:agent/ci-cd-variables.adoc[CI-CD-Variables] +** xref:agent/development/how-to-make-a-release.adoc[How to make a release] + +* Helm-Charts +** xref:helm_charts/trento-server.adoc[Trento Server] + +* Web + +** xref:web/alerting/alerting.adoc[Alerting] + +** Authentication +*** xref:web/authentication/jwt_specification.adoc[JWT Specification] +*** xref:web/authentication/spa_flow.adoc[SPA Authentication Flow] + +** Development +*** xref:web/development/environment_variables.adoc[Environment Variables] +*** xref:web/development/hack_on_the_trento.adoc[Development Setup] + +** Integration +*** xref:web/integration/oidc.adoc[OIDC Integration] + +** Monitoring +*** xref:web/monitoring/monitoring.adoc[Monitoring] + + +* Wanda +** xref:wanda/specification.adoc[Wanda Specification] +** xref:wanda/expression_language.adoc[Expression Language] +** xref:wanda/gatherers.adoc[Data Gatherers] +** xref:wanda/rhai_expressions_cheat_sheet.adoc[Rhai Expressions Cheat Sheet] + +** Development +*** xref:wanda/development/hack_on_wanda.adoc[Wanda Development] +*** xref:wanda/development/demo.adoc[Wanda Demo] \ No newline at end of file diff --git a/modules/developer/nav.adoc b/modules/developer/nav.adoc new file mode 100644 index 00000000..0c79b8d2 --- /dev/null +++ b/modules/developer/nav.adoc @@ -0,0 +1,41 @@ +.Developer Documentation + + +* ADR +** xref:adr/README.adoc[ADR Documentation] +** xref:adr/0001-record-architecture-decisions.adoc[0001: Record Architecture Decisions] +** xref:adr/0002-build-a-dsl-to-declare-checks.adoc[0002: Build a DSL to Declare Checks] +** xref:adr/0003-build-a-check-execution-orchestrator.adoc[0003: Build a Check Execution Orchestrator] +** xref:adr/0004-add-facts-gathering-capabilities-to-the-agent.adoc[0004: Add Facts Gathering to the Agent] +** xref:adr/0005-use-protobuf-to-define-and-generate-contracts.adoc[0005: Use Protobuf for Contracts] +** xref:adr/0006-use-cloudevents-to-describe-event-data.adoc[0006: Use CloudEvents for Event Data] +** xref:adr/0007-use-jwt-tokens-as-authentication-mechanism.adoc[0007: Use JWT Tokens for Authentication] +** xref:adr/0008-rest-api-versioning-strategy.adoc[0008: REST API Versioning Strategy] +** xref:adr/0009-frontend-directory-structure-and-architecture.adoc[0009: Frontend Directory Structure] +** xref:adr/0010-web-dashboard-directory-structure-and-contexts.adoc[0010: Web Dashboard Structure & Contexts] +** xref:adr/0011-sap-system-database-aggregate-split.adoc[0011: SAP System DB Aggregate Split] +** xref:adr/0012-reactiveness.adoc[0012: Reactiveness] +** xref:adr/0013-suma-integration.adoc[0013: SUMA Integration] +** xref:adr/0014-decoupling-of-trento-checks-from-wanda.adoc[0014: Decouple Trento Checks from Wanda] +** xref:adr/0015-activity-logging.adoc[0015: Activity Logging] +** xref:adr/0016-discarded-discovery-events.adoc[0016: Discarded Discovery Events] +** xref:adr/0018-agent-operations-orchestration.adoc[0018: Agent Operations Orchestration] +** xref:adr/0019-e2e-testing-practices.adoc[0019: E2E Testing Practices] +** xref:adr/0020-checks-customization.adoc[0020: Checks Customization] + +* Coding Standards +** xref:coding-standarts/README.adoc[Coding Standards Overview] +** xref:coding-standarts/elixir.adoc[Elixir] +** xref:coding-standarts/go.adoc[Go] +** xref:coding-standarts/javascript.adoc[JavaScript] + +* Architecture +*** xref:architecture/trento-architecture.adoc[Trento Architecture] + +* Development +** xref:development/pr-env-ssl-certificate-setup.adoc[PR Env SSL Certificate Setup] +** xref:development/release.adoc[Release Process] +** xref:development/test-manual-installation-with-agents.adoc[Test Manual Installation with Agents] + +* RFC +** xref:rfc/0001-checks-customization.adoc[Check Customization] \ No newline at end of file diff --git a/modules/trento_doc_unversioned/nav.adoc b/modules/trento_doc_unversioned/nav.adoc new file mode 100644 index 00000000..71a16d75 --- /dev/null +++ b/modules/trento_doc_unversioned/nav.adoc @@ -0,0 +1,27 @@ += Getting Started with Trento + +* xref:trento-guide.adoc[Getting Started with Trento] +** xref:trento-intro.adoc[1. What is Trento?] +** xref:trento-lifecycle.adoc[2. Product lifecycle and update strategy] +** xref:trento-requirements.adoc[3. Requirements] +** xref:trento-install-server.adoc[4. Installing Trento Server] +** xref:trento-install-agents.adoc[5. Installing Trento Agents] +** xref:trento-user-manage.adoc[6. User management] +** xref:trento-sso-integration.adoc[7. Single Sign-On integration] +** xref:trento-activity-log.adoc[8. Activity Log] +** xref:trento-checks.adoc[9. Performing configuration checks] +** xref:trento-web-console.adoc[10. Using Trento Web] +** xref:trento-housekeeping.adoc[11. Housekeeping] +** xref:trento-manage-tags.adoc[12. Managing tags] +** xref:trento-smlm-integration.adoc[13. Integration with SUSE Multi-Linux Manager] +** xref:trento-rotate-api-keys.adoc[14. Rotating API keys] +** xref:trento-update-trento-server.adoc[15. Updating Trento Server] +** xref:trento-update-trento-agent.adoc[16. Updating a Trento Agent] +** xref:trento-update-trento-checks.adoc[17. Updating Trento Checks] +** xref:trento-uninstall-trento-server.adoc[18. Uninstalling Trento Server] +** xref:trento-uninstall-trento-agent.adoc[19. Uninstalling a Trento Agent] +** xref:trento-report-issue.adoc[20. Reporting an Issue] +** xref:trento-analyze-problems.adoc[21. Problem Analysis] +** xref:trento-compatibility.adoc[22. Compatibility matrix between Trento Server and Trento Agents] +** xref:trento-version-history.adoc[23. Highlights of Trento versions] +** xref:trento-more-info.adoc[24. More information] diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..f4f34079 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1849 @@ +{ + "name": "docs", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@antora/collector-extension": "^1.0.1", + "@antora/lunr-extension": "^1.0.0-alpha.10" + }, + "devDependencies": { + "antora": "3.1.10" + } + }, + "node_modules/@antora/asciidoc-loader": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/asciidoc-loader/-/asciidoc-loader-3.1.10.tgz", + "integrity": "sha512-np0JkOV37CK7V4eDZUZXf4fQuCKYW3Alxl8FlyzBevXi2Ujv29O82JLbHbv1cyTsvGkGNNB+gzJIx9XBsQ7+Nw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/logger": "3.1.10", + "@antora/user-require-helper": "~3.0", + "@asciidoctor/core": "~2.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/cli": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/cli/-/cli-3.1.10.tgz", + "integrity": "sha512-gp8u9aVM0w1DtWSsB5PwvEfFYKrooPENLhN58RAfdgTrcsTsWw+CDysFZPgEaHB0Y1ZbanR82ZH/f6JVKGcZfQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/logger": "3.1.10", + "@antora/playbook-builder": "3.1.10", + "@antora/user-require-helper": "~3.0", + "commander": "~11.1" + }, + "bin": { + "antora": "bin/antora" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/collector-extension": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@antora/collector-extension/-/collector-extension-1.0.1.tgz", + "integrity": "sha512-Bq6s2ZN5VESJ3/skEeytfCeBRD4Z/K06dWQFPTMFUn3+nzaA/NDeQ1x16YPk491DeOSg4dXcW0CerwVqdL3zPQ==", + "license": "MPL-2.0", + "dependencies": { + "@antora/expand-path-helper": "~3.0", + "@antora/run-command-helper": "~1.0", + "cache-directory": "~2.0", + "fast-glob": "~3.3", + "js-yaml": "~4.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/content-aggregator": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/content-aggregator/-/content-aggregator-3.1.10.tgz", + "integrity": "sha512-OT6ZcCA7LrtNfrAZUr3hFh+Z/1isKpsfnqFjCDC66NEMqIyzJO99jq0CM66rYlYhyX7mb5BwEua8lHcwpOXNow==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/expand-path-helper": "~3.0", + "@antora/logger": "3.1.10", + "@antora/user-require-helper": "~3.0", + "braces": "~3.0", + "cache-directory": "~2.0", + "fast-glob": "~3.3", + "hpagent": "~1.2", + "isomorphic-git": "~1.25", + "js-yaml": "~4.1", + "multi-progress": "~4.0", + "picomatch": "~4.0", + "progress": "~2.0", + "should-proxy": "~1.0", + "simple-get": "~4.0", + "vinyl": "~3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/content-classifier": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/content-classifier/-/content-classifier-3.1.10.tgz", + "integrity": "sha512-3JJl4IIiTX00v/MirK603NoqIcHjGYAaRWt3Q4U03tI1Fv2Aho/ypO3FE45069jFf0Dx2uDJfp5kapb9gaIjdQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/asciidoc-loader": "3.1.10", + "@antora/logger": "3.1.10", + "mime-types": "~2.1", + "vinyl": "~3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/document-converter": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/document-converter/-/document-converter-3.1.10.tgz", + "integrity": "sha512-qi9ctgcKal8tZtWflVo66w+4zCJoBmUKRV+eA9aRRR09KDdU9r514vu1adWNgniPppISr90zD13V5l2JUy/2CQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/asciidoc-loader": "3.1.10" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/expand-path-helper": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@antora/expand-path-helper/-/expand-path-helper-3.0.0.tgz", + "integrity": "sha512-7PdEIhk97v85/CSm3HynCsX14TR6oIVz1s233nNLsiWubE8tTnpPt4sNRJR+hpmIZ6Bx9c6QDp3XIoiyu/WYYA==", + "license": "MPL-2.0", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/file-publisher": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/file-publisher/-/file-publisher-3.1.10.tgz", + "integrity": "sha512-DPR/0d1P+kr3qV4T0Gh81POEO/aCmNWIp/oLUYAhr0HHOcFzgpTUUoLStgcYynZPFRIB7EYKSab+oYSCK17DGA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/expand-path-helper": "~3.0", + "@antora/user-require-helper": "~3.0", + "vinyl": "~3.0", + "yazl": "~2.5" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/logger": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/logger/-/logger-3.1.10.tgz", + "integrity": "sha512-WSuIxEP2tVrhWtTj/sIrwBDjpi4ldB/1Kpiu4PXmY4/qeWP8thW6u8nXdwdDcWss5zqkZWjourvWKwVq7y8Wjg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/expand-path-helper": "~3.0", + "pino": "~9.2", + "pino-pretty": "~11.2", + "sonic-boom": "~4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/lunr-extension": { + "version": "1.0.0-alpha.10", + "resolved": "https://registry.npmjs.org/@antora/lunr-extension/-/lunr-extension-1.0.0-alpha.10.tgz", + "integrity": "sha512-YunvJ3D/Q/GfIgOvmoFvL6NrKLL3TBN2UiemIDCquvWl6z0Jlonb5w3GkT/CKhtnGWXDPOMOJ7M4fVOKjMOHZw==", + "license": "MPL-2.0", + "dependencies": { + "htmlparser2": "~9.1", + "lunr": "~2.3", + "lunr-languages": "~1.10" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/navigation-builder": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/navigation-builder/-/navigation-builder-3.1.10.tgz", + "integrity": "sha512-aLMK49nYsSB3mEZbLkmUXDAUYmscv2AFWu+5c3eqVGkQ6Wgyd79WQ6Bz3/TN9YqkzGL+PqGs0G39F0VQzD23Hw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/asciidoc-loader": "3.1.10" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/page-composer": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/page-composer/-/page-composer-3.1.10.tgz", + "integrity": "sha512-JoEg8J8HVsnPmAgUrYSGzf0C8rQefXyCi/18ucy0utyfUvlJNsZvUbGUPx62Het9p0JP0FkAz2MTLyDlNdArVg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/logger": "3.1.10", + "handlebars": "~4.7", + "require-from-string": "~2.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/playbook-builder": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/playbook-builder/-/playbook-builder-3.1.10.tgz", + "integrity": "sha512-UB8UmRYfkKgActTUlotdVS4FKGjaZgTnSXE7Fns1xb3/3HRanWvI+Yze1OmCkGC33cTpoQFnSYp7ySEH8LaiBw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@iarna/toml": "~2.2", + "convict": "~6.2", + "js-yaml": "~4.1", + "json5": "~2.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/redirect-producer": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/redirect-producer/-/redirect-producer-3.1.10.tgz", + "integrity": "sha512-IbWJGh6LmsxJQ821h0B9JfooofFZBgFLZxsbp/IoTLkBFGLFAY5tDRvB6rvubfNLRoSjM8VjEUXGqVLlwZOb+g==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "vinyl": "~3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/run-command-helper": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@antora/run-command-helper/-/run-command-helper-1.0.2.tgz", + "integrity": "sha512-fb8t6IQwQvP3Wsmd1pd9rk2R0wJcseY+2qTnhBhqjtM+N43/mT9OnqepRsPqtzzqAcLA1kp4x9SQvAi9QZgjrA==", + "license": "MPL-2.0", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/site-generator": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/site-generator/-/site-generator-3.1.10.tgz", + "integrity": "sha512-NCULYtwUjIyr5FGCymhfG/zDVUmZ6pfmCPorka8mAzo4/GDx1T7bgaRL9rEIyf2AMqcm7apQiAz03mpU4kucsw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/asciidoc-loader": "3.1.10", + "@antora/content-aggregator": "3.1.10", + "@antora/content-classifier": "3.1.10", + "@antora/document-converter": "3.1.10", + "@antora/file-publisher": "3.1.10", + "@antora/logger": "3.1.10", + "@antora/navigation-builder": "3.1.10", + "@antora/page-composer": "3.1.10", + "@antora/playbook-builder": "3.1.10", + "@antora/redirect-producer": "3.1.10", + "@antora/site-mapper": "3.1.10", + "@antora/site-publisher": "3.1.10", + "@antora/ui-loader": "3.1.10", + "@antora/user-require-helper": "~3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/site-mapper": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/site-mapper/-/site-mapper-3.1.10.tgz", + "integrity": "sha512-KY1j/y0uxC2Y7RAo4r4yKv9cgFm8aZoRylZXEODJnwj3tffbZ2ZdRzSWHp6fN0QX/Algrr9JNd9CWrjcj2f3Zw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/content-classifier": "3.1.10", + "vinyl": "~3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/site-publisher": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/site-publisher/-/site-publisher-3.1.10.tgz", + "integrity": "sha512-G4xcUWvgth8oeEQwiu9U1cE0miQtYHwKHOobUbDBt2Y6LlC5H31zQQmAyvMwTsGRlvYRgLVtG6j9d6JBwQ6w9Q==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/file-publisher": "3.1.10" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/ui-loader": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@antora/ui-loader/-/ui-loader-3.1.10.tgz", + "integrity": "sha512-H1f5wI5a5HjLuE/Wexvc8NZy8w83Bhqjka7t1DbwOOqP+LyxFGLx/QbBVKdTtgFNDHVMtNBlplQq0ixeoTSh0A==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/expand-path-helper": "~3.0", + "braces": "~3.0", + "cache-directory": "~2.0", + "fast-glob": "~3.3", + "hpagent": "~1.2", + "js-yaml": "~4.1", + "picomatch": "~4.0", + "should-proxy": "~1.0", + "simple-get": "~4.0", + "vinyl": "~3.0", + "yauzl": "~3.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/user-require-helper": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@antora/user-require-helper/-/user-require-helper-3.0.0.tgz", + "integrity": "sha512-KIXb8WYhnrnwH7Jj21l1w+et9k5GvcgcqvLOwxqWLEd0uVZOiMFdqFjqbVm3M+zcrs1JXWMeh2LLvxBbQs3q/Q==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/expand-path-helper": "~3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@asciidoctor/core": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@asciidoctor/core/-/core-2.2.8.tgz", + "integrity": "sha512-oozXk7ZO1RAd/KLFLkKOhqTcG4GO3CV44WwOFg2gMcCsqCUTarvMT7xERIoWW2WurKbB0/ce+98r01p8xPOlBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "asciidoctor-opal-runtime": "0.3.3", + "unxhr": "1.0.1" + }, + "engines": { + "node": ">=8.11", + "npm": ">=5.0.0", + "yarn": ">=1.1.0" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "dev": true, + "license": "ISC" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/antora": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/antora/-/antora-3.1.10.tgz", + "integrity": "sha512-FcXPfqxi5xrGF2fTrFiiau45q8w0bzRcnfk97nxvpvztPDHX/lUOrBF/GpaGl1JT5K085VkI3/dbxTlvWK1jjw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@antora/cli": "3.1.10", + "@antora/site-generator": "3.1.10" + }, + "bin": { + "antora": "bin/antora" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/asciidoctor-opal-runtime": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/asciidoctor-opal-runtime/-/asciidoctor-opal-runtime-0.3.3.tgz", + "integrity": "sha512-/CEVNiOia8E5BMO9FLooo+Kv18K4+4JBFRJp8vUy/N5dMRAg+fRNV4HA+o6aoSC79jVU/aT5XvUpxSxSsTS8FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "7.1.3", + "unxhr": "1.0.1" + }, + "engines": { + "node": ">=8.11" + } + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cache-directory": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cache-directory/-/cache-directory-2.0.0.tgz", + "integrity": "sha512-7YKEapH+2Uikde8hySyfobXBqPKULDyHNl/lhKm7cKf/GJFdG/tU/WpLrOg2y9aUrQrWUilYqawFIiGJPS6gDA==", + "license": "LGPL-3.0+", + "dependencies": { + "xdg-basedir": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convict": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/convict/-/convict-6.2.4.tgz", + "integrity": "sha512-qN60BAwdMVdofckX7AlohVJ2x9UvjTNoKVXCL2LxFk1l7757EJqf1nySdMkPQer0bt8kQ5lQiyZ9/2NvrFBuwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "yargs-parser": "^20.2.7" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/diff3": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", + "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isomorphic-git": { + "version": "1.25.10", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.25.10.tgz", + "integrity": "sha512-IxGiaKBwAdcgBXwIcxJU6rHLk+NrzYaaPKXXQffcA0GW3IUrQXdUPDXDo+hkGVcYruuz/7JlGBiuaeTCgIgivQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-lock": "^1.4.1", + "clean-git-ref": "^2.0.1", + "crc-32": "^1.2.0", + "diff3": "0.0.3", + "ignore": "^5.1.4", + "minimisted": "^2.0.0", + "pako": "^1.0.10", + "pify": "^4.0.1", + "readable-stream": "^3.4.0", + "sha.js": "^2.4.9", + "simple-get": "^4.0.1" + }, + "bin": { + "isogit": "cli.cjs" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "license": "MIT" + }, + "node_modules/lunr-languages": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/lunr-languages/-/lunr-languages-1.10.0.tgz", + "integrity": "sha512-BBjKKcwrieJlzwwc9M5H/MRXGJ2qyOSDx/NXYiwkuKjiLOOoouh0WsDzeqcLoUWcX31y7i8sb8IgsZKObdUCkw==", + "license": "MPL-1.1" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5" + } + }, + "node_modules/multi-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/multi-progress/-/multi-progress-4.0.0.tgz", + "integrity": "sha512-9zcjyOou3FFCKPXsmkbC3ethv51SFPoA4dJD6TscIp2pUmy26kBDZW6h9XofPELrzseSkuD7r0V+emGEeo39Pg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "progress": "^2.0.0" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pino": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.2.0.tgz", + "integrity": "sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.2.2.tgz", + "integrity": "sha512-2FnyGir8nAJAqD3srROdrF1J5BIcMT4nwj7hHSc60El6Uxlym00UbCCd8pYIterstVBFlMyF1yFV8XdGIPbj4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, + "node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/should-proxy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/should-proxy/-/should-proxy-1.0.4.tgz", + "integrity": "sha512-RPQhIndEIVUCjkfkQ6rs6sOR6pkxJWCNdxtfG5pP0RVgUYbK5911kLTF0TNcCC0G3YCGd492rMollFT2aTd9iQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz", + "integrity": "sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unxhr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unxhr/-/unxhr-1.0.1.tgz", + "integrity": "sha512-MAhukhVHyaLGDjyDYhy8gVjWJyhTECCdNsLwlMoGFoNJ3o79fpQhtQuzmAE4IxCMDwraF4cW8ZjpAV0m9CRQbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.11" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vinyl": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha512-1Dly4xqlulvPD3fZUQJLY+FUIeqN3N2MM3uqe4rCJftAvOjFa3jFGfctOgluGx4ahPbUCsZkmJILiP0Vi4T6lQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yauzl": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.3.tgz", + "integrity": "sha512-JCCdmlJJWv7L0q/KylOekyRaUrdEoUxWkWVcgorosTROCFWiS9p2NNPE9Yb91ak7b1N5SxAZEliWpspbZccivw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..f6602cf0 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "devDependencies": { + "antora": "3.1.10" + }, + "dependencies": { + "@antora/collector-extension": "^1.0.1", + "@antora/lunr-extension": "^1.0.0-alpha.10" + } +} diff --git a/scripts/generate-attributes-yaml.sh b/scripts/generate-attributes-yaml.sh new file mode 100755 index 00000000..24a7d1bc --- /dev/null +++ b/scripts/generate-attributes-yaml.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# Accepts generic-attributes.adoc from doc-unversioned and generates antora.yml with asciidoc attributes +INPUT_FILE="$1" +OUTPUT_FILE="$2" + +if [[ ! -f "$INPUT_FILE" ]]; then + echo "File not found: $INPUT_FILE" + exit 1 +fi + +# Generate output directory +TEMP_DIR="antora_tmp" +mkdir -p "$TEMP_DIR" + +# Keys to ignore (space-separated) from generic-attributes.adoc +IGNORE_KEYS=("toc" "toc-placement" "toc-title" "numbered" "sectnums") + +is_ignored() { + local key="$1" + for ignore in "${IGNORE_KEYS[@]}"; do + if [[ "$ignore" == "$key" ]]; then + return 0 + fi + done + return 1 +} + +echo "asciidoc:" > "$OUTPUT_FILE" +echo " attributes:" >> "$OUTPUT_FILE" + +declare -A seen + +# Read all keys from generic-attributes.adoc +grep '^:' "$INPUT_FILE" | while read -r line; do + key=$(echo "$line" | sed -E 's/^:([^:]+):.*/\1/' | xargs) + + # skip key if seen or in ignore list + if [[ -n "${seen[$key]}" ]] || is_ignored "$key"; then + continue + fi + + value=$(echo "$line" | sed -E 's/^:[^:]+:\s*(.*)/\1/' | xargs) + value="${value//\"/\\\"}" + + echo " $key: \"$value\"" >> "$OUTPUT_FILE" + seen["$key"]=1 +done + +echo "✅ Generated output file: $OUTPUT_FILE" \ No newline at end of file