| # Run the E2E Tests |
| |
| Every change to this chart is validated by an end-to-end suite that installs `chart/skywalking` into |
| a throwaway [kind](https://kind.sigs.k8s.io/) cluster, pushes real traffic through it, and then asks |
| **Horizon UI's BFF** the questions an operator would ask. Nothing in the suite talks to the OAP's |
| GraphQL endpoint directly — that would test the OAP, not the chart. Five cells run in CI, covering |
| two OAP lines against two storage backends. This page explains how they are put together and how to |
| run one on your own machine. |
| |
| ## The framework |
| |
| The tests are driven by [skywalking-infra-e2e](https://github.com/apache/skywalking-infra-e2e). Each |
| config file under `test/e2e/` is one complete test: the cluster to create, the setup steps to run, |
| the traffic to generate, and the assertions to make. The phases are: |
| |
| | Phase | What it does here | |
| | --- | --- | |
| | `setup` | Creates the kind cluster from `file: kind28.yaml`, loads `init-system-environment: env` into the shell environment, then runs the `steps:` in order — install tooling, install Istio, `helm install` the chart, deploy bookinfo, start traffic. Each step may declare `wait:` conditions; the whole phase has `timeout: 25m`. | |
| | `trigger` | Unused. The three traffic cells generate load with a `wrk` Deployment (`test/e2e/traffic-gen.yaml`) applied as a setup step, so no cell has a `trigger:` block. | |
| | `verify` | Runs each `query:` and matches its output against a template in `test/e2e/expected/`, retrying on `retry: {count: 30, interval: 10s}`. | |
| | `cleanup` | No cell declares `cleanup:`, so infra-e2e's own default applies — `always` when `CI=true` (GitHub Actions always tears down), `success` otherwise. A locally *failed* run therefore leaves the cluster up for debugging. | |
| |
| `kind.expose-ports` is what makes the verify phase possible. Every cell exposes exactly one service: |
| |
| ```yaml |
| kind: |
| expose-ports: |
| - namespace: istio-system |
| resource: service/skywalking-ui |
| port: 80 |
| ``` |
| |
| infra-e2e port-forwards it and exports two shell variables named after the resource and the port — |
| `${service_skywalking_ui_host}` and `${service_skywalking_ui_80}` — which every verify case |
| interpolates into a base URL. The OAP's own ports are not exposed at all. |
| |
| `expected/*.yml` are templates, not literals. They use matchers such as `{{- contains .services }}` |
| and `{{ notEmpty .version }}`, so a case asserts "these services are present" or "this field has a |
| value", not an exact payload. |
| |
| ## The four cells |
| |
| Three of them run the same fixture — kind `kindest/node:v1.28.15` (one control plane, three workers), |
| namespace `istio-system`, `fullnameOverride=skywalking`, `oap.replicas=1`, Satellite enabled, Horizon |
| UI at `$UI_REPO:$UI_TAG` — and differ in which OAP and which storage they install, plus the flags |
| each of those pairings needs. |
| |
| | File | OAP | Storage | Distinguishing flags | |
| | --- | --- | --- | --- | |
| | `test/e2e/e2e-oap11-elasticsearch.yaml` | `$OAP_REPO:$OAP_TAG` (11.0.0) | `oap.storageType=elasticsearch` against the ECK subchart, left on by the chart's `elasticsearch.enabled: true` | Pre-installs the ECK CRDs out of `chart/skywalking/charts/eck-operator-3.3.1.tgz` and passes `eck-operator.installCRDs=false`. | |
| | `test/e2e/e2e-oap11-banyandb-standalone.yaml` | 11.0.0 | BanyanDB 0.11 (`$BANYANDB_REPO:$BANYANDB_TAG`) | `oap.storageType=banyandb`, `elasticsearch.enabled=false`, `banyandb.enabled=true`, `banyandb.standalone.enabled=true`, `banyandb.cluster.enabled=false`, `banyandb.auth.enabled=true`. | |
| | `test/e2e/e2e-oap11-banyandb-cluster.yaml` | 11.0.0 | BanyanDB 0.11, cluster mode | As above but `banyandb.standalone.enabled=false`, `banyandb.cluster.enabled=true`. | |
| |
| All three also switch Zipkin on — `oap.ports.zipkin-query=9412` plus `SW_RECEIVER_ZIPKIN=default` |
| and `SW_QUERY_ZIPKIN=default` — because the chart only emits `oap.zipkinUrl` into Horizon's config |
| when that port is set, and one verify case asserts Horizon can reach it. Each runs seven cases. |
| |
| ### The fourth cell: `e2e-config-override.yaml` |
| |
| The other three take the default configuration path — Horizon set up purely by environment variable, |
| with no ConfigMap created at all. This cell covers the opt-in third mechanism for **both** |
| components: a file rendered into a ConfigMap and mounted over the image's own. |
| |
| | | | |
| | --- | --- | |
| | Storage | BanyanDB 0.11 standalone — the cheapest backend that still exercises a real one | |
| | UI override | `ui.config.session.ttlMinutes=17`, which makes the chart render and mount the UI ConfigMap | |
| | OAP override | `oap.config.log4j2\.xml=<Configuration …>`, mounted at `/skywalking/config/log4j2.xml` | |
| | Cases | Four | |
| |
| It is deliberately cheap — no Istio, no bookinfo, no traffic generator. What is under test is |
| whether the overrides are read, which needs none of that, so it skips the `MESH` fixture entirely |
| and never queries a metric. |
| |
| The mount is the riskier path, and the reason this cell exists. Mounting over Horizon's |
| `/app/horizon.yaml` *replaces* it, so the chart has to merge its computed values back in as |
| `${HORIZON_*:default}` tokens. Two of the four cases exist to catch that regression rather than to |
| check the ConfigMap: logging in proves the `HORIZON_AUTH_LOCAL_USERS` token survived the mount |
| (without it the pod is Ready and nobody can sign in), and `/api/oap/info` proves `queryUrl` did not |
| revert to `127.0.0.1`. |
| |
| ### There are no OAP 10.4 cells |
| |
| CI covers the OAP 11 line only. [Version Compatibility](../evaluate/version-compatibility.md) still |
| documents running Horizon 1.0.0 against OAP 10.4.0 — `oap.ports.admin=null` and |
| `HORIZON_TEMPLATES_MODE=readonly` — but nothing here tests it, so treat that combination as |
| documented rather than verified. |
| |
| ## The traffic fixture: everything lands in `MESH` |
| |
| This is the single most useful fact for anyone adding a case. There is no Java agent anywhere in |
| this suite. Each cell: |
| |
| 1. installs Istio with the demo profile and Envoy ALS pointed at the chart's Satellite — |
| `meshConfig.defaultConfig.envoyAccessLogService.address=skywalking-satellite.istio-system:11800` |
| and `meshConfig.enableEnvoyAccessLogService=true` — then labels `default` with |
| `istio-injection=enabled`; |
| 2. installs the chart with `SW_ENVOY_METRIC_ALS_HTTP_ANALYSIS=k8s-mesh` and |
| `SW_ENVOY_METRIC_ALS_TCP_ANALYSIS=k8s-mesh`; |
| 3. deploys [bookinfo](https://istio.io/latest/docs/examples/bookinfo/) from the `$ISTIO_VERSION` |
| manifests and runs `wrk` against `http://istio-ingressgateway.istio-system:80/productpage`. |
| |
| So every service the tests can see is synthesized by the OAP from Envoy access logs, and it lands in |
| the **`MESH`** layer — never `GENERAL`. A verify case that queries `/api/layer/GENERAL/services` |
| will find nothing, no matter how long it retries. |
| |
| Service names come from two places that must agree: `K8S_SERVICE_NAME_RULE='e2e::${service.metadata.name}'` |
| on the OAP command line, and `oap.config."metadata-service-mapping.yaml"` in `test/e2e/values.yaml`, |
| which maps the Istio canonical-name label to `e2e::<name>`. That is why the assertions look for |
| `e2e::productpage` and `e2e::reviews`. |
| |
| ## How an assertion works |
| |
| Every verify case shells out to `test/e2e/script/horizon.sh`, which does what an operator does: log |
| in, keep the session cookie, then call an API with it. |
| |
| ``` |
| horizon.sh <base-url> get <api-path> |
| horizon.sh <base-url> post <api-path> <json-body> |
| ``` |
| |
| It `POST`s to `/api/auth/login` with `curl --fail-with-body -c "$JAR"`, fails loudly unless the |
| response actually set a `horizon_sid` cookie, and reuses that jar (`-b "$JAR"`) for the real request. |
| Credentials come from `HORIZON_USERNAME` / `HORIZON_PASSWORD` and default to `admin`/`admin`. |
| |
| The JSON that comes back is piped through `yq` into a small projection, and *that* is what the |
| `expected/` template matches. One case verbatim, from `test/e2e/e2e-oap11-elasticsearch.yaml`: |
| |
| ```yaml |
| - query: | |
| bash test/e2e/script/horizon.sh http://${service_skywalking_ui_host}:${service_skywalking_ui_80} get /api/layer/MESH/services | yq -p json -o yaml '{"layer": .layer, "services": ([.services[].name] | sort)}' |
| expected: expected/horizon-mesh-services.yml |
| ``` |
| |
| and the template it matches, `test/e2e/expected/horizon-mesh-services.yml` (below its license |
| header): |
| |
| ```yaml |
| layer: MESH |
| services: |
| {{- contains .services }} |
| - e2e::productpage |
| - e2e::reviews |
| {{- end }} |
| ``` |
| |
| The `yq` projection is deliberate: pull out the two or three fields that carry the meaning, sort |
| anything order-dependent, and keep the expected file small enough to read. |
| |
| ## Two traps |
| |
| Both of these cost real debugging time, so they are commented in the cell files as well. |
| |
| **`/api/layer/:key/services` returns `reachable: true` with an empty list even when the OAP is |
| down.** Asserting on `.reachable` there proves nothing — the case would pass against a dead backend. |
| Assert on the service *names*, as the case above does. |
| |
| **infra-e2e's `notEmpty` matcher rejects numbers.** It only accepts nil or a string |
| (`notEmpty only supports nil or string type, but was ...`), so a numeric assertion has to be turned |
| into a boolean in the `yq` projection before it reaches the template. That is what the `service_cpm` |
| case does: |
| |
| ```yaml |
| ... | yq -p json -o yaml '{"reachable": .reachable, "id": .widgets[0].id, "positive": (.widgets[0].value != null and .widgets[0].value > 0)}' |
| ``` |
| |
| against `expected/horizon-service-cpm.yml`, which expects `positive: true` alongside |
| `reachable: true` and `id: cpm`. Use `notEmpty` for strings such as `.version`; compare numbers |
| yourself. |
| |
| ## Login: the chart ships no users |
| |
| `chart/skywalking/values.yaml` sets `ui.config: {}`, and Horizon has no built-in `admin/admin` |
| fallback — a fresh install has nobody who can log in until you configure `auth`, which is exactly |
| what [Set Up Logins](../ui/logins.md) is about. |
| |
| `test/e2e/values.yaml` seeds two local users — `admin`/`admin` (role `admin`) and |
| `skywalking`/`skywalking` (roles `viewer`, `maintainer`) — as argon2id hashes. **Every cell must |
| therefore pass `-f test/e2e/values.yaml`**, or the very first `horizon.sh` call fails at the login |
| step. That failure is the intended signal, not a flake. |
| |
| The same overlay carries the other two things the tests need on top of chart defaults: |
| |
| - `oap.config."metadata-service-mapping.yaml"` — the `e2e::` service naming described above. |
| - `elasticsearch.nodeSets` — a single 2Gi ES node with `node.store.allow_mmap: false` and relaxed |
| disk watermarks, so ES stays green on a kind node with little free disk. |
| |
| The first verify case in every cell (`GET /api/auth/me` against `expected/horizon-me.yml`) asserts |
| `username: admin` with `roles: [admin]`, which proves the overlay was applied before anything else |
| is tested. |
| |
| ## `test/e2e/env` — the image pin file |
| |
| Every cell loads this file via `init-system-environment`, so it is the single place where the |
| versions under test are pinned. |
| |
| | Variable | Pins | Moves? | |
| | --- | --- | --- | |
| | `OAP_REPO` / `OAP_TAG` | `docker.io/apache/skywalking-oap-server` : `11.0.0` | Yes — this is the current line. | |
| | `UI_REPO` / `UI_TAG` | `docker.io/apache/skywalking-ui` : `horizon-1.0.0` | Yes. Dev images live on GHCR (`apache/skywalking-horizon-ui`) if CI needs an unreleased fix. | |
| | `BANYANDB_REPO` / `BANYANDB_TAG` | `ghcr.io/apache/skywalking-banyandb` at commit `3b83e18…` | Yes. A GHCR commit pin rather than `docker.io/apache/skywalking-banyandb:0.11.0` **on purpose**: that commit *is* the v0.11.0 tag, and naming it pins the exact source under test instead of a tag that can be re-pushed. The release image is published; user-facing install docs quote it. | |
| | `SATELLITE_REPO` / `SATELLITE_TAG` | `ghcr.io/apache/skywalking-satellite/skywalking-satellite` at a commit tag | Yes. | |
| |
| The OAP 11 line moves as a trio: OAP 11.0.0 accepts BanyanDB server API 0.11 only, and Horizon 1.0.0 |
| is the UI tested against it — see [Version Compatibility](../evaluate/version-compatibility.md). |
| The BanyanDB *chart* version is not pinned here; it comes from `chart/skywalking/Chart.yaml`. |
| |
| ## Run one locally |
| |
| You need Docker and Go — the `e2e` binary embeds kind as a library, so no separate `kind` binary is |
| required. The setup steps install `yq`, `kubectl`, `istioctl` and `helm` into `/usr/local/bin` and |
| install Istio into the cluster, so run this on a machine you don't mind changing. |
| |
| Build the `e2e` CLI once: |
| |
| ```shell |
| git clone https://github.com/apache/skywalking-infra-e2e.git |
| cd skywalking-infra-e2e |
| make install DESTDIR=/usr/local/bin |
| ``` |
| |
| Then, **from the root of this repo** (the config files reference `chart/skywalking` and |
| `test/e2e/…` relative to the working directory): |
| |
| ```shell |
| export ISTIO_VERSION=1.24.0 |
| e2e run -c test/e2e/e2e-oap11-banyandb-standalone.yaml |
| ``` |
| |
| `ISTIO_VERSION` is set by the CI workflow, not by `test/e2e/env`, and both `install-istioctl.sh` and |
| the bookinfo manifest URLs read it — export it yourself when running locally. |
| |
| To iterate without re-creating the cluster, run the phases separately: |
| |
| ```shell |
| e2e setup -c test/e2e/e2e-oap11-banyandb-standalone.yaml |
| e2e verify -c test/e2e/e2e-oap11-banyandb-standalone.yaml # repeat as you debug |
| e2e cleanup -c test/e2e/e2e-oap11-banyandb-standalone.yaml # deletes the kind cluster |
| ``` |
| |
| While the cluster is up: |
| |
| ```shell |
| kubectl -n istio-system get pods |
| kubectl -n istio-system logs deploy/skywalking-oap |
| kubectl -n istio-system logs deploy/skywalking-ui # Horizon's BFF, where the API calls land |
| kubectl -n istio-system port-forward svc/skywalking-ui 8080:80 # then log in as admin/admin |
| ``` |
| |
| With that port-forward running you can also drive the script by hand, exactly as the verify phase |
| does: |
| |
| ```shell |
| bash test/e2e/script/horizon.sh http://localhost:8080 get /api/layer/MESH/services | yq -p json |
| ``` |
| |
| ## How CI runs them |
| |
| `.github/workflows/e2e.ci.yaml` runs the suite from one matrix job, `als`, with `fail-fast: false` |
| and a matrix of four entries — one per cell — each with a 60-minute timeout, so one failing storage |
| backend does not cancel the other four: |
| |
| ```yaml |
| strategy: |
| fail-fast: false |
| matrix: |
| test: |
| - name: Horizon + OAP 11 + Elasticsearch |
| config: test/e2e/e2e-oap11-elasticsearch.yaml |
| # …the two OAP 11 BanyanDB cells, and e2e-config-override.yaml |
| ``` |
| |
| Each entry logs in to `ghcr.io` (the Satellite and BanyanDB 0.11 images live there), sets up Go 1.24, |
| and hands its config file to the `apache/skywalking-infra-e2e` action, pinned to SHA `8c21e43e…`: |
| |
| ```yaml |
| - uses: apache/skywalking-infra-e2e@8c21e43e241a32a54bdf8eeceb9099eb27e5e9b4 |
| with: |
| e2e-file: $GITHUB_WORKSPACE/${{ matrix.test.config }} |
| ``` |
| |
| The workflow sets `ISTIO_VERSION: 1.24.0` in its top-level `env:`. On failure it dumps disk usage |
| and the local Docker images, then uploads `$SW_INFRA_E2E_LOG_DIR` as the `logs` artifact — start |
| there when a CI run fails but a local run passes. A trailing `build` job depends on `als` and only |
| runs `echo`, so one job name aggregates the whole matrix. |
| |
| The workflow runs on every `pull_request` and on pushes to `master`. The `paths-ignore: ['**.md']` |
| filter applies only to the push trigger, so a docs-only pull request still runs the full matrix. |
| |
| ## Adding a case |
| |
| 1. Add the `query:` to **all three traffic cells**, and put the expectation in |
| `test/e2e/expected/`. Leave `e2e-config-override.yaml` alone unless the case is about |
| configuration: it installs no Istio and generates no traffic, so anything asking for a metric |
| will never pass there. |
| 2. Go through `test/e2e/script/horizon.sh`. Anything that calls the OAP directly is testing the OAP. |
| 3. Query the `MESH` layer, and project the response with `yq` down to the fields that carry meaning. |
| 4. Keep image references as `$OAP_REPO` / `$OAP_TAG` style variables so `test/e2e/env` stays the only |
| place versions are pinned. |
| |
| ## Adding a cell |
| |
| Copy the closest existing file, change the storage flags, and **add a matrix entry in |
| `.github/workflows/e2e.ci.yaml`** — a config file that is not in the matrix never runs. See |
| [Elasticsearch](../storage/elasticsearch.md) and [BanyanDB](../storage/banyandb.md) for what those |
| storage flags mean outside the tests. |