Technical Documentation: hashicorp/consul
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
Consul
Consul is a distributed, highly available, and data center aware solution to connect and configure applications across dynamic, distributed infrastructure.
- Documentation and Tutorials: [https://developer.hashicorp.com/consul]
- Forum: Discuss
Consul provides several key features:
Multi-Datacenter - Consul is built to be datacenter aware, and can
support any number of regions without complex configuration.Service Mesh - Consul Service Mesh enables secure service-to-service
communication with automatic TLS encryption and identity-based authorization. Applications
can use sidecar proxies in a service mesh configuration to establish TLS
connections for inbound and outbound connections with Transparent Proxy.API Gateway - Consul API Gateway manages access to services within Consul Service Mesh,
allow users to define traffic and authorization policies to services deployed within the mesh.Service Discovery - Consul makes it simple for services to register
themselves and to discover other services via a DNS or HTTP interface.
External services such as SaaS providers can be registered as well.Health Checking - Health Checking enables Consul to quickly alert
operators about any issues in a cluster. The integration with service
discovery prevents routing traffic to unhealthy hosts and enables service
level circuit breakers.Dynamic App Configuration - An HTTP API that allows users to store indexed objects within Consul,
for storing configuration parameters and application metadata.
Consul runs on Linux, macOS, FreeBSD, Solaris, and Windows and includes an
optional browser based UI. A commercial version
called Consul Enterprise is also
available.
Please note: We take Consul's security and our users' trust very seriously. If you
believe you have found a security issue in Consul, please responsibly disclose
by contacting us at [email protected].
Quick Start
A few quick start guides are available on the Consul website:
- Standalone binary install: https://learn.hashicorp.com/collections/consul/get-started-vms
- Minikube install: https://learn.hashicorp.com/tutorials/consul/kubernetes-minikube
- Kind install: https://learn.hashicorp.com/tutorials/consul/kubernetes-kind
- Kubernetes install: https://learn.hashicorp.com/tutorials/consul/kubernetes-deployment-guide
- Deploy HCP Consul: https://learn.hashicorp.com/tutorials/consul/hcp-gs-deploy
Documentation
Full, comprehensive documentation is available on the Consul website: https://developer.hashicorp.com/consul/docs
Contributing
Thank you for your interest in contributing! Please refer to CONTRIBUTING.md
for guidance. For contributions specifically to the browser based UI, please
refer to the UI's README.md
for guidance.
Overview (`api/README.md`)
Consul API Client
This package provides the api package which provides programmatic access to the full Consul API.
The full documentation is available on Godoc.
Usage
Below is an example of using the Consul client. To run the example, you must first
install Consul and
Go.
To run the client API, create a new Go module.
go mod init consul-demoCopy the example code into a file called main.go in the directory where the module is defined.
As seen in the example, the Consul API is often imported with the alias capi.
package main
import (
"fmt"
capi "github.com/hashicorp/consul/api"
)
func main() {
// Get a new client
client, err := capi.NewClient(capi.DefaultConfig())
if err != nil {
panic(err)
}
// Get a handle to the KV API
kv := client.KV()
// PUT a new KV pair
p := &capi.KVPair{Key: "REDIS_MAXCLIENTS", Value: []byte("1000")}
_, err = kv.Put(p, nil)
if err != nil {
panic(err)
}
// Lookup the pair
pair, _, err := kv.Get("REDIS_MAXCLIENTS", nil)
if err != nil {
panic(err)
}
fmt.Printf("KV: %v %s\n", pair.Key, pair.Value)
}Install the Consul API dependency with go mod tidy.
In a separate terminal window, start a local Consul server.
consul agent -dev -node machineRun the example.
go run .You should get the following result printed to the terminal.
KV: REDIS_MAXCLIENTS 1000After running the code, you can also view the values in the Consul UI on your local machine at http://localhost:8500/ui/dc1/kv
Overview (`bench/README.md`)
Consul Benchmark
This repo contains the automation necessary for the Consul benchmarks.
There is a single main Packer file bench.json. To use it, the variables
for do_client_id and do_api_key must be provided. These correspond to
your DigitalOcean client ID and API key.
When Packer runs, it will generate 3 images:
- bench-bootstrap - Consul server in bootstrap mode
- bench-server - Consul server
- bench-worker - Worker node
For the benchmark you should start 1 bootstrap instance, and 2 normal
servers. As many workers as desired can be started. Once the nodes are
up, you must SSH into one of the Consul servers.
Connect all the nodes with:
$ consul join <n1> ... <n5>This will connect all the nodes within the same datacenter.
To run the benchmarks, use the Makefile:
$ cd /
$ make # Runs all the benchmarks
$ make put # Runs only the PUT benchmarksThere is no good way to currently cause multiple workers to run at the same
time, so I just type in the make command and rapidly start the test on all
workers. It is not perfect, but the test runs long enough that the calls
overlap.
Overview (`sdk/README.md`)
Internal SDK
Please note that this folder, while public, is not meant for new consumers of
these libraries; this should currently be considered an internal, not external,
SDK. It is public due to existing needs from other HashiCorp software. The tags
in this folder will stay at the 0.x.y level; accordingly users should expect
that things can move around, disappear, or change API at any time.
Overview (`test-integ/README.md`)
test-integ
Go integration tests for consul. /test/integration also holds integration tests; they need migrating.
These should use the testing/deployer framework to bring
up some local testing infrastructure and fixtures to run test assertions against.
Where reasonably possible, try to bring up infrastructure interesting enough to
be able to run many related sorts of tests against it, rather than waiting for
many similar clusters to be provisioned and torn down. This will help ensure
that the integration tests do not consume CPU cycles needlessly.
Prerequisites
Before you can run these tests, a fresh dev build should be created and tagged
for automatic use by tests using utils.TargetImages(). This can be done by
the toplevel consul make target:
make test-deployer-setupYou can run the entire set of deployer integration tests using:
make test-deployer # everything except peering_commontopo
make test-deployer-peering # just peering_commontopoYou can also run them one by one if you like:
go test ./connect/ -run Test_Snapshot_Restore_Agentless -vYou can have the logs stream unbuffered directly to your terminal which can
help diagnose stuck tests that would otherwise need to fully timeout before the
logs would print by also setting the NOLOGBUFFER=1 environment variable.
Getting started
Deployer tests have three main parts:
- Declarative topology description.
- Launching the infrastructure defined by that description.
- Making test assertions about the infrastructure.
Some tests may also have an optional mutation phase followed by additional
assertions. These are only needed if the test needs to observe a reaction in
the system to a change in the environment or configuration.
Topology description
Test authors craft a declarative description of the infrastructure necessary to exist for the test.
These are also referred to as a "topology".
These are comprised of 4 main parts:
Images: The set of docker images and specific versions that will be used
by default if not overridden on each Cluster or Node.- Consul CE
- Consul Enterprise
- Consul Dataplane
- Envoy Proxy
Networks: The non-overlapping networks that should exist for use by the Clusters.
Clusters: The unique Consul clusters that should exist.
Nodes: A "box with ip address(es)". This should feel a bit like a VM or
a Kubernetes Pod as an enclosing entity.- Workloads: The list of service instances that will execute on the given node.
InitialConfigEntries: Config entries that should be created as
part of the fixture and that make sense to
include as part of the test definition, rather
than something created during the test assertion
phase.InitialResources: Resources that should be created as part of
the fixture and that make sense to include as part of
the test definition, rather than something created
during the test assertion phase.
Peerings: The peering relationships between Clusters to establish.
In the topoutil package there are some helpers for defining
common sets of nodes or workloads like Consul Servers, Mesh Gateways, or fortio
servers
Useful topology concepts
Consul has a lot of independent configurables that can greatly increase the
testing configuration space required to flush out any bugs. The topology
definition was designed to be easily "exploded" to create testing microcosms on
a variety of axes:
- agentful (clients) vs agentless (dataplane)
- tenancies (partitions, namespaces)
- locally or across a peering
Since the topology is just a declarative struct, a test author could rewrite
any one of these attributes with a single field (such as Node.Kind) and cause
the identical test to run against the other configuration. With the addition of
a few if enterprise {} blocks and for loops, a test author could easily
write one test of a behavior and execute it to cover agentless, agentful, and
non-default tenancy in a few extra lines of code.
Non-optional security settings
The test framework always enables ACLs in default deny mode and provisions
minimal-permission tokens automatically to the various containers that need
them.
TLS certificates are similarly minted and distributed to all components that
need them.
Launching a topology
There is a sprawltest package that has
utilities to bring up a topology in the context of a Go test. This is basically a one-liner:
sp := sprawltest.Launch(t, config)After this line returns you will have a handle (sp) to the running cluster
and can use it to get ready-made api clients, http clients, gRPC resource
client, or test sockets open to a variety of the topology components for use in
authoring test code.
This helper will rig up a t.Cleanup handler that will destroy all resources
created during the test. This can be opted-out of by setting theSPRAWL_KEEP_RUNNING=1 environment variable before running the tests.
Upgrade test
We are migrating upgrade tests from consul-container(/test/integration) to
this directory using the testing/deployer framework.
The current implementation supports two upgrade strategies: standard upgrade
and autopilot upgrade. The basic test scenario can be found in ./test-integ/upgrade/basic.
Test assertions
Typical service mesh tests want to ensure that use of a service from another
service behaves in a certain way. Because the entire set of components is known
declaratively, we can process it into a flat list of known source/destination
relationships:
ships := topology.ComputeRelationships()This works hand-in-hand with the topology concepts mentioned above to
programmatically verify independent subunits of a topology that may exist (this
is helpful for things like testing multiple tenancy configurations without
duplicating all of the assertion code).
This can also be pretty printed to the log for diagnostic purposes with:
t.Log(topology.RenderRelationships(ships))Which looks like this:
$ NOLOGBUFFER=1 go test ./catalogv2/ -run TestBasicL4ExplicitDestinations -v
...(skipping a bunch of output)...
2023-11-08T11:48:04.395-0600 [INFO] TestBasicL4ExplicitDestinations: topology is ready for use: elapsed=33.510298357s
explicit_destinations_test.go:55: DOWN |node |service |port |UP |service |
dc1 |default/dc1-box2 |default/default/single-client |5000 |dc1 |default/default/single-server |
dc1 |default/dc1-box4 |default/default/multi-client |5000 |dc1 |default/default/multi-server |
dc1 |default/dc1-box4 |default/default/multi-client |5001 |dc1 |default/default/multi-server |
| | | | | |
=== RUN TestBasicL4ExplicitDestinations/relationship:_default/default/single-client_on_default/dc1-box2_in_dc1_via_:5000_=>_default/default/single-server_in_dc1_port_http
service.go:224: making call to http://10.238.170.5:5000
service.go:245: ...got response code 200
=== RUN TestBasicL4ExplicitDestinations/relationship:_default/default/multi-client_on_default/dc1-box4_in_dc1_via_:5000_=>_default/default/multi-server_in_dc1_port_http
service.go:224: making call to http://10.238.170.7:5000
service.go:245: ...got response code 200
=== RUN TestBasicL4ExplicitDestinations/relationship:_default/default/multi-client_on_default/dc1-box4_in_dc1_via_:5001_=>_default/default/multi-server_in_dc1_port_http-alt
service.go:224: making call to http://10.238.170.7:5001
service.go:245: ...got response code 200
2023-11-08T11:48:04.420-0600 [INFO] TestBasicL4ExplicitDestinations.tfgen: Running 'terraform destroy'...
--- PASS: TestBasicL4ExplicitDestinations (40.60s)
--- PASS: TestBasicL4ExplicitDestinations/relationship:_default/default/single-client_on_default/dc1-box2_in_dc1_via_:5000_=>_default/default/single-server_in_dc1_port_http (0.01s)
--- PASS: TestBasicL4ExplicitDestinations/relationship:_default/default/multi-client_on_default/dc1-box4_in_dc1_via_:5000_=>_default/default/multi-server_in_dc1_port_http (0.01s)
--- PASS: TestBasicL4ExplicitDestinations/relationship:_default/default/multi-client_on_default/dc1-box4_in_dc1_via_:5001_=>_default/default/multi-server_in_dc1_port_http-alt (0.01s)
PASS
ok github.com/hashicorp/consul/test-integ/catalogv2 40.612sThere is a ready-made helper to assist with making common inquiries to Consul
and Envoy that you can create in your test:
asserter := topoutil.NewAsserter(sp)
asserter.UpstreamEndpointStatus(t, svc, clusterPrefix+".", "HEALTHY", 1)Overview (`types/README.md`)
Consul `types` Package
The Go language has a strong type system built into the language. Thetypes package corrals named types into a single package that is terminal ingo's import graph. The types package should not have any downstream
dependencies. Each subsystem that defines its own set of types exists in its
own file, but all types are defined in the same package.
Why
Everything should be made as simple as possible, but not simpler.
string is a useful container and underlying type for identifiers, however
the string type is effectively opaque to the compiler in terms of how a
given string is intended to be used. For instance, there is nothing
preventing the following from happening:
// `map` of Widgets, looked up by ID
var widgetLookup map[string]*Widget
// ...
var widgetID string = "widgetID"
w, found := widgetLookup[widgetID]
// Bad!
var widgetName string = "name of widget"
w, found := widgetLookup[widgetName]but this class of problem is entirely preventable:
type WidgetID string
var widgetLookup map[WidgetID]*Widget
var widgetNameTL;DR: intentions and idioms aren't statically checked by compilers. Thetypes package uses Go's strong type system to prevent this class of bug.
Overview (`ui/README.md`)
Consul UI Monorepo
This monorepo contains multiple projects, the UI for Consul and addons and
packages used by the UI.
This top-level repository provides limited common tasks, such as installation
and commit assistance. However, most tasks must be executed from within a
subproject, e.g. running or testing.
If you are looking to work on the Consul UI you probably want to read
the README that is in ./packages/consul-ui/README.md.
Table of Contents generated with DocToc
Prerequisites
You will need the following things properly installed on your computer.
- Git
- Node.js
- Pnpm installed globally
- Google Chrome
- Firefox
Installation
git clone https://github.com/hashicorp/consul.gitthis repositorycd uipnpm i
Pnpm Commands
List of available project commands. pnpm run <command-name>
| Command | Description |
|---|---|
| doc:toc | Re-builds the ToC for this README. |
| compliance:licenses | Checks that all dependencies have CE-compatible licenses. |
For Vercel preview deployments, the workspace-level configuration lives in./vercel.json.
Contributing
Building ToC
To autogenerate a ToC (table of contents) for this README,
run pnpm doc:toc. Please update the ToC whenever editing the structure
of README.
Overview (`agent/uiserver/README.md`)
uiserver
The contents of dist/ are generated by make ui in the root of this repo
which compiles ui assets and copies them here.
A CI job (publish-static-assets) will detect any diffs in ui files and
commit the compiled files. Avoid committing files manually to dist/.
Overview (`internal/go-sso/README.md`)
go-sso
This is a Go library that is being incubated in Consul to assist in doing
opinionated OIDC-based single sign on.
The go.mod.sample and go.sum.sample files are what the overall realgo.mod and go.sum files should end up being when extracted from the Consul
codebase.
Overview (`sdk/testutil/README.md`)
Consul Testing Utilities
This package provides some generic helpers to facilitate testing in Consul.
TestServer
TestServer is a harness for managing Consul agents and initializing them with
test data. Using it, you can form test clusters, create services, add health
checks, manipulate the K/V store, etc. This test harness is completely decoupled
from Consul's core and API client, meaning it can be easily imported and used in
external unit tests for various applications. It works by invoking the Consul
CLI, which means it is a requirement to have Consul installed in the $PATH.
Following is an example usage:
/* Detailed source-code truncated for AI context efficiency. */Overview (`test-integ/peering_commontopo/README.md`)
CONSUL PEERING COMMON TOPOLOGY TESTS
These peering tests all use a commonTopo (read: "common topology") to enable sharing a deployment of a Consul. Sharing a deployment of Consul cuts down on setup time.
To run these tests, you will need to have docker installed. Next, make sure that you have all the required consul containers built:
make test-deployer-setupNon-Shared CommonTopo Tests
The tests in question are designed in a manner that modifies the topology. As a result, it is not possible to share the testing environment across these tests.
Shared CommonTopo Tests
The tests in question are designed in a manner that does not modify the topology in any way that would interfere with other tests. As a result, it is possible to share the testing environment across these tests.
To run all consul peering tests with no shared topology, run the following command:
cd /path/to/peering_commontopo
go test -timeout=10m -v -no-share-topo .To run all peering tests with shared topology only:
cd /path/to/peering_commontopo
go test -timeout=10m -run '^TestSuitesOnSharedTopo' -v .To run individual peering topology tests:
cd /path/to/peering_commontopo
go test -timeout=10m -run '^TestSuiteExample' -v -no-share-topo .Local Development and Testing
If writing tests for peering with no shared topology, this recommendation does not apply. The following methods below not necessarily need to be implmented. For shared topology tests, all the methods in the sharedTopoSuite interface must be implemented.
testName()prepends the test suite name to each test in the test suite.setup()phase must ensure that any resources added to the topology cannot interfere with other tests. Principally by prefixing.test()phase must be "passive" and not mutate the topology in any way that would interfere with other tests.
Common topology peering tests are defined in the test-integ/peering_commontopo/ directory and new peering integration tests should always be added to this location. Adding integration tests that does not modify the topology should always start by invoking
runShareableSuites(t, testSuiteExample)else
func TestSuiteExample(t *testing.T) {
ct := NewCommonTopo(t)
s := &testSuiteExample{}
s.setup(t, ct)
ct.Launch(t)
s.test(t, ct)
}Some of these tests do mutate in their test() phase, and while they use commonTopo for the purpose of code sharing, they are not included in the "shared topo" tests in sharedtopology_test.go.
Overview (`test/load/README.md`)
Terraform Consul Load Testing
Consul Load Testing is used to capture baseline performance metrics for Consul under stress. This will assist in ensuring there are no performance regressions during releases and substantial changes to Consul. Per the defaults, the test runs for 10 minutes with 25 virtual users spawned by k6. Instance sizes for test instances are t2.small and for the Consul cluster m5n.large. All metrics from Consul are pushed to a datadog dashboard for user review.
This relies on the Gruntwork's Terraform AWS Consul Module which by default creates 3 Consul servers across 3 availability zones. A load test instance which has an image that is configured with the necessary scripts and k6 is created and sends traffic to a load balancer. The load balancer will distribute requests across the Consul clients who will ultimately forward the requests to the servers.
How to use
Terraform and Packer, AWS and Datadog are required to use this. All of this, except the AWS resources that will be utilized, are free.
This repo has the following folder structure:
- packer: This contains all the necessary stuff to make the load test and the Consul AMI to be utilized in Terraform.
- terraform: This contains all the relevant Terraform files
Getting Started
- Download all necessary tools listed (Terraform, Packer)
- Set up an AWS account and a Datadog account - downloading the Datadog client is not necessary.
- Configure your AWS credentials using one of the options supported by the AWS
SDK. Usually, the easiest option is to
set theAWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, andAWS_DEFAULT_REGIONenvironment variables. - Follow the Packer README to generate your load test and Consul AMIs
- Follow the Terraform README to stand up the infrastructure in AWS
- Watch the results in either your datadog dashboard or in the output of your Terraform Apply
Debugging in Datadog
Consul has hundreds of metrics to choose from. We recommend reading over Datadog's article that breaks down relevant Consul metrics, especially on their "communication" portion. This test runs solely based on making requests to endpoints which is what makes that section so relevant.
Overview (`testing/deployer/README.md`)
Summary
This is a Go library used to launch one or more Consul clusters that can be
peered using the cluster peering feature. Under the covers terraform is used
in conjunction with thekreuzwerker/docker
provider to manage a fleet of local docker containers and networks.
Configuration
The complete topology of Consul clusters is defined using a topology.Config
which allows you to define a set of networks and reference those networks when
assigning nodes and workloads to clusters. Both Consul clients andconsul-dataplane instances are supported.
Here is an example configuration with two peered clusters:
/* Detailed source-code truncated for AI context efficiency. */Once you have a topology configuration, you simply call the appropriateLaunch function to validate and boot the cluster.
You may also modify your original configuration (in some allowed ways) and callRelaunch on an existing topology which will differentially adjust the running
infrastructure. This can be useful to do things like upgrade instances in place
or subly reconfigure them.
For Testing
It is meant to be consumed primarily by unit tests desiring a complex
reasonably realistic Consul setup. For that use case use the sprawl/sprawltest wrapper:
func TestSomething(t *testing.T) {
cfg := &topology.Config{...}
sp := sprawltest.Launch(t, cfg)
// do stuff with 'sp'
}Overview (`test/load/packer/README.md`)
Terraform Consul Load Testing
Packer will output AMI IDs when it completes - save these AMI IDs as Terraform will require them later.
The generated ami will have common tag Environment = "consul-load-test".
==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
us-east-1: ami-19601070Consul AMI:
Within the consul-ami/ directory
- Retrieve your Datadog API key, set this as an environment variable, ex:
export DD_API_KEY=$YOURDDAPIKEYHERE - Set the AWS_DEFAULT_REGION for Packer, ex:
export AWS_DEFAULT_REGION=us-east-1 - Set up AWS credentials, ex:
export AWS_ACCESS_KEY_ID=#YOURAWSKEYID & export AWS_SECRET_ACCESS_KEY=YOURAWSKEY - Run
packer build consul.pkr.hcl.
For additional customization you can add tags within the scripts/datadog.yaml file. An example of a tag could be "consul_version" : "consulent_175". These tags are searchable through the datadog dashboard. Another form of customization is changing the datacenter tag within scripts/telemetry.json, however it is defaulted to us-east-1.
Load Test AMI
Within the loadtest-ami/ directory
- Set the AWS_DEFAULT_REGION for Packer, ex:
export AWS_DEFAULT_REGION=us-east-1 - Run the command
packer build loadtest.pkr.hcl
The script that k6 runs is found within scripts/loadtest.js. This script can be updated to send requests to more Consul endpoints. For additional information on k6 please check out their guides.
Overview (`test/load/terraform/README.md`)
Terraform Consul Load Testing
How to use
- Build an image with the desired Consul version and a load test image in the Packer folder here.
- Create your own
vars.tfvarsfile in this directory. - Place the appropriate AMI IDs in the
consul_ami_idandtest_server_amivariables. If no AMI ID is specified it will default
to pulling from latest. - Set either
consul_versionorconsul_download_url. If neither is set it will default to utilizing Consul 1.9.0 - AWS Variables are set off of environment variables. Make sure to export necessary variables shown here.
- Run
terraform initonce to setup the working directory. - Run
terraform plan -var-file=vars.tfvars, and thenterraform apply -var-file=vars.tfvarswhen ready. - Upon completion k6 should run and push metrics to the desired Datadog dashboard.
An example of a vars.tfvars :
vpc_name = "consul-test-vpc"
vpc_cidr = "11.0.0.0/16"
vpc_allwed_ssh_cidr = "0.0.0.0/0"
public_subnet_cidrs = ["11.0.1.0/24", "11.0.3.0/24"]
private_subnet_cidrs = ["11.0.2.0/24"]
vpc_az = ["us-east-2a", "us-east-2b"]
test_instance_type = "t2.micro"
test_server_ami = "ami-0ad7711e837ebe166"
cluster_name = "ctest"
test_public_ip = "true"
instance_type = "t2.micro"
ami_owners = ["******"]
consul_ami_id = "ami-016d80ff5472346f0"Note that vpc_allwed_ssh_cidr must be set to allowed the test server to be accessible from the
machine running the load test, e.g., "0.0.0.0/0" (It is disabled by default).
Customization
All customization for infrastructure that is available can be found by looking through the variables.tf file.
How to SSH
After terraform apply is run Terraform should create a keys/ directory which will give access to all instances created.
For example, ssh -i "keys/[cluster-name]-spicy-banana.pem" ubuntu@[IPADDRESS]
Overview (`ui/packages/consul-ui/README.md`)
consul-ui
Prerequisites
You will need the following things properly installed on your computer.
Node.js (with npm)
Installation
git clone https://github.com/hashicorp/consul.gitthis repositorycd ui/packages/consul-ui
then:
To run the UI
From within ui/packages/consul-ui directory run:
make start
To run tests
From within ui/packages/consul-ui directory run:
make test-oss-viewwhich will run the tests in Chrome
(see below and/or the testing section of the engineering docs for
further detail)
Pnpm Commands
Most used tooling scripts below primarily use make which will pnpm install
and in turn call node package scripts.
List of available project commands. pnpm run <command-name>
| Command | Description |
|---|---|
| build:staging | Builds the UI in staging mode (ready for PR preview site). |
| build:ci | Builds the UI for CI. |
| build | Builds the UI for production. |
| lint | Runs all lint commands. |
| lint:hbs | Lints hbs template files. |
| lint:js | Lints js files. |
| format | Runs all auto-formatters. |
| format:js | Auto-formats js files using Prettier. |
| format:sass | Auto-formats scss files using Prettier. |
| start | Runs the development app on a local server using the mock API. |
| start:consul | Runs the development app local server using a real consul instance as the backend. |
| start:staging | Runs the staging app local server. |
| test | Runs the ember tests in a headless browser. |
| test:view | Runs the ember tests in a non-headless browser. |
| test:oss | Runs only the OSS ember tests in a headless browser. |
| test:oss:view | Runs only the OSS ember tests in a non-headless browser. |
| test:coverage:view | Runs only the test specified for coverage in a non-headless browser. |
| test:node | Runs tests that can't be run in ember using node. |
| doc:toc | Automatically generates a table of contents for this README file. |
Running / Development
The source code comes with a small development mode that runs enough of the consul API
as a set of mocks/fixtures to be able to run the UI without having to run
consul.
make startorpnpm startto start the ember app- Visit your app at http://localhost:4200.
You can also run the UI against a normal Consul installation.
consul agent -devto start consul listening on http://localhost:8500make start-consulto start the ember app proxying toconsul(this will
respect theCONSUL_HTTP_ADDRenvironment variable to locate the Consul
installation.- Visit your app at http://localhost:4200.
Example:
CONSUL_HTTP_ADDR=http://10.0.0.1:8500 make start-consulEnvironment Variables
See ./docs/index.mdx
Branching
We follow a ui/**/** branch naming pattern. This branch naming pattern allows
front-end focused builds, such as FE tests, to run automatically in Pull
Requests. Please note this only works if you are a member of the HashiCorp
GitHub Org. If you are an external contributor, these tests won't run and will
instead be run by a member of our team during review.
Examples:
ui/feature/add...ui/bugfix/fix...ui/enhancement/update...
Contributing/Engineering Documentation
We have an in-app (only during development) component storybook and
documentation site which can be visited using the Eng
Docs link in the top navigation of the UI.
Alternatively all of these docs are also readable via GitHub's UI, so folks can
use whatever works best for them.
Browser 'Debug Utility' Functions and 'Environment' Variables
Run make start then visit http://localhost:4200/ui/docs/bookmarklets for a
list of debug/engineering utilities you can use to help development of the UI
under certain scenarios.
Code Generators
Many classes used in the UI can be generated with ember generators, try ember help generate for more details
Running Tests
Tests use the mock api (see ./mock-api for details), the mock-api runs
automatically during testing, you don't need to run anything separately from
the below commands in order for the tests to use the mock-api.
make testorpnpm run testmake test-vieworpnpm run test:viewto view the tests running in Chrome
For more guidance on running tests, see the testing section of the engineering docs.
OSS only tests can also be run using:
make test-ossorpnpm run test:ossmake test-oss-vieworpnpm run test:oss:viewto view the tests running in Chrome
Linting
make lint currently runs linting on the majority of js files and hbs files (using ember-template-lint).
See .eslintrc.js and .eslintignore for specific configuration.
Building
make buildbuilds the UI for production usage (env=production)make build-cibuilds the UI for CI/test usage (env=test)
Static files are built into ./dist
Running Tests in Parallel
You probably don't need to understand this if you are simply running some
tests locally.
Alternatively, ember-exam can be used to split the tests across multiple browser instances for faster results. Most options are the same as ember test. To see a full list of options, run ember exam --help.
Note: The EMBER_EXAM_PARALLEL environment variable must be set to override the default parallel value of 1 browser instance in testem.js.
To quickly run the tests across 4 parallel browser instances:
make test-parallelTo run manually:
$ EMBER_EXAM_PARALLEL=true ./node_modules/.bin/ember exam --split <num> --parallelMore ways to split tests can be found in the ember-exam README.md.
Vercel Deploys
A Vercel preview deploy Github action triggers after each pushed change in a pull request. Vercel checks if there was changes within the UI folder in the last commit. If there are no changes the build is cancelled. If the build proceeds it will include a link to the preview deploy in your PRs feed.
If you were to push changes to the UI folder and then immediately follow it up with a change to something outside of the UI folder, the lastest Vercel build will be ignored. Because of this, the preview link will not get posted to the PR feed even if the original Vercel build completes. If that is the case, you may browse the Vercel deploys for the original build.
Another scenario to watch for is when you rebase a series of commits, but the last commit is outside of the UI folder. Doing this will not trigger a new Vercel deploy as the last commit doesn't have any changes in the UI folder.
Overview (`test/integration/connect/envoy/README.md`)
Envoy Integration Tests
Overview
These tests validate that Consul is configuring Envoy correctly. They set up various scenarios using Docker containers and then run
Bats (a Bash test framework) tests to validate the expected results.
Running Tests
To run the tests locally, cd into the root of the repo and run:
make test-envoy-integTo run a specific test, run:
make test-envoy-integ GO_TEST_FLAGS="-run TestEnvoy/case-basic"Where case-basic can be replaced by any directory name from this directory.
How Do These Tests Work
- The tests are all run through Go test via the
main_test.gofile. Each directory prefixed bycase-is a subtest, for example,TestEnvoy/case-basicandTestEnvoy/case-wanfed-gw. - The real framework for this test suite lives in
run-tests.sh. Under the hood,main_test.gojust runsrun-tests.shwith
various arguments. - The tests use your local code by building a Docker image from your local directory just before executing.
Note: this is implemented as thedocker-envoy-integMakefile target which is a prerequisite to thetest-envoy-integtarget,
so if you are running the tests by invokingrun-tests.shorgo testmanually, be sure to rebuild the Docker image to ensure
you are running your latest code. - The tests run Docker containers connected by a shared Docker network. All tests have at least one Consul server running and then
depending on the test case they will spin up additional services or gateways. Some tests run multiple Consul servers to test
multi-DC setups. See thecase-wanfed-gatewaytest for an example of this. - At a high level, tests are set up by executing the
setup.shscript in each directory. This script uses helper functions
defined inhelpers.bash. Once the test case is set up, the validations inverify.batsare run. - If there exists a
vars.shfile in the top-level of the case directory, the test runner will source it prior to invoking
therun_tests,test_teardownandcapture_logsphases of the test scenario. - If there exists a
capture.shfile in the top-level of the case directory, it will be executed after the test is done, but prior to
the containers being removed. This is useful for capturing logs or Envoy snapshots for debugging test failures. - Any files matching the
*.hclglob will be copied to the container$WORKDIR/$CLUSTER/consuldirectory prior to running the tests.
This is useful for defining Consul configuration for each agent process to load on start up. - In CI, the tests are executed against different Envoy versions and with both
XDS_TARGET=clientandXDS_TARGET=server.
If set toclient, a Consul server and client are run, and services are registered against the client. If set toserver,
only a Consul server is run, and services are registered against the server. By default,XDS_TARGETis set toserver.
See this comment for more information.
Investigating Test Failures
- When tests fail in CI, logs and additional debugging data are available in the artifacts of the test run.
- You can re-run the tests locally by running
make test-envoy-integ GO_TEST_FLAGS="-run TestEnvoy/<case-directory>"where<case-directory>is
replaced with the name of the directory, e.g.case-basic. - You can override the envoy version by specifying
ENVOY_VERSION=<your envoy version>eg.ENVOY_VERSION=1.27.0 make test-envoy-integ. - Locally, all the logs of the failed test will be available in
workdirin this directory. - You can run with
DEBUG=1to print out all the commands being run, e.g.DEBUG=1 make test-envoy-integ GO_TEST_FLAGS="-run TestEnvoy/case-basic". - If you want to prevent the Docker containers from being spun down after test failure, add a
sleep 9999to theverify.batstest case that's failing.
Creating a New Test
Below is a rough outline for creating a new test. For the example, assume our test case will be called my-feature.
- Create a new directory named
case-my-feature - If the test involves multiple datacenters/clusters, create a separate subdirectory for each cluster (eg.
case-my-feature/{dc1,dc2}) - Add any necessary configuration to
*.hclfiles in the respective cluster subdirectory (or the test case directory when using a single cluster). - Create a
setup.shfile in the case directory - Create a
capture.shfile in the case directory - Create a
verify.batsfile in the case directory - Populate the
setup.sh,capture.shandverify.batsfiles with the appropriate code for running your test, validating its state and capturing any logs or snapshots.
Overview (`ui/packages/consul-ui/mock-api/README.md`)
consul-api-double
Super simple API 'double' for easily visualizing the Consul API via the filesystem.
This can be served via either a simple javascript HTTP server, or entirely via frontend methods if your project/circumstances means you are unable to use or it's complicated to use a backend server for testing.
See @hashicorp/api-double for more details.
Overview (`test/integration/consul-container/test/upgrade/README.md`)
Upgrade Integration Tests
Introduction
The goal of upgrade tests is to ensure problem-free upgrades on supported upgrade paths.
At any given time, Consul supports the latest minor release, and two older minor releases, e.g. 1.15, 1.14, and 1.13.
Upgrades to any higher version are permitted, including skipping a minor version e.g. from 1.13 to 1.15.
The upgrade tests also aim to highlight errors that may occur as users attempt to upgrade their current version to a newer version.
How it works
This diagram illustrates the deployment architecture of an upgrade test, where
two consul agents (one server and one client), a static-server, static-client,
and envoy sidecars are deployed.
Note that all consul agents and user workloads such as application services, mesh-gateway are running in docker containers.
In general, each upgrade test has the following steps:
- Create a cluster with a specified number of server and client agents, then enable the feature to be tested.
- Create some workload in the cluster, e.g., registering 2 services: static-server, static-client.
Static-server is a simple http application and the upstream service of static-client. - Make additional configuration to the cluster. For example, configure Consul intention to deny
connection between static client and server. Ensure that a connection cannot be made. - Upgrade Consul cluster to the
target-versionand restart the Envoy sidecars
(we restart Envoy sidecar to ensure the upgraded Consul binary can read the state from
the previous version and generate the correct Envoy configurations) - Re-validate the client, server and sidecars to ensure the persisted data from the previous
version can be accessed in the target version. Verify connection / disconnection
(e.g., deny Action)
Getting Started
Prerequisites
To run the upgrade test, the following tools are required:
- install Go (the version should match that of our CI config's Go image).
- install
golangci-lint - install
Makefile Dockerrequired to run tests locally
Running Upgrade integration tests
- run
make dev-docker - build a consul-envoy container imageshell
cd test/integration/consul-container docker build -t consul-envoy:latest-version \ --build-arg CONSUL_IMAGE=docker.mirror.hashicorp.services/consul:1.20.0-rc1 \ --build-arg ENVOY_VERSION=1.31.2 -f ./assets/Dockerfile-consul-envoy ./assets - run the single test
go test -v -timeout 30m -run ^TestACL_Upgrade_Node_Token$ ./.../upgrade/ - run all upgrade tests
go test -v -timeout 30m -run ./.../upgrade
To specify targets and latest image pass --target-version and --latest-version
to the tests. By default, it uses the consul docker image with respectivelylocal and latest tags.
To use dev consul image, pass target-image and target-version:
-target-image hashicorppreview/consul -target-version 1.15-devBy default, all container's logs are written to either stdout, or stderr;
this makes it hard to debug, when the test case creates many containers. To
disable following container logs, run the test with -follow-log false.
Below are the supported CLI options
| Flags | Default value | Description |
|---|---|---|
| --latest-image | consul in CE, hashicorp/consulenterprise in ENT |
Name of the Docker image to deploy initially. |
| --latest-version | latest | Tag of the Docker image to deploy initially. |
| --target-image | consul in Ce, hashicorp/consulenterprise in ENT |
Name of the Docker image to upgrade to. |
| --target-version | local | Tag of the Docker image to upgrade to. local is the tag built by make dev-docker above. |
| -follow-log | true | Emit all container logs. These can be noisy, so we recommend --follow-log=false for local development. |
Adding a new upgrade integration test
All upgrade tests are defined in test/integration/consul-container/test/upgrade subdirectory.
Following is a guide for adding a new upgrade test case.
- Create consul cluster(s) with a specified version. Some utility functions are provided to make
a single cluster or two peered clusters:
// NewCluster creates a single cluster
cluster, _, _ := libtopology.NewCluster(t, &libtopology.ClusterConfig{
NumServers: 1,
NumClients: 1,
BuildOpts: &libcluster.BuildOptions{
Datacenter: "dc1",
ConsulVersion: utils.LatestVersion,
},
})Or
// BasicPeeringTwoClustersSetup creates two peered clusters, named accpeting and dialing
accepting, dialing := libtopology.BasicPeeringTwoClustersSetup(t, utils.LatestVersion, false)Some workloads may require extra resources. They should be created in this setup section. For example,
[https://github.com/hashicorp/consul-enterprise/blob/19e515db29541132dbbda73efb7a458cd29d705f/test/integration/consul-container/test/upgrade/peering_http_test.go#L30-L41](this peering test creates a second static-server).
- Verify the workload
libassert.HTTPServiceEchoes(t, "localhost", port, "")
libassert.AssertFortioName(t, fmt.Sprintf("http://localhost:%d", appPort), "static-server-2-v2", "")- Call the
StandardUpgrademethod and check that the upgrade succeeded.
We also restart the Envoy proxy to make sure the upgraded agent can generate
the correct Envoy configurations.
require.NoError(t,
cluster.StandardUpgrade(t, context.Background(), utils.TargetVersion))
require.NoError(t, staticServerConnectProxy.Restart())
require.NoError(t, staticClientConnectProxy.Restart())- Verify the workload from step 3 again
libassert.HTTPServiceEchoes(t, "localhost", port, "")
libassert.AssertFortioName(t, fmt.Sprintf("http://localhost:%d", appPort), "static-server-2-v2", "")For longer verifications, it can be nice to make a local function instead:
tests := func() {
libassert.HTTPServiceEchoes(t, "localhost", port, "")
libassert.AssertFortioName(t, fmt.Sprintf("http://localhost:%d", appPort), "static-server-2-v2", "")
}
tests()
// ... do upgrade
tests()Errors Test Cases
There are some caveats for special error handling of versions prior to 1.14.
Upgrade tests for features such as peering had API changes that return an error if an upgrade is attempted, and should be accounted for in upgrade tests. If running upgrade tests for any version before 1.14, the following lines of code need to be added to skip it or it will not pass.
fromVersion, err := version.NewVersion(utils.LatestVersion)
require.NoError(t, err)
if fromVersion.LessThan(utils.Version_1_14) {
t.Skip("...")
}See example here
To write tests for bugs found during upgrades, see example on how to add a testcase for those scenarios here.
FAQS
Q. Are containers' ports (e.g., consul's 8500, envoy sidecar's admin port
or local upstream port) exposed on the docker host?
A. Yes, they are exposed. However, they are exposed through a pod container.
That is, a consul agent and the envoy proxy containers registered with the agent
share the same Linux network namespace (i.e., they share localhost) as the pod container.
The pod container use the same prefix as the consul agent in its name.
Q. To troubleshoot, how can I send API request or consul command to the deployed cluster?
A. To send an API request or command to the deployed cluster, ensure that a cluster, services and sidecars have been created. See example below:
cluster, _, _ := topology.NewCluster()
clientService := createServices(t, cluster)
_, port := clientService.GetAddr()
_, adminPort := clientService.GetAdminAddr()
...
time.Sleep(900 * time.Second)
fmt.Println(port, adminPort)Then in your terminal docker ps -a | grep consul to get the running services and cluster. Exec in the cluster and run commands directly or make API request to localhost:port to relevant service or localhost:adminPort for envoy.
Q. To troubleshoot, how can I access the envoy admin page?
A. To access envoy admin page, ensure that a cluster, services and sidecars have been created. Then get the adminPort for the client or server sidecar. See example on how to get the port above. Then navigate to a browser and go to the url http://localhost:adminPort/
Q. My test is stuck with the error "could not start or join all agents: container 0: port not found"?
A. Simply re-run the tests. If the error persists, prune docker images docker system prune, run make dev-docker, then re-run tests again.
Q. How to clean up the resources created the upgrade test?
A. Run the command docker ps | grep consul to find all left over resources, then docker stop {CONTAINER_ID} && docker rm {CONTAINER_ID}
Overview (`ui/packages/consul-ui/docs/a11y-audit/README.md`)
Consul UI accessibility audit
Tracks a systematic a11y audit of consul-ui, broken into independent, per-section
tasks. Audit only — findings are documented here with suggested fixes, but
no fixes are applied as part of this work.
Method (applies to every task file)
Automated: two independent rule engines are run against each representative
page/state, since they catch different things:- Chrome DevTools Lighthouse accessibility audit (axe-core-based).
- IBM's
accessibility-checker(the open-source engine behind DevTools'
"Accessibility Assessment" panel — that panel and "Screen Reader Simulator"
themselves aren't drivable by any available automation tool, since they're
proprietary Chrome-extension UI with no exposed API; running the engine
directly via its Node API is the closest equivalent). Installed standalone
in the scratchpad dir (npm install accessibility-checker), not added
as a project dependency. Run via a small script callingaChecker.getCompliance(url, label)against the dev server. - In this pass IBM's checker surfaced 3 concrete bugs axe/Lighthouse missed
entirely (findings #5–#7 in 01-shared-chrome.md),
plus independently confirmed the contrast finding (#1) both tools agree on.
Worth running both engines on every task, not just one.
Reproducing the IBM scan (scratchpad install, not a project dependency —
recreate this each session, it doesn't persist):bashmkdir a11y-ibm && cd a11y-ibm && npm init -y && npm install accessibility-checkerjs// scan.js const aChecker = require("accessibility-checker"); (async () => { const { report } = await aChecker.getCompliance("http://localhost:4200/ui/dc1/services", "services"); for (const it of report.results) { if (it.level === "violation" || it.level === "potentialviolation") { console.log(`[${it.level}] ${it.ruleId} :: ${it.message}`); console.log(" path:", it.path && it.path.dom); } } await aChecker.close(); })();Run with
node scan.js. Swap the URL/label per page being audited.Manual keyboard: Tab/Shift+Tab/Enter/Escape/Arrow-key traversal, checking
focus order, visible focus indication, keyboard operability of all
interactive elements (dropdowns, modals, tables, forms), and no keyboard traps.Screen-reader-equivalent spot checks: inspection of the accessibility
tree (roles, accessible names, states, live regions) that Chrome DevTools
exposes. This is not a live run of VoiceOver/NVDA/JAWS — no screen
reader automation is available in this environment. Tree data is a good
proxy for what would be announced, but doesn't substitute for a real AT
pass before shipping fixes.
Findings use axe's severity vocabulary: Critical / Serious / Moderate / Minor.
Environment used
Dev server at localhost:4200, with packages/consul-ui/config/environment.jsoperatorConfig set to ACLsEnabled: true, NamespacesEnabled: true, PeeringEnabled: true, PartitionsEnabled: true (already the case on this
branch), V2CatalogEnabled: false, HCPEnabled: false. The V2 catalog / HCP
shell is a different application mode, not just extra routes, and is out of
scope for this pass.
Run with pnpm start (the mock API, see mock-api) — exportCONSUL_SERVICE_MIN=2 first so the randomly-generated service count can
never collapse to a single, non-mesh service (see
task-4-mock-data-plan.md for why that matters). A
stable backend service with a sidecar proxy — real Upstreams andExpose.Paths — is now always present in the services list regardless of
that random count, so dc.services.instance.upstreams/.exposedpaths have
a reliable, reproducible target to audit against.
Tasks
| # | Task | Routes | Status | File |
|---|---|---|---|---|
| 1 | Shared chrome & global patterns | app chrome, settings, unavailable, notfound |
Complete (7 findings) | 01-shared-chrome.md |
| 2 | Overview | dc.show(.serverstatus/.cataloghealth/.license) |
Complete (1 new finding + confirmed recurrences) | 02-overview.md |
| 3 | Services (list & detail) | dc.services(.show.*), dc.routing-config |
Complete (3 new findings) | 03-services.md |
| 4 | Service instance detail | dc.services.instance.* |
Complete (1 finding + confirmed recurrences on all 5 sub-tabs) | 04-service-instance.md |
| 5 | Nodes | dc.nodes(.show.*) |
Complete (1 high-reach finding) | 05-nodes.md |
| 6 | Key/Value | dc.kv.* |
Complete (2 minor findings) | 06-kv.md |
| 7 | Intentions (top-level) | dc.intentions.* |
Complete (1 finding, upstream HDS) | 07-intentions.md |
| 8 | Access Controls — Tokens | dc.acls.tokens.* |
Complete (no new findings; 1 tool false-positive documented) | 08-acls-tokens.md |
| 9 | Access Controls — Policies | dc.acls.policies.* |
Complete (no new findings) | 09-acls-policies.md |
| 10 | Access Controls — Roles | dc.acls.roles.* |
Complete (no new findings) | 10-acls-roles.md |
| 11 | Access Controls — Auth Methods | dc.acls.auth-methods.* |
Complete (no new findings; corroborates #3) | 11-acls-auth-methods.md |
| 12 | Peers | dc.peers.* |
Complete (no new findings; .show blocked on no peered clusters in mock data) |
12-peers.md |
| 13 | Admin Partitions | dc.partitions.* |
Complete (no new findings) | 13-partitions.md |
| 14 | Namespaces | dc.nspaces.* |
Complete (1 finding, 3rd occurrence of a recurring pattern) | 14-namespaces.md |
Task 1 (shared chrome) is audited first since its components (header, side
nav, tables, pagination, modals, notifications) are reused across every other
section — findings there apply everywhere and don't need to be re-logged
per-section.
Summary — all 14 tasks
All tasks are complete except Task 12, which is partial: dc.peers.show has
no peered cluster to open in this dataset. (Task 4 was in the same state —dc.services.instance's .upstreams/.exposedpaths/.addresses/.metadata
tabs needed mesh mock data before they could be audited — but that's now
resolved; see task-4-mock-data-plan.md and
04-service-instance.md.) Everything else was fully
reachable.
Recurring patterns worth fixing once, centrally, rather than per-instance:
aria-label/aria-labelledbyon an element with no compatible role (the
attribute is silently dropped because the element's implicit role isgeneric, which doesn't support naming) — found independently 4 times:<header>wrappers (02-overview.md #1), HDS's ownHds::Alert(07-intentions.md #1, recurring again on
the service-instance Exposed Paths tab's intro alert —
04-service-instance.md), and the ACL
ruleset badge (14-namespaces.md #1, the one instance
with a confirmed real information loss). Worth anember-template-lint
rule if one doesn't already exist for this.- HDS's
{{hds-tooltip}}modifier makes any host element tabbable with no
widget role (01-shared-chrome.md #7) — recurred in
nearly every task (Overview, Services, Service instance, Nodes, Peers).
Upstream in@hashicorp/design-system-components; consul-ui uses it 30+
places. ConsulCopyButton'saria-labelreplaces its visible value instead of
including it (05-nodes.md #1) — WCAG 2.5.3 Label in Name,
11 usage sites app-wide. Single highest-reach fix in the whole audit: one
component change covers all 11.- The side-nav
<aside>has no accessible name despite HDS rendering a
hidden heading that was clearly meant to be it
(01-shared-chrome.md #5) — confirmed on every single
page audited. One-line fix (aria-labelonFrame.Sidebar). - Side-nav/footer text contrast (
#8c909con#fafafa, 3.05:1 vs the
4.5:1 required) — 01-shared-chrome.md #1, confirmed
on every single page audited, both by axe/Lighthouse and IBM's checker.
The two most significant individual (non-recurring) findings:
- The health-checks list loses all ``/
<li>semantics becausedisplay: flexis applied to<li>for its layout, stripping the implicitlist/listitemroles Chrome would otherwise assign — confirmed on bothdc.services.instance.healthchecksanddc.nodes.show.healthchecks
(04-service-instance.md #1). ~20 checks read as
one undifferentiated wall of text with no way to navigate check-by-check. - The service topology diagram conveys health status by color alone (bare
"45%9%45%" with no passing/warning/critical label) and has no structural/
textual equivalent of the visual graph at all
(03-services.md #1–#2). Neither automated engine caught
either of these — both came from reading the topology component's source
directly, a reminder that automated tools have real blind spots around
custom visualizations.
On tooling: two IBM accessibility-checker results were investigated and
turned out to be false positives rather than real bugs — a combobox_haspopup_valid
complaint about Ember Power Select's portal wrapper (the real listbox
underneath is correctly implemented — 08-acls-tokens.md),
and the CSS-driven focus-ring pattern initially looked broken viagetComputedStyle until the ::before-pseudo-element ring was checked
directly (01-shared-chrome.md). Both are recorded in
their task files so they aren't rediscovered and mis-reported as bugs later.
Overview (`test/integration/consul-container/test/envoy_extensions/testdata/wasm_test_files/README.md`)
Building WASM test files
We have seen some issues with building the wasm test files on the build runners for the integration test. Currently,
the theory is that there may be some differences in the clang toolchain on different runners which cause panics in
tinygo if the job is scheduled on particular runners but not others.
In order to get around this, we are just building the wasm test file and checking it into the repo.
To build the wasm test file,
~/consul/test/integration/consul-container/test/envoy_extensions/testdata/wasm_test_files
> docker run -v ./:/wasm --rm $(docker build -q .)Ui Packages Consul Ui Docs Index (`ui/packages/consul-ui/docs/index.mdx`)
title: Introduction
Consul UI Engineering Docs
Welcome to Consul UIs engineering documentation.
Environment Variables
There are various environment variable you can use whilst running make start or make test to configure various features:
| Variable | Default Value | Description |
|---|---|---|
TESTEM_AUTOLAUNCH |
Chrome | Controls which browser to open tests in. A setting of "" means 'let me manually open the browser' |
EMBER_TEST_REPORT |
Output a test report | |
CONSUL_DOCFY_CONFIG |
Define an additional docfy-config.json file to use |
Adding documentation
Our documentation use docfy for rendering our markdown+glimmer-component documentation. In order to live render any code examples use the preview-template meta, for example:
```hbs
<YourComponent
@thing={{var}}
/>
```The above will render the same code snippet in a box above the snippet.
The location and name of markdown files within the project differs slightly depending on what you need to add documentation for:
- docs:
docs/filename.mdx - styles:
app/styles/*/*/README.mdx(currently mostly inapp/styles/base) - components:
app/components/your-component-name/README.mdx - helpers:
app/helpers/your-helper-name.mdx - modifiers:
app/modifiers/your-modifier-name.mdx - services:
app/services/your-service-name.mdx(eventually these will partly use jsdoc code style generation)
Config Checklist Adding Config Fields (`docs/config/checklist-adding-config-fields.md`)
Adding a Consul Config Field
This is a checklist of all the places you need to update when adding a new field
to config. There may be a few other special cases not included but this covers
the majority of configs.
We suggest you copy the raw markdown into a gist or local file and check them
off as you go (you can mark them as done by replace [ ] with [x] so github
renders them as checked). Then please include the completed lists you worked
through in your PR description.
Examples of special cases this doesn't cover are:
- If the config needs special treatment like a different default in
-devmode
or differences between CE and Enterprise. - If custom logic is needed to support backwards compatibility when changing
syntax or semantics of anything
There are four specific cases covered with increasing complexity:
- adding a simple config field only used by client agents
- adding a CLI flag to mirror that config field
- adding a config field that needs to be used in Consul servers
- adding a field to the Service Definition
Adding a Simple Config Field for Client Agents
- Add the field to the Config struct (or an appropriate sub-struct) in
agent/config/config.go. - Add the field to the actual RuntimeConfig struct in
agent/config/runtime.go. - Add an appropriate parser/setter in
agent/config/builder.goto
translate. - Add the new field with a random value to both the JSON and HCL files in
agent/config/testdata/full-config.*, which should cause the test to fail.
Then update the expected value inTestLoad_FullConfiginagent/config/runtime_test.goto make the test pass again. - Run
go test -run TestRuntimeConfig_Sanitize ./agent/config -updateto update
the expected value forTestRuntimeConfig_Sanitize. Look atgit diffto
make sure the value changed as you expect. - If your new config field needed some validation as it's only valid in
some cases or with some values (often true).- Add validation to Validate in
agent/config/builder.go. - Add a test case to the table test
TestLoad_IntegrationWithFlagsinagent/config/runtime_test.go.
- Add validation to Validate in
- If your new config field needs a non-zero-value default.
- Add that to
DefaultSourceinagent/config/defaults.go. - Add a test case to the table test
TestLoad_IntegrationWithFlagsinagent/config/runtime_test.go. - If the config needs to be defaulted for the test server used in unit tests,
also add it toDefaultConfig()inagent/consul/config.go.
- Add that to
- If your config should take effect on a reload/HUP.
- Add necessary code to to trigger a safe (locked or atomic) update to
any state the feature needs changing. This needs to be added to one or
more of the following places:ReloadConfiginagent/agent.goif it needs to affect the local
client state or another client agent component.ReloadConfiginagent/consul/client.goif it needs to affect
state for client agent's RPC client.
- Add a test to
agent/agent_test.gosimilar to others with prefixTestAgent_reloadConfig*.
- Add necessary code to to trigger a safe (locked or atomic) update to
- Add documentation to
website/content/docs/agent/config/config-files.mdx.
Done! You can now use your new field in a client agent by accessings.agent.Config.<FieldName>.
If you need a CLI flag, access to the variable in a Server context, or touched
the Service Definition, make sure you continue on to follow the appropriate
checklists below.
Adding a CLI Flag Corresponding to the new Field
If the config field also needs a CLI flag, then follow these steps.
- Do all of the steps in Adding a Simple Config
Field For Client Agents. - Add the new flag to
agent/config/flags.go. - Add a test case to TestParseFlags in
agent/config/flag_test.go. - Add a test case (or extend one if appropriate) to the table test
TestLoad_IntegrationWithFlagsinagent/config/runtime_test.goto ensure setting the
flag works. - Add flag (as well as config file) documentation to
website/source/docs/agent/config/config-files.mdxandwebsite/source/docs/agent/config/cli-flags.mdx.
Adding a Simple Config Field for Servers
Consul servers have a separate Config struct for reasons. Note that Consul
server agents are actually also client agents, so in some cases config that is
only destined for servers doesn't need to follow this checklist provided it's
only needed during the bootstrapping of the server (which is done in code shared
by both server and client components in agent.go). For example WAN Gossip
configs are only valid on server agents but since WAN Gossip is setup inagent.go they don't need to follow this checklist. The simplest (and mostly
accurate) rule is:
If you need to access the config field from code in
agent/consul(e.g. RPC
endpoints), then you need to follow this. If it's only inagent(e.g. HTTP
endpoints or agent startup) you don't.
A final word of warning - you should never need to pass config into the FSM
(agent/consul/fsm) or state store (agent/consul/state). Doing so is very
dangerous and can violate consistency guarantees and corrupt databases. If
you think you need this then please discuss the design with the Consul team
before writing code!
Consul's server components for historical reasons don't use the RuntimeConfig
struct they have their own struct called Config in agent/consul/config.go.
- Do all of the steps in Adding a Simple Config
Field For Client Agents. - Add the new field to Config struct in
agent/consul/config.go - Add code to set the values from the
RuntimeConfiginnewConsulConfigmethod inagent/agent.go - If needed, add a test to
agent_test.goif there is some non-trivial
behavior in the code you added in the previous step. We tend not to test
simple assignments from one to the other since these are typically caught by
higher-level tests of the actual functionality that matters but some examples
can be found prefixed withTestAgent_consulConfig* - If your config should take effect on a reload/HUP
- Add necessary code to
ReloadConfiginagent/consul/server.gothis
needs to be adequately synchronized with any readers of the state being
updated.
- [ ] Add a new test or a new assertion toTestServer_ReloadConfig
- Add necessary code to
You can now access that field from s.srv.config.<FieldName> inside an RPC
handler.
Adding a New Field to Service Definition
The Service Definition syntax
appears both in Consul config files but also in the /v1/agent/service/register
API.
For wonderful historical reasons, our config files have always used snake_case
attribute names in both JSON and HCL (even before we supported HCL!!) while our
API uses CamelCase.
Because we want documentation examples to work in both config files and API
bodies to avoid needless confusion, we have to accept both snake case and camel
case field names for the service definition.
Finally, adding a field to the service definition implies adding the field to
several internal structs and to all API outputs that display services from the
catalog. That explains the multiple layers needed below.
This list assumes a new field in the base service definition struct. Adding new
fields to health checks is similar but mostly needs HealthCheck structs and
methods updating instead. Adding fields to embedded structs like ProxyConfig
is largely the same pattern but may need different test methods etc. updating.
- Do all of the steps in Adding a Simple Config
Field For Client Agents. -
agent/structspackage- Add the field to
ServiceDefinition(service_definition.go) - Add the field to
NodeService(structs.go) - Add the field to
ServiceNode(structs.go) - Update
ServiceDefinition.ToNodeServiceto translate the field - Update
NodeService.ToServiceNodeto translate the field - Update
ServiceNode.ToNodeServiceto translate the field - Update
TestStructs_ServiceNode_Conversions - Update
ServiceNode.PartialClone - Update
TestStructs_ServiceNode_PartialClone(structs_test.go) - If needed, update
NodeService.Validateto ensure the field value is
reasonable - Add test like
TestStructs_NodeService_Validate*instructs_test.go - Add comparison in
NodeService.IsSame - Update
TestStructs_NodeService_IsSame - Add comparison in
ServiceNode.IsSameService - Update
TestStructs_ServiceNode_IsSameService - If your field name has MultipleWords,
- Add it to the
auxinline struct inServiceDefinition.UnmarshalJSON(service_defintion.go).- Note: if the field is embedded higher up in a nested struct,
follow the chain and update the necessary struct'sUnmarshalJSON
method - you may need to add one if there are no other case
transformations being done, copy and existing example. - Note: the tests that exercise this are in agent endpoint for
historical reasons (this is where the translation used to happen).
- Note: if the field is embedded higher up in a nested struct,
- Add it to the
- Add the field to
-
agentpackage- Update
testAgent_RegisterServiceand/or add a new test to ensure
your fields register correctly via API (agent_endpoint_test.go) - If your field name has MultipleWords,
- Update
testAgent_RegisterService_TranslateKeysto include
examples with it set insnake_caseand ensure it is parsed
correctly. Run this viaTestAgent_RegisterService_TranslateKeys
(agent_endpoint_test.go).
- Update
- Update
-
apipackage- Add the field to
AgentService(agent.go) - Add/update an appropriate test in
agent_test.go- (Note you need to use
make testor ensure theconsulbinary on
your$PATHis a build with your new field - usuallymake dev
ensures this unless you're path is funky or you have a consul binary
even further up the shell's$PATH).
- (Note you need to use
- Add the field to
- Docs
- Update docs in
website/source/docs/agent/services.html.md - Consider if it's worth adding examples to feature docs or API docs
that show the new field's usage.
- Update docs in
Note that although the new field will show up in the API output of/agent/services , /catalog/services and /health/services, those tests
right now don't exercise anything that's super useful unless custom logic is
required since they don't even encode the response object as JSON and just
assert on the structs you already modified. If custom presentation logic is
needed, tests for these endpoints might be warranted too. It's usual to useomit-empty for new fields that will typically not be used by existing
registrations although we don't currently test for that systematically.
Contributing Add A Changelog Entry (`docs/contributing/add-a-changelog-entry.md`)
Adding a Changelog Entry
Any change that a Consul user might need to know about should have a changelog entry.
What doesn't need a changelog entry?
- Docs changes
- Typos fixes, unless they are in a public-facing API
- Code changes we are certain no Consul users will need to know about
To include a changelog entry in a PR, commit a text file
named .changelog/.txt, where `` is the number associated with the open
PR in Github. The text file should describe the changes in the following format:
```release-note:<change type>
<code area>:
```Valid values for <change type> include:
feature: for the addition of a new featureimprovement: for an improvement (not a bug fix) to an existing featurebug: for a bug fixsecurity: for any Common Vulnerabilities and Exposures (CVE) resolutionsbreaking-change: for any change that is not fully backwards-compatibledeprecation: for functionality which is now marked for removal in a future release
<code area> is meant to categorize the functionality affected by the change.
Some common values are:
checks: related to node or service health checkscli: related to the command-line interface and its commandsconfig: related to configuration changes (e.g., adding a new config option)connect: catch-all for the Connect subsystem that provides service mesh functionality
if no more specific<code area>applieshttp: related to the HTTP API interface and its endpointsdns: related to DNS functionalityui: any change related to the built-in Consul UI (ui/folder)
Look in the .changelog/ folder for examples of existing changelog entries.
If a PR deserves multiple changelog entries, just add multiple entries separated by a newline
in the format described above to the .changelog/.txt file.
Contributing Fork The Project (`docs/contributing/fork-the-project.md`)
Forking the Consul Repo
Community members wishing to contribute code to Consul must fork the Consul project
(your-github-username/consul). Branches pushed to that fork can then be submitted
as pull requests to the upstream project (hashicorp/consul).
To locally clone the repo so that you can pull the latest from the upstream project
(hashicorp/consul) and push changes to your own fork (your-github-username/consul):
- Create the forked repository (
your-github-username/consul) - Clone the
hashicorp/consulrepository andcdinto the folder - Make
hashicorp/consultheupstreamremote rather thanorigin:git remote rename origin upstream. - Add your fork as the
originremote. For example:git remote add origin https://github.com/myusername/consul - Checkout a feature branch:
git checkout -t -b new-feature - Make changes
- Push changes to the fork when ready to submit a PR:
git push -u origin new-feature
Ui Packages Consul Ui Docs Bookmarklets (`ui/packages/consul-ui/docs/bookmarklets.mdx`)
Debug/Engineering Utilities
The Consul UI has a set of debug/engineering utility functions that only exist in non-production environments (they are noop'ed from production builds using Ember's runInDebug). You can call these either straight from Web Inspectors console, or by using javascript: URLs i.e. javascript:Routes().
Below is a list of the most commonly used functions as bookmarklets followed by more detailed documentation on these utilities. The bookmarklets can be added to your browser by dragging the bookmarklet link into your browsers Bookmarks Bar. Please add any new commonly used environment variables as bookmarklets here for other engineers to use.
| Link/Bookmarklet | Description |
|---|---|
| Print Routing DSL | Print out Ember's Route DSL for the application |
| Save Current Scenario | Opens a tab with links to allow you to create a bookmarklet or share a URL of your current scenario (your Consul UI related development/debug cookies) |
| Toggle Color Scheme | Toggle Color Scheme from light to dark or vice versa |
| Enable ACLs | Enable ACLs |
| Enable Agentless | Enable synthetic nodes (agentless) in the mock data |
| Enable TProxy | Enable TProxy |
| Enable Nspaces | Enable Namespace Support |
| Enable Peers | Enable Peering Support |
| Enable Partitions | Enable Admin Partition Support |
| Enable SSO | Enable SSO Support |
| Enable Metrics | Enable all configuration required for viewing the full Metrics Visualization |
| Enable Intl Debug | Render intl interpolation variable names instead of interpolating them |
| Intl Lorem Ipsum | Render all intl copy as lorem ipsum |
| Intl Dashes | Render all intl copy as - |
| Toggle QUnit Display | Toggle the QUnit display on or off during browser testing |
| Function | Arguments | Description |
|---|---|---|
Routes(url) |
url: The url to pass the DSL to, if left undefined just use a blank tab |
Provides a way to easily print out Embers Route DSL for the application or to pass it straight to any third party utility such as ember-diagonal |
Scenario(str) |
str: 'Cookie formatted' string, if left undefined open a new tab with a link/bookmarklet to the current Scenario |
Provides a way to easily save and reload scenarios of configurations via URLs or bookmarklets |
You can also configure these manually or example using Web Inspector to set a cookie as follows:
CONSUL_ACLS_ENABLE=1This will enable the ACLs login page, to which you can login with any ACL
token/secret.
| Variable | Default Value | Description |
|---|---|---|
CONSUL_ACLS_ENABLE |
false | Enable/disable ACLs support. |
CONSUL_ACLS_LEGACY |
false | Enable/disable legacy ACLs support. |
CONSUL_AGENTLESS_ENABLE |
false | Enable/disable synthetic nodes (agentless) in the mock data. |
CONSUL_TPROXY_ENABLE |
false | Enable/disable TProxy support globally (if not a service may have this applied randomly). |
CONSUL_NSPACES_ENABLE |
false | Enable/disable Namespace support. |
CONSUL_SSO_ENABLE |
false | Enable/disable SSO support. |
CONSUL_OIDC_PROVIDER_URL |
undefined | Provide a OIDC provider URL for SSO. |
CONSUL_LATENCY |
0 | Add a latency to network requests (milliseconds) |
CONSUL_METRICS_POLL_INTERVAL |
10000 | Change the interval between requests to the metrics provider (milliseconds) |
CONSUL_METRICS_PROXY_ENABLE |
false | Enable/disable the metrics proxy |
CONSUL_METRICS_PROVIDER |
Set the metrics provider to use for the Topology Tab | |
CONSUL_SERVICE_DASHBOARD_URL |
Set the template URL to use for Service Dashboard links | |
CONSUL_UI_CONFIG |
Set the entire ui_config for the UI as JSON if you don't want to use the above singular variables |
|
CONSUL_SERVICE_COUNT |
(random) | Configure the number of services that the API returns. |
CONSUL_NODE_COUNT |
(random) | Configure the number of nodes that the API returns. |
CONSUL_KV_COUNT |
(random) | Configure the number of KV entires that the API returns. |
CONSUL_INTENTION_COUNT |
(random) | Configure the number of intentions that the API returns. |
CONSUL_POLICY_COUNT |
(random) | Configure the number of policies that the API returns. |
CONSUL_ROLE_COUNT |
(random) | Configure the number of roles that the API returns. |
CONSUL_NSPACE_COUNT |
(random) | Configure the number of namespaces that the API returns. |
CONSUL_UPSTREAM_COUNT |
(random) | Configure the number of upstreams that the API returns. |
CONSUL_EXPOSED_COUNT |
(random) | Configure the number of exposed paths that the API returns. |
CONSUL_CHECK_COUNT |
(random) | Configure the number of health checks that the API returns. |
CONSUL_OIDC_PROVIDER_COUNT |
(random) | Configure the number of OIDC providers that the API returns. |
CONSUL_RESOURCE_<singular-resource-name>_<access-type> |
true | Configure permissions e.g CONSUL_RESOURCE_INTENTION_WRITE=false. |
DEBUG_ROUTES_ENDPOINT |
undefined | When using the window.Routes() debug utility (see utility functions), use a URL to pass the route DSL to. %s in the URL will be replaced with the route DSL - http://url.com?routes=%s |
See ./mock-api for more details.
Ui Packages Consul Ui Docs Data Layer (`ui/packages/consul-ui/docs/data-layer.mdx`)
title: The Data/Model Layer
The Data/Model Layer
Consul UI is currently backed by an Ember conventional ember-data model/adapter/serializer set of classes. But the primary unit for the model layer of the application are our Repository classes which 'front' ember-data and provide a layer of abstraction between the application code and ember-data.
ember-data Model classes
These model classes are primarily used to describe the schema of our data objects. We mainly use the ember-data @attr decorator to do this, but we also have @nullValue and @replace decorators which allow you to define what to do when the HTTP API responds with a null value for a property (using @nullValue) something which is very common in with the Consul HTTP API. @nullValue is a shortcut for @replace which can be used to declaratively specify any sort of replacement. For example we use this for our HealthCheck model to replace a Type property with an empty string ('') to the value serf (which is what the empty string actually means).
When you come to need extra checks for abilities or characteristics of models, prefer using our test (or is/can) helpers along with ember-cans Ability classes instead of adding more computed properties to the model classes. Model classes should be primarily used for declaratively specifying the schema for the model. Here 'prefer' means don't worry if you can't, it's fine to keep using model classes if you can't use the Ability classes for some reason.
We don't use ember-data to model relationships spread across more than one API request. Please use nested DataLoader/DataSource components which offer a more flexible/featured way to model relationships between different API requests.
Whilst we don't use ember-data async relationships, we do use ember-data-model-fragments minimally for when you need to add computed/tracked properties to nested objects of a model. We use them minimally because the compatibility matrix for this addon with ember-data can often lead to this addon blocking ember upgrades. Despite this, if you need to use it, then use it, we have it installed as a dependency currently.
ember-data Adapter classes
We use a completely custom base adapter class (HTTPAdapter) which is based on (but doesn't inherit from) the ember-data RESTAdapter. This is not as strict at sticking to REST conventions as the original RESTAdapter, which suits Consul's HTTP API a lot better.
The adapter aims to be very simple and gives full control of the HTTP request to the engineer all in one place. For example:
async requestForQuery(request, { dc, ns, partition, index, id, uri }) {
return request`
GET /v1/internal/ui/nodes?${{ dc }}
X-Request-ID: ${uri}
${{
ns,
partition,
index,
}}
`;
}Here the request Tagged Template Literal here ensures that it is very difficult not to URL encode user provided values. Depending on where you interpolate a user provided value the value will be encoded accordingly, whether thats URL parameters, query parameters or header values. The format of the template follows the HTTP protocol as much as possible.
- The HTTP method followed by a single space, followed by the URL for the request, followed by a question mark, an object interpolated directly after the question mark will be encoded as query parameters (using ='s &'s etc).
- After a single return, you can optinally provide HTTP header values, separated by a single return.
- A double return then lets you interpolate HTTP body values for POSTing/PUTing, if you've defined the HTTP method to be a GET request, then these values will be added to the query parameters.
As request performs all of the encoding for you when the user provided values are interpolated into the template, the only way you can add a URL encoding error is if you actually type one yourself into the code. This avoids all manner of bugs.
request is usually just returned by the method, but you can further normalize the response here also (which gives you direct access to the methods parameters without having to pass them all through somehow to the serializer. For example:
async requestForQuery(request, { ns, dc, index }) {
const respond = await request`
GET /v1/partitions?${{ dc }}
${{ index }}
`;
// deleting the x-consul-index header disables blocking queries for this request
await respond((headers, body) => delete headers['x-consul-index']);
return respond;
}respond here acts as kind of a distributed filter chain, in that you can call respond multiple times across different classes and it will keep filtering/mutating the values as you go. As the request and the response of a HTTP API endpint is usually very tightly coupled, it probably makes sense for us to do more and more of this here in the adapter in the future, and eventually to move all of this directly into our repositories.
ember-data Serializer classes
Currently the majority of our serializing code (of which there is very little), is done in conventional ember-data serializers. Our Serializers also use a HTTPSerializer class as a base, which does inherit form the standard ember-data serializer. Every model object gets fingerprinted with a ['partition', 'nspace', 'dc', 'slug', ...] formatted unique ID. Therefore most models have Partition, Namespace, and Datacenter properties. The functions used for serializing are unfortunately a little verbose, but this gives use the distributed filter chain characteristic and was originally planned to also support streaming JSON responses.
Consul UIs Repository classes
Most importantly Consul UI uses an extra Repository class per data model. This gives us a thin layer of abstraction over ember-data which means the rest of our application doesn't need to know it is using ember-data at all. We have a base/abstract Repository class which all of the others inherit from for now, and mostly just pass things through to more conventional ember-data access using the ember-data store. Importantly, it also gives us a place to smooth over any wrinkles that we might have with ember-data (and/or ember-changeset) without doing that outside of the data/model layer.
All access methods on the Repository classes follow a fairly standard naming scheme findAll/findBySlug, but since we use our @dataSource decorator/annotation to access these methods, functionally the naming isn't important, so naming is mainly for documentation/consistency purposes.
You'll find most Repository methods look like this:
@dataSource('/:partition/:ns/:dc/auth-method/:id')
async findBySlug() {
return super.findBySlug(...arguments);
}Which means that partition, ns and id parameters will eventually find their way into the Adapter class ready for you to make the request.
The above method can be called from within the app, along with Blocking Query and/or more traditional polling support, using our DataSource and/or DataLoader component:
{{! with loading, loaded, error and disconnection states }}
<DataLoader @src={{uri '/${partition}/${nspace}/${dc}/auth-method/${id}'
(hash
partition=route.params.partition
nspace=route.params.nspace
dc=route.params.dc
id=(or route.params.id '')
)
}}
as |loader|>
<:loaded>
Show the Auth Methods here {{loader.data}}
</:loaded>
</DataLoader>
{{! or just }}
<DataSource @src={{uri '/${partition}/${nspace}/${dc}/auth-method/${id}'
(hash
partition=route.params.partition
nspace=route.params.nspace
dc=route.params.dc
id=(or route.params.id '')
)
}}
as |source|>
Show the Auth Methods here {{source.data}}
</DataSource>See the separate Component documentation for both the DataLoader and the DataSource component for more details.
Ui Packages Consul Ui Docs Hds (`ui/packages/consul-ui/docs/hds.mdx`)
title: Hashicorp Design System
Hashicorp Design System
The application integrates setup that make it possible to use the Hashicorp Design System (HDS) in the application. The application also
bundles TailwindCSS to follow a utility-based CSS approach that mixes well with HDS utility classes. Tailwind is expected to be used
as a supplement to HDS's utility classes, and should mainly be used for layouting.
Design Tokens
Colors
To make sure we follow HDS-guidelines, we disabled Tailwind's color utilities completely. Instead of relying on Tailwind for colors, you should be working with the colors from HashiCorp-Design-System and follow the guidance in the docs on how to use them:
<h2 class='hds-foreground-strong'>
HDS is awesome
</h2>Regular TailwindCSS colors aren't available.
<h2 class='text-red-400'>
TailwindCSS colors won't work
</h2>HDS core styles
HDS core styles are available via the provided hds-utility-classes made available via @hashicorp/design-system-tokens.
You for example would work with HDS' typography styles in this way:
<p class='hds-typography-display-400'>
A paragraph styled via HDS.
</p>Because HDS provides utility classes for core styles, you should not be using Tailwind classes to customize core styles like typography.
Combining HDS and Tailwind
Tailwind is used as a supplement to what HDS provides. You will be using Tailwind for layouting and other utility functionality that isn't provided by HDS.
<button type='button' class='underline transform scale-100 transition ease-in-out hover:scale-125'>
Hover me
</button>Components
All components from Hashicorp Design System are available for you to use. Here's an example that shows how to implement a navigation bar with HDS and Tailwind in combination.
<nav
class='h-16 w-full consul-surface-nav flex items-center justify-between px-4 hds-font-weight-medium'
>
<ul class='flex items-center'>
<li>
{{! should probably be a context-switcher }}
<FlightIcon
@name='menu'
@color='var(--token-color-foreground-high-contrast)'
class='cursor-pointer'
/>
</li>
<li>
<FlightIcon @name='consul' class='ml-4 h-8 w-8' @color='var(--token-color-consul-brand)' />
</li>
</ul>
<ul class='flex items-center space-x-2'>
<li>
<Hds::Dropdown as |dd|>
<dd.ToggleButton @text='Help' />
<dd.Title @text='Consul HDS' />
<dd.Interactive @text='Documentation' @icon='docs-link' />
<dd.Interactive @text='HashiCorp Learn' @icon='learn-link' />
<dd.Separator />
<dd.Interactive @text='Provide Feedback' @icon='github' />
</Hds::Dropdown>
</li>
<li>
<Hds::Link::Standalone
@href='https://hashicorp.com'
@icon='settings'
@iconPosition='trailing'
@size='medium'
@text='Settings'
/>
</li>
</ul>
</nav>Please notice the custom consul-surface-nav-utility class. This class has been created because at the moment of writing,
there wasn't a hds-surface- that matched what we needed for this navigation header. If you run into situations like
that, you can add a consul--utility class to the utilities-layer in tailwind.scss.
// consul-ui/app/styles/tailwind.scss
@layer utilities {
.consul-surface-nav {
background: var(--token-color-palette-neutral-700);
}
// ...
}This should only be used as a last resort and you should stick to hds--utilities whenever possible.
Ui Packages Consul Ui Docs I18n (`ui/packages/consul-ui/docs/i18n.mdx`)
i18n
At the time of writing we are currently in the process of
localizing/internationalizing the entire codebase. To help us to do this we
use the ember-intl ember addon
Please see ember-intls documentation
for information on how to use this.
Additionally we have several dev-time only cookie/environment variables to
help engineers see what still requires localizing, to test copy/text using
localizations other than en-us and to view any required interpolation
variables with rendered text, instead of doing the actual interpolation
itself.
| Link/Bookmarklet | Description |
|---|---|
| Enable Intl Debug | Render intl interpolation variable names instead of interpolating them |
| Intl Lorem Ipsum | Render all intl copy as lorem ipsum |
| Intl Dashes | Render all intl copy as - |
en-us
If in doubt, or unless specifically directed otherwise, all text should use
sentence case, apart from 'Consul Nouns' which should use capital casing.
'Consul Nouns' here would be things like 'Services', 'Ingress Gateways', 'Nodes'
and other things that are specific 'objects' in Consul. We also use the form
'ACLs' for acronyms instead of 'Acls'.
text: Viewing metrics for Ingress Gateways is not currently supported
text: ACLs are disabledinstead of:
text: Viewing Metrics for ingress gateways is not currently supported
text: Acls are disabledUi Packages Consul Ui Docs Node Metadata Hds Migration (`ui/packages/consul-ui/docs/node-metadata-hds-migration.md`)
Node "Metadata" tab — HDS migration
Migrate the node detail Metadata tab (/ui/{dc}/nodes/{name}/metadata) from
the legacy TabularCollection table to the HDS Card layout shown in the
Figma design (HDS-Migration, node 3324-78030).
Because the same Consul::Metadata::List component also renders the
service-instance metadata tab (/ui/{dc}/services/{name}/instances/{id}/metadata),
this single change migrates both tabs consistently.
Goal / before → after
Before: Consul::Metadata::List rendered a TabularCollection with Key /Value / columns and row striping — a full data-grid treatment.
After: a single Hds::Card::Container (white surface, 24px padding,6px radius, subtle border + low elevation) containing a two-column
Key / Value layout. No row dividers, no sortable headers, no toolbar, no
search, no copy button — a read-only description card, per Figma.
Layout (from Figma node `3324-78089`)
| Property | Value |
|---|---|
| Card chrome | Hds::Card::Container @level="mid" @hasBorder |
| Card padding | 24px |
| Columns | Key (fixed 300px) + Value (fills remaining) |
| Column gap | 32px |
| Row gap | 16px |
| Header weight | semibold (Key / Value) |
| Body weight | regular |
| Typography | body-300 (16px / 24px), foreground-strong |
| Wrapping | long keys/values wrap (word-break: break-word) |
The consul logo shown before v1.17.2 in the mock is sample data, not a
generic feature — metadata values are arbitrary strings and are rendered as-is.
Architecture — CSS grid, not two stacks
The card uses a CSS grid (grid-template-columns: 300px 1fr) rather than two
independent flex columns so that Key and Value rows stay aligned even when a key
wraps onto multiple lines (e.g. the long consul-dashboard-url… sample). The
header cells (Key / Value) and each dt / dd pair are direct grid items.
File-by-file changes
1. `app/components/consul/metadata/list/index.hbs`
Replace TabularCollection with Hds::Card::Container wrapping a <dl> grid:
data-test-metadatamoves onto the card container (preserved).- Each entry renders
<dt data-test-metadata-key>+<dd data-test-metadata-value>.
2. `app/components/consul/metadata/list/index.scss` (new)
Grid + typography tokens (--token-typography-body-300-*,--token-color-foreground-strong, --token-typography-font-weight-*). Mirrors
the tokens used by the RTT card migration.
3. `app/styles/components.scss`
Register the new stylesheet:@import 'consul-ui/components/consul/metadata/list';
4. `app/components/consul/metadata/list/index.js`
Unchanged — classic tagName: '' component; public @items API is unchanged
([key, value] pairs from entries).
5. Tab templates — unchanged
app/templates/dc/nodes/show/metadata.hbs andapp/templates/dc/services/instance/metadata.hbs keep their Route wrapper andEmptyState blocks; only the shared component's internals changed.
Tests / page objects
The old markup emitted [data-test-tabular-row] per row (fromTabularCollection); the new grid emits one [data-test-metadata-key] per row.
Both metadata collections were updated to count that selector instead:
tests/pages/dc/nodes/show.js—.consul-metadata-list [data-test-metadata-key]tests/pages/dc/services/instance.js—.metadata [data-test-metadata-key]
The show.feature metadata-count scenarios (e.g. "I see 3 of the metadata
object") continue to pass against the new selector.
Testing
ember test --filter metadata- Visual QA at
/ui/dc1/nodes/{node}/metadataand/ui/dc1/services/{name}/instances/{id}/metadata: card renders, Key/Value rows
align, long keys wrap, and the empty state ("This node/instance has no
metadata.") is unchanged whenMetais absent.
Out of scope
- Toolbar / search / sort / pagination — not present in the Figma for this tab.
- Copy buttons on values — not in the Figma.
- Route query params / controller — none needed (no search/sort/filter state).
Ui Packages Consul Ui Docs Node Rtt Hds Migration (`ui/packages/consul-ui/docs/node-rtt-hds-migration.md`)
Node Detail — Round Trip Time Tab — HDS Migration
Branch: suresh/nodes-hds-migration
Page: /ui/dc1/nodes/node-0/round-trip-time (route dc.nodes.show.rtt)
Figma: HDS-Migration — node 3324:70738
Type: HDS Migration
Scope: The Round Trip Time tab body on a single node's detail page only — the min/median/max
stats block and the radial tomography graph. Follow-on tonodes-hds-migration.md, which deferred "Node detail page & tabs" to
separate tickets. Sibling of node-healthchecks-hds-migration.md
and node-lock-sessions-hds-migration.md.
Overview
The Figma target redesigns the Round Trip Time tab so its whole body sits inside a single HDS
card, with the min/median/max stats and the radial graph laid out side by side (separated by a
vertical divider) instead of stacked. Four things change, all presentational:
- The tab body is wrapped in an
Hds::Card::Container(white surface, 1px border, low
elevation shadow,6pxradius,24pxpadding) — currently the content sits bare in a.tab-sectionwith no card chrome. - The stats and the graph move from stacked (stats above graph) to a horizontal two-column
layout with a vertical separator between them (Figma:flex,gap: 64px,items: start). - The legacy
.definition-table<dl>/<dt>/<dd>stats block is replaced with a simple two-column
label/value layout using HDS body typography, and the labels gain a "time" suffix —
"Minimum time" / "Median time" / "Maximum time" (currently "Minimum" / "Median" /
"Maximum"). - The
Consul::Tomography::GraphSVG itself is unchanged — its colors are already tokenized.
Only its container/placement changes.
No backend, data-model, or route-redirect changes are required — the rtt.js redirect (bounce todc.nodes.show when tomography.distances is empty) stays as-is.
Files in Scope
| File | Change type | Notes |
|---|---|---|
app/templates/dc/nodes/show/rtt.hbs |
HBS | Wrap body in Hds::Card::Container; two-column layout; add vertical separator; rename stat labels; drop .definition-table <dl> |
app/styles/routes/dc/nodes/index.scss |
CSS | New scoped rules for the RTT card's two-column layout + stats typography (scoped to dc.nodes.show.rtt) |
app/components/consul/tomography/graph/index.scss |
CSS | .background disc fill surface-strong → transparent to match the Figma white field (see §4). Only usage of this component is the RTT tab. |
app/routes/dc/nodes/show/rtt.js |
No change | Empty-distances redirect unchanged |
translations/** |
No change | Labels kept as inline literals, consistent with the pre-migration template (see §3) |
Comparison: Current vs Target
1. Card chrome — bare `.tab-section` → `Hds::Card::Container`
| Current | Target | |
|---|---|---|
| Visual | Stats <dl> and graph sit directly in .tab-section with no border/background. |
Whole body in a white card: --token-color-surface-primary bg, 1px border, Elevation/Surface/Low shadow, border-radius: 6px, 24px padding, overflow clipped. |
| Figma | — | Card node 3324:70797: bg surface-primary, p-24, rounded 6px, shadow 0 0 0 1px rgba(101,106,118,.15), 0 1px 1px rgba(...,.05), 0 2px 2px rgba(...,.05), overflow-clip. |
| Implementation | — | Reuse the established card primitive: <Hds::Card::Container @level="mid" @hasBorder={{true}} @overflow="hidden"> — same invocation as consul/server/card and consul/peer/bento-box. @level="mid" maps to the Elevation/Surface/Low shadow in the Figma. |
| Status | ⚠️ Change required. | — |
Note: unlike the health-checks / service-instances toolbar cards (which used
--token-color-surface-faint+--token-color-border-faint), this content card uses the standardHds::Card::Containerwhite surface + elevation shadow, matching the Figma exactly.
2. Layout — stacked → side-by-side with vertical separator
| Current | Target | |
|---|---|---|
| Visual | Stats block on top, graph below (vertical stack in .tab-section). |
Stats column on the left, graph on the right, side by side, with a vertical divider between them. Figma: flex, gap: 64px, align-items: start; separator is w-px h-[329px]. |
| Implementation | — | Card body is a horizontal flex container (gap: 64px, align-items: flex-start). Left: the stats column. Middle: a 1px bordered `` that align-self: stretches to the graph height. Right: <Consul::Tomography::Graph>. |
| Status | ✅ Done. Hds::Separator was rejected — it renders a horizontal <hr> with only a @spacing arg (no orientation), so the bordered-`` divider is cleaner. |
3. Stats block — `.definition-table` `` → two-column label/value + "time" labels
| Current | Target | |
|---|---|---|
| Visual | <dl><dt>Minimum</dt><dd>0.12ms</dd>… styled by legacy %definition-table (dt = display-100-semibold, small). |
Three rows, each [label] [value]. Label = Body/300/Semibold (16px / 600 / lineHeight 24, color #0c0c0e → --token-color-foreground-strong), fixed 120px width. Value = Body/300/Regular (16px / 400). Row gap 20px, label→value gap 16px. Labels read "Minimum time" / "Median time" / "Maximum time". |
| Current code | rtt.hbs lines 14–35 — <dl>…</dl>. |
Replace with a small flex-column block of three [label][value] rows. Keep the {{format-number … maximumFractionDigits=2}}ms value formatting exactly. |
| Status | ⚠️ Change required — markup + typography + label copy. | — |
Copy decision (resolved): labels are kept as inline literals ("Minimum time" etc.),
consistent with the pre-migration template. No new i18n keys were added.
4. Tomography graph — one background fix
| Current | Target | |
|---|---|---|
| Visual | Radial SVG with a grey filled disc (--token-color-surface-strong), neutral-300 dashed axis rings + solid border, consul-foreground (pink) spokes + center point, neutral-300/foreground-strong tick labels. |
Radial graph on the card's white field — only dashed ring outlines, no grey disc; pink spokes; ms tick labels to the right. |
| Code | consul/tomography/graph/index.scss .background { fill: var(--token-color-surface-strong); }. |
.background { fill: transparent; } — the only change to the component. SVG geometry, sampling, spokes, ticks, and the 336px size are unchanged. This component is used only on the RTT tab, so the change is contained. |
| Status | ✅ Done — single-line fill change (small deviation from the original "no change" scope, to match the Figma white field). |
Ring geometry differs slightly (current draws dashed rings at 25/50/75/100%; the Figma shows
33/66/100%). This was left as-is — changing it would also move the tick-label math, a deeper
change than this presentational pass warrants.
Task Checklist
- Wrap the
rtt.hbsbody in<Hds::Card::Container @level="mid" @hasBorder={{true}} @overflow="hidden"> - Make the card body a horizontal flex (
gap: 64px,align-items: start) — stats left, graph right - Replace the
.definition-table<dl>with a flex-column of three[label 120px semibold][value regular]rows, gap20px - Rename labels to "Minimum time" / "Median time" / "Maximum time" (kept as inline literals — see §3)
- Preserve
{{format-number … maximumFractionDigits=2}}msvalue formatting - Add a vertical divider between stats and graph (
1pxbordered ``;Hds::Separatoris horizontal-only) - Fix the graph
.backgrounddisc fill totransparentto match the Figma white field (§4) - Add scoped SCSS in
styles/routes/dc/nodes/index.scss(html[data-route^='dc.nodes.show.rtt']) for layout + stats typography; global%definition-tableuntouched - Keep the
{{#if (gt tomography.distances.length 0)}}guard and thertt.jsempty-distances redirect - Run
ember test --filter "nodes/show"— confirm no regressions - Visual QA at
http://localhost:4200/ui/dc1/nodes/node-1/round-trip-timeagainst the Figma
Acceptance Criteria
- The RTT tab body renders inside a single white
Hds::Card::Containerwith a 1px border, low
elevation shadow,6pxradius, and24pxinterior padding. - Stats and graph are laid out side by side, stats on the left, graph on the right, with a vertical
separator between them. - The stats show three rows — Minimum time / Median time / Maximum time — each with a
120px
semibold label and a regular value, using Body/300 (16px) typography. - Values keep the current formatting (
format-numberto 2 fraction digits +ms). - The radial tomography graph is visually unchanged from today.
- When a node has no tomography distances, the tab still redirects to
dc.nodes.show(unchanged). - No visual regressions on other node tabs or on any other page that uses
%definition-table
(the new styles must be scoped todc.nodes.show.rtt).
Testing Notes
- Run
ember test --filter "nodes/show"to catch template/selector regressions on the node detail
route. - Visually verify at
http://localhost:4200/ui/dc1/nodes/node-1/round-trip-timeagainst the Figma
frame (3324:70738), checking card chrome, the two-column layout, the vertical separator, and the
"… time" labels. - Confirm
%definition-tablestyling elsewhere (e.g. intentions edit) is unaffected — the new rules
must be route-scoped, not global. - Confirm a node with no RTT data still bounces back to the node overview tab.
Out of Scope (this ticket)
Consul::Tomography::Graphinternals (SVG geometry, sampling, colors) — reused unchanged.- Node header (name / status badge / "Imported from billing" badge / address) and breadcrumbs —
handled elsewhere. - Tab navigation bar — already migrated.
- Backend / API / model /
rtt.jsredirect logic — no changes.
Ui Packages Consul Ui Docs Service Tags Hds Migration (`ui/packages/consul-ui/docs/service-tags-hds-migration.md`)
Tags (`TagList`) — HDS migration
Migrate the shared TagList component from the legacy <dl> / CSV-list layout
to the HDS Card + Tag pills design shown in the Figma
(HDS-Migration, node 3376-125619).
TagList is shared, so this single change updates both consumers:
- Service "Tags" tab —
/ui/{dc}/services/{name}/tags
(app/templates/dc/services/show/tags.hbs). - Service-instance "Tags" section — the metadata tab
(app/templates/dc/services/instance/metadata.hbs, under its<h2>Tags</h2>).
Goal / before → after
Before: a <dl class="tag-list"> with a <dt> "Tags" label (tag-icon mask +
tooltip) and a <dd> rendering each tag as a bare `` in a comma-separated
(%csv-list) run.
After: an Hds::Card::Container (white surface, 24px padding, 6px
radius, subtle border + low elevation) containing a flex-wrap row ofHds::Tag pills. No inline "Tags" label — the tab/section heading already names
it, matching the Figma.
Layout (from Figma node `3376-125670`)
| Property | Value |
|---|---|
| Card chrome | Hds::Card::Container @level="mid" @hasBorder |
| Card padding | 24px |
| Tags row | display: flex; flex-wrap: wrap; — gap: 12px 8px |
| Pill | Hds::Tag @text={{tag}} (default primary color) |
The Figma maps each pill to the Helios Tag component, and Hds::Tag is
already used for the tag pills in the Service / Service-instance tables — so the
pill styling (border, 20px radius, 13px medium text, foreground-primary)
comes from HDS directly with no bespoke CSS.
File-by-file changes
1. `app/components/tag-list/index.hbs`
Non-block branch replaced: <dl> → Hds::Card::Container wrapping
`` with one <Hds::Tag @text /> per
tag. data-test-tags moves onto the tags container (the direct parent of the
pills). The recursive has-block yield branch is unchanged.
2. `app/components/tag-list/index.scss`
Rewritten: dropped the %tag-list / %horizontal-kv-list / %csv-list@extends and the unused td.tags selector. Now just card padding + the
flex-wrap tags row with the Figma gaps.
3. Tab templates — unchanged
services/show/tags.hbs and services/instance/metadata.hbs keep their Route
wrappers, empty states, and (for the instance) the <h2>Tags</h2> heading.
Tests / page objects
Hds::Tag renders each pill as …, so it remains
the first-child span under [data-test-tags] in order — the existing feature
assertions ([data-test-tags] span:nth-child(n) → "Tag1" / "Tag2" / "Tag3") inservices/show.feature and services/instances/show.feature continue to pass
via the find()-based see the text … in … step.
The one page-object collection that targeted the old <dd> > span markup was
updated:
tests/pages/dc/services/show.js—.tag-list dd > span→.tag-list .hds-tag.
Testing
ember test --filter "Show Service"— 15 pass (includes the tags-display
scenario and the instance tags-section assertions).- Visual QA at
/ui/dc1/services/{name}/tagsand/ui/dc1/services/{name}/instances/{id}/metadata: tags render as pills inside
the card, wrapping across rows; empty states unchanged.
Out of scope
- No routing / data changes —
TagList's@item/@tagsAPI is unchanged. - The recursive block-form (
has-block) usage is preserved as-is.
Ui Packages Consul Ui Docs Significant Patterns (`ui/packages/consul-ui/docs/significant-patterns.mdx`)
Significant patterns
Ember specific
The Consul UI tries to follow typical conventional ember but differs
significantly in several places:
Routing
We use a declarative configuration format for our routing rather than Ember's
DSL. The format is closely modeled on Ember's DSL and if you need to generate
Ember's DSL from this for some reason you can use one of our Debug Utilities
to do this.
All Route based configuration including paths, queryParams, simple redirects
and non-default templateNames are configured in this configuration format.
Please note redirects and template names use relative slash separated paths to
avoid copypasta and for future potential reuse purposes.
Routes
We use a specific BaseRoute as a parent Route for all our Routes. This contains project wide
functionality for several important additions. If you use the normal ember generate route route-name generator this inheritance is automatically added
for you. If you don't use the generator please ensure you use:
import Route from 'consul-ui/routing/route';
export default class NameOfRoute extends Route {}Query parameters should be added to Routes, not Controllers. If a controller
has no query parameters configured they are copied over from the Route.
Preferably we don't use Controllers, but this doesn't mean you shouldn't if
you need to.
Routlets
We use have a concept of 'routlets', the combination of a <Route /> and<Outlet /> components that we use within our route templates to achieve
several different pieces of functionality.
Every route template should be wrapped in a <Route @name={{routeName}}> component and ever{{outlet}} should be wrapped in <Outlet @name={{routeName}}> component.
The routeName variable is made available in every single route template and
it equal to the routeName of the current template e.g. dc.services.index
DataSources
In order to support our live updating long-polling blocking queries we use
'DataSources' which come in two flavours. A service backed
approach for use within javascript:
class RouteName extends Route {
@service('datasource/service') data;
async model(params) {
return this.data.source(uri => uri`/${params.nspace}/${params.dc}/services`)
}
}This method returns an Ember proxy that lets you access the data as if it was
'just the data', but is also reactive/auto updates when the data in the
backend updates, for example:
{{@model.Name}}And a component based approach for use within templates.
<DataSource @src={{
uri '/${nspace}/${dc}/services'
(hash
nspace="default"
dc="dc"
)
}}
@onchange=""
/>See the relavant component/service documentation for more detail.
Components
You could group our components into two different groups:
- UI Components - generic components not necessarily specific to the product.
- Consul Components - Components that are specific to Consul/the product.
These are mostly 'glorified partials'.
Mostly the CSS for a component lives in the component folder itself, but if it
makes sense for it not to live here, thats fine.
We currently use a mix of named/block slots and contextual components and its
fair to say that we use more named/block slots, but both are fine depending on
your use case.
Significant Addons
ember-data- model layerember-stargate- wormhole/portal/put this DOM over there functionalityember-can- user abilities and permissionsember-composable-helpers,ember-string-fns,ember-truth-helpers- helpers x lotsember-changesetandember-changeset-validations- form validationember-cli-flash- notifications
Please see our package.json file dependencies.
Consul specific
Namespace support
Whilst Consul namespaces are a CE feature of Consul, we almost always build
things 'pretending' that namespaces are always enabled. Then there is code
within the data layer of the application that removes any namespace related
query parameters.
Namespaces are a little more complex to think about than other things in
Consul as we have the idea of 'the default' namespace, and then additionally
an ACL token can have 'a default' namespace (not the same as 'the default'
namespace) which is a little like a namespace that is assigned to the token,
or where the token originated from. The Consul backend can then use the tokens
default/origin/assigned namespace to figure out whether and how to filter any
results by namespace before sending those results to the UI.
When you are requesting data from the API you should always include a
namespace (if applicable to the API endpoint), but you should never default
the namespace value to 'the default' namespace - default. Conversly, when
receiving data from an API response, there are some places where we do default
a namespace to 'the default' namespace default. The easiest way to think
about this is: If its on the way out, don't add a default ever, if its on the
way in you may need to default any potentially empty Namespace parameters todefault (due to using Consul without namespaces enabled), there is a
NspaceAbility and/or an env feature flag to help you to discover this.
We've made various decisions in the UI to make it hard to omit a namespace
when you shouldn't, for example using our DataSource component requires you to
include a defined namespace parameter (even if the value for that is ''),
and the route parameter nspace value will always default to ''. It also
reminds you to be continually thinking about namespaces whilst adding new
features.
Lastly, we always refer to namespaces in code as the word nspace ornspaces. Both ns and namespace have other purposes in Ember and Ember
Data (in various places one is significant but not the other and vice versa).
Always using the term nspace means we avoid any strange bugs or
clashes/overwrites with anything in Ember or Ember Data core. The only place
where we use ns or Namespace is to construct API requests/responses.
Sometimes when using this term in CSS or on plain javascript objects, this may
seem unnecessary but it just helps smooth over writing code around namespaces
as you don't have the split second decision of whether you write ns ornamespace - just use nspace and everything will be fine :)
Ui Packages Consul Ui Docs Testing (`ui/packages/consul-ui/docs/testing.mdx`)
Testing
Running Tests
Our Ember application uses QUnit and QUnit's HTML runner in order to run tests.
As a human both of these should always be used to run tests. Currently you can
start the tests running by typing:
make test-oss-view...in your terminal. This command will be changed to be the default make test
command at some point in the future.
As a human please always run the tests in a browser. You will sometimes spot
tiny details that are wrong with the application if you do this. We have othermake targets for running tests as a machine. Please don't use these machine
oriented targets as a human.
To re-run tests, simply refresh your browser or click the [Go] button in QUnit's
HTML test runner. If you then want to filter which tests to run you can use the
filter field in QUnit's HTML test runner to run a subset of tests (see below).
If you have started the tests with a filter, you don't have to quit everything
and start again in order to filter by something different.
Browser support
Please check our current browser support when adding features or fixing bugs.
At the time of writing we use a rough ~2 years back support matrix for our
browser support. As most of our targetted browsers are 'evergreen' and
auto-update this feels like it strikes a balance between the folks that are
likely to be using the Consul UI and the desire to maintain a modern
codebase/feature set. This is a balance that should always be revisited and is
not set in stone. i.e. if you need to use something check-in with the team
first, it might be the time to bump again.
Please see ../config/targets.js for our current support.
Running a single or a range of tests
Please use the same make test-oss-view command to get the tests up and
running. Once you have opened your browser window to view the tests, click the
[Abort] button to stop the test running. Then use the QUnit filter input text
field to filter by the tests you want to run.
Alternatively you can also run a single test by bypassing make and using:
pnpm test:oss:view --filter=<test name substring>Please note: If filtering using the CLI, normally you would expect a filter to
accept filenames. This is not the case here. When using Ember, tests are
filtered by test name not file name. You can get these test names by opening the
test file itself and finding the test you want run. You may need to escape
characters when doing that so be aware of that if you are copypasting.
Please be aware this that in bypassing make you will not have the project
setup for you automatically, certain housekeeping duties will not be performed
and you may skip some tests that are important for the thing (or things) you are
working on.
We also run a quick lint check for you (plus a few other things) that you should
fix up before you run your test.
If you do use pnpm --filter, please run these other checks before submitting a
PR. If you just use make test-oss-view you (and nobody else) needs to worry
about those things.
Additionally, there are ongoing discussions regarding moving to use npm and or We have moved to pnpm. If you are using
corepack) as a package manager, so if you are using yarn, we may break your
workflow.make, don't worry as it stands right now nothing
will change.
If you specify your filter using the CLI and you make a typo or type your filter
wrong, or want to run a different filter. Don't quit the executing CLI command
and restart. That takes comparatively long time. You may aswell wait and then
amend the typo in QUnit's HTML test runner's filter input field.
Ember folks can also just use ember test
and bypass both pnpm and make but all of the above regarding using pnpm
instead of make applies there also.
Please note: Currently the QUnit runner we depend on has an unfortunate UX
issue which means the filter input field is obscured by the frame used to run
your tests in. Until this is fixed in a version of QUnit we use, you can use
the Toggle QUnit Display bookmarklet to easily toggle the visibility of this window (you
should drag the below link to your browser bookmark bar, then clicking it
whilst on the test runner will hide the display):
| Link/Bookmarklet | Description |
|---|---|
| Toggle QUnit Display | Toggle the QUnit display on or off during browser testing |
This also obscures test errors, messaging and the buttons to help you to re-run
a single test if you want to. So if you are doing a lot of testing for your
feature you might want to consider adding this to your bookmarks bar.
Multiple 'spawning' test runners
Ember's testing support also has an unfortunate UX issue in that it can
sometimes spawn multiple test runners and once you have a few of these opened
for you things can get very confusing. To disable Ember from automatically
opening test runner windows for you, there is non-Ember environment variable we
added to tell it to stop doing this:
export TESTEM_AUTOLAUNCH=0Suggestion: You could add this to your bash profile if you find it useful.
The above will prevent Ember's 'testem' implementation from automatically
opening windows for you. This of course means you have to navigate to the test
runner yourself, but that's sometimes a small wrinkle compared to having lots
and lots and lots of test runner windows open.
Testing different scenarios
Most tests are written to automatically switch between different scenarios
during the test so you don't usually have to worry about this, just run the
tests and you are done.
The exception is our namespace testing. We run our entire acceptance test
suite 4 or 5 times for different namespace settings. If you want to run some
acceptance tests with namespace scenarios enabled, this could be run by
using:
make test-view...but please don't do that, it takes a long time.
In CI these tests are run by a machine by using:
make test...but again as a human, please don't do that, it takes a long time. PRs are
blocked for merge until these tests pass CI, i.e. the machine will run it for
you.
Of course there are times where you want to run these tests as a human. If you
want to run a specfic test and include testing those tests whilst namespaces
are enabled then run the test suite as you usually would:
make test-view-ossIf you only want to run a filtered set of tests use the QUnit filter input
field to filter for the test you want run (for example maybe dc / acls)
Then use our development bookmarklet for turning on namespace support in the
UI:
| Link/Bookmarklet | Description |
|---|---|
| Enable Nspaces | Enable Namespace Support |
When you click that it will run the filtered tests against all 'types' of
namespace scenarios.
Finally, for integration testing where we need to check for namespace support,
these tests are always run in our integration tests (these run a lot faster
than acceptance tests). You probably only want to use the above approach when
needing to test specific acceptance tests with namespace scenarios.
Writing tests
Speaking generally, currently Consul favours unit tests (in ./tests/unit)
and acceptance tests (in ./tests/acceptance). We also have a fair amount of
integration tests (in ./tests/integration) for testing that systems of
combined units work well together. What ember traditionally calls integration
tests, are mostly 'component' tests. We have far less of these as the vast
majority of our components are just 'glorified partials' and mostly contain
very simple template logic that is mostly (but not always) not very worthwhile
testing.
Overall, please don't test things we don't own for example don't spend time
testing that Ember's handlebars templating is printing out variables
correctly. Don't spend time testing that ember-composable-helpers not
helper is working correctly. The great thing about a declarative template
(basically a configuration file) is that it probably won't need much testing
in isolation.
Acceptance tests
Our acceptance tests use a BDD style of testing. This means tests are
relatively simple to write, the writer does not need to think about
asynchronicity, and best of all they are very easy to read and understand what
is being tested, and therefore anyone can roughly understand what is being
tested/passing (or failing) tests.
We have roughly 20-30 reusable BDD steps you can use to write tests. We
occasionally add a new step but not very often. You should be able to write
the majority of your tests using the steps we already have. The steps
themselves are in ./tests/steps/{assertions,debug,double,interactions} and
they are just simple javascript optionally asynchronous functions that are
specifically built to be highly reusable.
If you come to write a new step, please take some time to think about how
reusable it will be and try to word the title of the step taking that into
account. Look at some of the existing steps for examples.
We use the yadda npm package for our BDD support, just as the ember addon we
originally used for this used yadda. In the future we'd like to re-consider
and potentially use cucumber.js.
In order to create a new acceptance test/feature please use the ember
generator to do so:
ember generate feature dc/model/thingThis creates some additional JS that unfortunately is required by the ember
addon, so until we move away from that addon, this is the easiest way to get
started with a brand new acceptance test.
If you are not familiar with the steps available to write tests the best way
to write a new test is to look at the tests already written/and or scan
through the steps folders to see what's available. As mentioned above, there
are only around 30 steps.
We make some use of ember-page-objects to encapsulate some DOM
selecting/functionality. To be fair we kind of dip in and out of usingember-page-objects and there is ongoing work to remove it entirely and
replace it with something simpler to work with. We also haveember-test-selectors installed. So please use either a page object or a
test-selector for selecting/asserting DOM elements when writing tests. Prefer
page objects, but if you find it complicated/difficult or just plain smelly,
feel free to use a test-selector and use that directly in the test instead. At
some point we will reassess this and bring it all into line with one single
approach.
Most importantly: Please try to only use either a page-object or a
test-selector for DOM assertions/selections. Please try to avoid using any
complex selectors.
Integration testing
We use sinon for making mocks for testing that systems work well together,
other than that please see Ember's own documentation for writing tests.
Remember: Integration testing does not mean 'component tests' it means
checking to make sure the units of a system work well together.
If you want to test a component in isolation whilst working on it, please create
an .mdx file in the component folder and use that to work on the component. We
use these .mdx files to isolate the component, work on it, manually/visually
test it and document it for others. If somebody needs to change the component in
future this means they already have test scenarios enabled/arranged for them to
use. Components should generally be small things, or made up of further small
components that are individually testable in isolation. There shouldn't be much
logic required in a new component. And if there is, and it's important enough to
need testing, you should consider splitting the logic off and testing it outside
of the component via integration testing and/or unit testing. This more manual
kind of component testing should be thought of as 'unit testing for a
component'. You are very unlikely to break something unless you change the code
of the actual component itself, and if you are doing this you are working with
the mdx file.and likely to notice. Whilst not perfect, this is where we are
right now.
We have very few single components that are an integrated system of further
components (where automated testing would be more necessary/useful) and our
application aims to stay that way for the foreseeable future. But if you write
something that may require more complicated and/or integrated testing consider
splitting those pieces into smaller more testable chunks that can be
automatically tested.
Unit testing
Unit testing should be just that, testing a single unit, and therefore should
be as vanilla JS as possible. Our application generally favours using vanilla
JS where possible and then plumbing that functionality/logic into the Ember
framework. Therefore there should be very little mention of Ember in our Unit
tests (apart from Ember's test harness).
We do have coverage enabled for most of our unit tests, but it's not something
we generally use/keep an eye on. Coverage shouldn't necessarily be used as a
yard stick. We have it enabled for folks that find it useful/beneficial and
whilst good coverage is a positive it's not always an essential.
Coverage is run in CI but you can also run it locally with:
make test-coverage-viewWe also have a fair amount of node based unit tests to test things that can't
be tested using Ember's testing approach or are just quicker to run using
node. You'll see these executed using TAP reporting when you run the make test-view-oss command. But they can be executed on their own with:
make test-nodeThese node based unit tests live in ./node-tests and use the beautifully
simple tape test runner with TAP reporting.
Lastly, just to reiterate when testing, just use:
make test-oss-viewIf you need to filter to only run specific tests, use the QUnit HTML runner's
filter input field.
Ui Packages Consul Ui Docs Upgrades (`ui/packages/consul-ui/docs/upgrades.mdx`)
Upgrades
Node
Node upgrades should be done when it is convienient, preferably using the
latest Active LTS version (https://nodejs.org/en/about/releases/) that also
corresponds with the ember version we are on (https://github.com/ember-cli/ember-cli/blob/master/docs/node-support.md)
Aswell as bumping our .nvmrc and package.json files, when bumping
node versions, please check with the rest of the team to see if there are
other repositories that should track the same node version as this repository.
Ember
TBD
Other
TBD
Test Integration Connect Envoy Docs Windows Testing Architecture (`test/integration/connect/envoy/docs/windows-testing-architecture.md`)
Windows Testing Architecture
Index
About
The purpose of this document is not only to explain why the testing architecture is different on Windows but also to describe how the Single Container test architecture is composed.
Testing Architectures
Linux Test Architecture
On Linux, tests take advantage of the Host network feature (only available for Linux containers). This means that every container within the network shares the host’s networking namespace. The network stack for every container that uses this network mode won’t be isolated from the Docker host and won’t get their own IP address.
Every time a test is run, a directory called workdir is created, here all the required files to run the tests are copied. Then this same directory is mounted as a named volume, a container with a Kubernetes pause image tagged as envoy_workdir_1 is run to keep the volume accessible as other containers start while running the tests. Linux containers allow file system operations on runtime unlike Windows containers.
Replicating the Linux Test Architecture on Windows
As we previously mentioned, on Windows there is no Host networking feature, so we went with NAT network instead. The main consequences of this is that now each container has their own networking stack (IP address) separated from each other, they can communicate among themselves using Docker's DNS feature (using the containers name) but no longer through localhost.
Another problem we are facing while sticking to this architecture, is that configuration files assume that every service (services run by fortio and Envoy's sidecar proxy service) are running in localhost. Though we had some partial success on modifying those files on runtime still we are finding issues related to this.
Test's assertions are composed of either function calls or curl executions, we managed this by mapping those calls to the corresponding container name.
Above, the failing connections are depicted. We kept the same architecture as on Linux and worked around trying to solve those connectivity issues.
Finally, after serveral tries, it was decided that instead of replicating the Linux architecture on Windows, it was more straightforward just to have a single container with all the required components to run the tests. This single container test architecture is the approach that works best on Windows.
Single Container Test Architecture
As mentioned above, the single container approach, means building a Windows Docker image not only with Consul and Envoy, but also with all the tools required to execute the existing Envoy integration tests.
Below you can find a list and a brief description of those components.
Docker Image Components
The Docker image used for the Consul - Envoy integration tests has several components needed to run those tests.
- Main Components:
- Additional tools:
Bats
BATS stands for Bash Automated Testing System and is the one in charge of executing the tests.
Fortio
Fortio is a microservices (http, grpc) load testing library, command line tool, advanced echo server, and web UI. It is used to run the services registered into Consul during the integration tests.
Jaegertracing
Jaeger is open source software for tracing transactions between distributed services. It's used for monitoring and troubleshooting complex microservices environments. It is used along with Openzipkin in some test cases.
Openzipkin
Zipkin is also a tracing software.
Socat
Socat is a command line based utility that establishes two bidirectional byte streams and transfers data between them. On this integration tests it is used to redirect Envoy's stats. There is no official Windows version. We are using this unofficial release available here.
Git Bash
This tool is only used in Windows tests, it was added to the Docker image to be able to use some Linux commands during test execution.
JQ
Jq is a lightweight and flexible command-line JSON processor. It is used in several tests to modify and filter JSON outputs.
Netcat
Netcat is a simple program that reads and writes data across networks, much the same way that cat reads and writes data to files.
Openssl
Open SSL is an all-around cryptography library that offers open-source application of the TLS protocol. It is used to verify that the correct tls certificates are being provisioned during tests.
--- METRICS ---
- Files Extracted: 36
- Estimated Token Budget: ~36991 tokens
- Canonical Reference: https://codewiki.google/github.com/hashicorp/consul