diff --git a/.changelog/11774.txt b/.changelog/11774.txt new file mode 100644 index 000000000..bf52d2c3a --- /dev/null +++ b/.changelog/11774.txt @@ -0,0 +1,4 @@ +```release-note:bug +ui: Differentiate between Service Meta and Node Meta when choosing search fields +in Service Instance listings +``` diff --git a/.changelog/11781.txt b/.changelog/11781.txt new file mode 100644 index 000000000..754d9e01b --- /dev/null +++ b/.changelog/11781.txt @@ -0,0 +1,3 @@ +```release-note:bug +cli: when creating a private key, save the file with mode 0600 so that only the user has read permission. +``` diff --git a/.changelog/11868.txt b/.changelog/11868.txt new file mode 100644 index 000000000..a81ac0683 --- /dev/null +++ b/.changelog/11868.txt @@ -0,0 +1,5 @@ +```release-note:bug +ui: Fix an issue where attempting to delete a policy from the policy detail page when +attached to a token would result in the delete button disappearing and no +deletion being attempted +``` diff --git a/.changelog/11891.txt b/.changelog/11891.txt new file mode 100644 index 000000000..3ddb9a09d --- /dev/null +++ b/.changelog/11891.txt @@ -0,0 +1,4 @@ +```release-note:bug +ui: Fixes an issue where once a 403 page is displayed in some circumstances its +diffcult to click back to where you where before receiving a 403 +``` diff --git a/.changelog/11892.txt b/.changelog/11892.txt new file mode 100644 index 000000000..5709c6569 --- /dev/null +++ b/.changelog/11892.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Ensure a login buttons appear for some error states, plus text amends +``` diff --git a/.github/scripts/metrics_checker.sh b/.github/scripts/metrics_checker.sh new file mode 100755 index 000000000..067fdd96a --- /dev/null +++ b/.github/scripts/metrics_checker.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -uo pipefail + +### This script checks if any metric behavior has been modified. +### The checks rely on the git diff against origin/main +### It is still up to the reviewer to make sure that any tests added are needed and meaningful. + +# search for any "new" or modified metric emissions +metrics_modified=$(git --no-pager diff HEAD origin/main | grep -i "SetGauge\|EmitKey\|IncrCounter\|AddSample\|MeasureSince\|UpdateFilter") +# search for PR body or title metric references +metrics_in_pr_body=$(echo "${PR_BODY-""}" | grep -i "metric") +metrics_in_pr_title=$(echo "${PR_TITLE-""}" | grep -i "metric") + +# if there have been code changes to any metric or mention of metrics in the pull request body +if [ "$metrics_modified" ] || [ "$metrics_in_pr_body" ] || [ "$metrics_in_pr_title" ]; then + # need to check if there are modifications to metrics_test + test_files_regex="*_test.go" + modified_metrics_test_files=$(git --no-pager diff HEAD "$(git merge-base HEAD "origin/main")" -- "$test_files_regex" | grep -i "metric") + if [ "$modified_metrics_test_files" ]; then + # 1 happy path: metrics_test has been modified bc we modified metrics behavior + echo "PR seems to modify metrics behavior. It seems it may have added tests to the metrics as well." + exit 0 + else + echo "PR seems to modify metrics behavior. It seems no tests or test behavior has been modified." + echo "Please update the PR with any relevant updated testing or add a pr/no-metrics-test label to skip this check." + exit 1 + fi + +else + # no metrics modified in code, nothing to check + echo "No metric behavior seems to be modified." + exit 0 +fi diff --git a/.github/workflows/pr-metrics-test-checker.yml b/.github/workflows/pr-metrics-test-checker.yml new file mode 100644 index 000000000..3019d357c --- /dev/null +++ b/.github/workflows/pr-metrics-test-checker.yml @@ -0,0 +1,25 @@ +name: "Check for metrics tests" +on: + pull_request: + types: [ opened, synchronize, labeled ] + # Runs on PRs to main + branches: + - main + +jobs: + metrics_test_check: + if: "!contains(github.event.pull_request.labels.*.name, 'pr/no-metrics-test')" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + name: "checkout repo" + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 # by default the checkout action doesn't checkout all branches + - name: "Check for metrics modifications" + run: ./.github/scripts/metrics_checker.sh + # as of now, cannot use github vars in "external" scripts; ref: https://github.community/t/using-github-action-environment-variables-in-shell-script/18330 + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + shell: bash diff --git a/agent/consul/autopilot_test.go b/agent/consul/autopilot_test.go index 8b2632411..1935fc5e8 100644 --- a/agent/consul/autopilot_test.go +++ b/agent/consul/autopilot_test.go @@ -6,12 +6,13 @@ import ( "testing" "time" - "github.com/hashicorp/consul/agent/structs" - "github.com/hashicorp/consul/sdk/testutil/retry" - "github.com/hashicorp/consul/testrpc" "github.com/hashicorp/raft" "github.com/hashicorp/serf/serf" "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/sdk/testutil/retry" + "github.com/hashicorp/consul/testrpc" ) func TestAutopilot_IdempotentShutdown(t *testing.T) { @@ -19,7 +20,7 @@ func TestAutopilot_IdempotentShutdown(t *testing.T) { t.Skip("too slow for testing.Short") } - dir1, s1 := testServerWithConfig(t, nil) + dir1, s1 := testServerWithConfig(t) defer os.RemoveAll(dir1) defer s1.Shutdown() retry.Run(t, func(r *retry.R) { r.Check(waitForLeader(s1)) }) diff --git a/agent/consul/intention_endpoint_test.go b/agent/consul/intention_endpoint_test.go index ec3941348..ec8349f82 100644 --- a/agent/consul/intention_endpoint_test.go +++ b/agent/consul/intention_endpoint_test.go @@ -1599,7 +1599,7 @@ func TestIntentionList_acl(t *testing.T) { t.Parallel() - dir1, s1 := testServerWithConfig(t, testServerACLConfig(nil)) + dir1, s1 := testServerWithConfig(t, testServerACLConfig) defer os.RemoveAll(dir1) defer s1.Shutdown() codec := rpcClient(t, s1) diff --git a/agent/consul/internal_endpoint_test.go b/agent/consul/internal_endpoint_test.go index f9105304f..7a354e7b5 100644 --- a/agent/consul/internal_endpoint_test.go +++ b/agent/consul/internal_endpoint_test.go @@ -1784,7 +1784,7 @@ func TestInternal_GatewayIntentions_aclDeny(t *testing.T) { t.Skip("too slow for testing.Short") } - dir1, s1 := testServerWithConfig(t, testServerACLConfig(nil)) + dir1, s1 := testServerWithConfig(t, testServerACLConfig) defer os.RemoveAll(dir1) defer s1.Shutdown() codec := rpcClient(t, s1) diff --git a/agent/consul/server_test.go b/agent/consul/server_test.go index d608b8fa0..0c7b84223 100644 --- a/agent/consul/server_test.go +++ b/agent/consul/server_test.go @@ -66,21 +66,12 @@ func testTLSCertificates(serverName string) (cert string, key string, cacert str return cert, privateKey, ca, nil } -// testServerACLConfig wraps another arbitrary Config altering callback -// to setup some common ACL configurations. A new callback func will -// be returned that has the original callback invoked after setting -// up all of the ACL configurations (so they can still be overridden) -func testServerACLConfig(cb func(*Config)) func(*Config) { - return func(c *Config) { - c.PrimaryDatacenter = "dc1" - c.ACLsEnabled = true - c.ACLInitialManagementToken = TestDefaultMasterToken - c.ACLResolverSettings.ACLDefaultPolicy = "deny" - - if cb != nil { - cb(c) - } - } +// testServerACLConfig setup some common ACL configurations. +func testServerACLConfig(c *Config) { + c.PrimaryDatacenter = "dc1" + c.ACLsEnabled = true + c.ACLInitialManagementToken = TestDefaultMasterToken + c.ACLResolverSettings.ACLDefaultPolicy = "deny" } func configureTLS(config *Config) { @@ -164,8 +155,6 @@ func testServerConfig(t *testing.T) (string, *Config) { config.ServerHealthInterval = 50 * time.Millisecond config.AutopilotInterval = 100 * time.Millisecond - config.Build = "1.7.2" - config.CoordinateUpdatePeriod = 100 * time.Millisecond config.LeaveDrainTime = 1 * time.Millisecond @@ -187,14 +176,12 @@ func testServerConfig(t *testing.T) (string, *Config) { return dir, config } +// Deprecated: use testServerWithConfig instead. It does the same thing and more. func testServer(t *testing.T) (string, *Server) { - return testServerWithConfig(t, func(c *Config) { - c.Datacenter = "dc1" - c.PrimaryDatacenter = "dc1" - c.Bootstrap = true - }) + return testServerWithConfig(t) } +// Deprecated: use testServerWithConfig func testServerDC(t *testing.T, dc string) (string, *Server) { return testServerWithConfig(t, func(c *Config) { c.Datacenter = dc @@ -202,6 +189,7 @@ func testServerDC(t *testing.T, dc string) (string, *Server) { }) } +// Deprecated: use testServerWithConfig func testServerDCBootstrap(t *testing.T, dc string, bootstrap bool) (string, *Server) { return testServerWithConfig(t, func(c *Config) { c.Datacenter = dc @@ -210,6 +198,7 @@ func testServerDCBootstrap(t *testing.T, dc string, bootstrap bool) (string, *Se }) } +// Deprecated: use testServerWithConfig func testServerDCExpect(t *testing.T, dc string, expect int) (string, *Server) { return testServerWithConfig(t, func(c *Config) { c.Datacenter = dc @@ -218,16 +207,7 @@ func testServerDCExpect(t *testing.T, dc string, expect int) (string, *Server) { }) } -func testServerDCExpectNonVoter(t *testing.T, dc string, expect int) (string, *Server) { - return testServerWithConfig(t, func(c *Config) { - c.Datacenter = dc - c.Bootstrap = false - c.BootstrapExpect = expect - c.ReadReplica = true - }) -} - -func testServerWithConfig(t *testing.T, cb func(*Config)) (string, *Server) { +func testServerWithConfig(t *testing.T, configOpts ...func(*Config)) (string, *Server) { var dir string var srv *Server @@ -235,8 +215,8 @@ func testServerWithConfig(t *testing.T, cb func(*Config)) (string, *Server) { retry.RunWith(retry.ThreeTimes(), t, func(r *retry.R) { var config *Config dir, config = testServerConfig(t) - if cb != nil { - cb(config) + for _, fn := range configOpts { + fn(config) } // Apply config to copied fields because many tests only set the old @@ -257,8 +237,11 @@ func testServerWithConfig(t *testing.T, cb func(*Config)) (string, *Server) { // cb is a function that can alter the test servers configuration prior to the server starting. func testACLServerWithConfig(t *testing.T, cb func(*Config), initReplicationToken bool) (string, *Server, rpc.ClientCodec) { - dir, srv := testServerWithConfig(t, testServerACLConfig(cb)) - t.Cleanup(func() { srv.Shutdown() }) + opts := []func(*Config){testServerACLConfig} + if cb != nil { + opts = append(opts, cb) + } + dir, srv := testServerWithConfig(t, opts...) if initReplicationToken { // setup some tokens here so we get less warnings in the logs @@ -266,7 +249,6 @@ func testACLServerWithConfig(t *testing.T, cb func(*Config), initReplicationToke } codec := rpcClient(t, srv) - t.Cleanup(func() { codec.Close() }) return dir, srv, codec } @@ -1284,7 +1266,11 @@ func TestServer_Expect_NonVoters(t *testing.T) { } t.Parallel() - dir1, s1 := testServerDCExpectNonVoter(t, "dc1", 2) + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.Bootstrap = false + c.BootstrapExpect = 2 + c.ReadReplica = true + }) defer os.RemoveAll(dir1) defer s1.Shutdown() diff --git a/agent/consul/system_metadata_test.go b/agent/consul/system_metadata_test.go index 30f57defd..61f62c8d1 100644 --- a/agent/consul/system_metadata_test.go +++ b/agent/consul/system_metadata_test.go @@ -4,9 +4,10 @@ import ( "os" "testing" + "github.com/stretchr/testify/require" + "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/testrpc" - "github.com/stretchr/testify/require" ) func TestLeader_SystemMetadata_CRUD(t *testing.T) { @@ -32,10 +33,10 @@ func TestLeader_SystemMetadata_CRUD(t *testing.T) { state := srv.fsm.State() - // Initially has no entries + // Initially has one entry for virtual-ips feature flag _, entries, err := state.SystemMetadataList(nil) require.NoError(t, err) - require.Len(t, entries, 0) + require.Len(t, entries, 1) // Create 3 require.NoError(t, srv.setSystemMetadataKey("key1", "val1")) @@ -52,12 +53,13 @@ func TestLeader_SystemMetadata_CRUD(t *testing.T) { _, entries, err = state.SystemMetadataList(nil) require.NoError(t, err) - require.Len(t, entries, 3) + require.Len(t, entries, 4) require.Equal(t, map[string]string{ - "key1": "val1", - "key2": "val2", - "key3": "", + structs.SystemMetadataVirtualIPsEnabled: "true", + "key1": "val1", + "key2": "val2", + "key3": "", }, mapify(entries)) // Update one and delete one. @@ -66,10 +68,11 @@ func TestLeader_SystemMetadata_CRUD(t *testing.T) { _, entries, err = state.SystemMetadataList(nil) require.NoError(t, err) - require.Len(t, entries, 2) + require.Len(t, entries, 3) require.Equal(t, map[string]string{ - "key2": "val2", - "key3": "val3", + structs.SystemMetadataVirtualIPsEnabled: "true", + "key2": "val2", + "key3": "val3", }, mapify(entries)) } diff --git a/command/tls/ca/create/tls_ca_create.go b/command/tls/ca/create/tls_ca_create.go index ceef70b37..810d452c4 100644 --- a/command/tls/ca/create/tls_ca_create.go +++ b/command/tls/ca/create/tls_ca_create.go @@ -83,7 +83,7 @@ func (c *cmd) Run(args []string) int { } c.UI.Output("==> Saved " + certFileName) - if err := file.WriteAtomicWithPerms(pkFileName, []byte(pk), 0755, 0666); err != nil { + if err := file.WriteAtomicWithPerms(pkFileName, []byte(pk), 0755, 0600); err != nil { c.UI.Error(err.Error()) return 1 } diff --git a/command/tls/ca/create/tls_ca_create_test.go b/command/tls/ca/create/tls_ca_create_test.go index 568958959..19c5fb965 100644 --- a/command/tls/ca/create/tls_ca_create_test.go +++ b/command/tls/ca/create/tls_ca_create_test.go @@ -3,6 +3,7 @@ package create import ( "crypto" "crypto/x509" + "io/fs" "io/ioutil" "os" "strings" @@ -120,6 +121,14 @@ func expectFiles(t *testing.T, caPath, keyPath string) (*x509.Certificate, crypt require.FileExists(t, caPath) require.FileExists(t, keyPath) + fi, err := os.Stat(keyPath) + if err != nil { + t.Fatal("should not happen", err) + } + if want, have := fs.FileMode(0600), fi.Mode().Perm(); want != have { + t.Fatalf("private key file %s: permissions: want: %o; have: %o", keyPath, want, have) + } + caData, err := ioutil.ReadFile(caPath) require.NoError(t, err) keyData, err := ioutil.ReadFile(keyPath) diff --git a/command/tls/cert/create/tls_cert_create.go b/command/tls/cert/create/tls_cert_create.go index 6281ca3ae..b1cdaa131 100644 --- a/command/tls/cert/create/tls_cert_create.go +++ b/command/tls/cert/create/tls_cert_create.go @@ -196,7 +196,7 @@ func (c *cmd) Run(args []string) int { } c.UI.Output("==> Saved " + certFileName) - if err := file.WriteAtomicWithPerms(pkFileName, []byte(priv), 0755, 0666); err != nil { + if err := file.WriteAtomicWithPerms(pkFileName, []byte(priv), 0755, 0600); err != nil { c.UI.Error(err.Error()) return 1 } diff --git a/command/tls/cert/create/tls_cert_create_test.go b/command/tls/cert/create/tls_cert_create_test.go index 306eed8df..78f75eb11 100644 --- a/command/tls/cert/create/tls_cert_create_test.go +++ b/command/tls/cert/create/tls_cert_create_test.go @@ -3,6 +3,7 @@ package create import ( "crypto" "crypto/x509" + "io/fs" "io/ioutil" "net" "os" @@ -242,6 +243,14 @@ func expectFiles(t *testing.T, certPath, keyPath string) (*x509.Certificate, cry require.FileExists(t, certPath) require.FileExists(t, keyPath) + fi, err := os.Stat(keyPath) + if err != nil { + t.Fatal("should not happen", err) + } + if want, have := fs.FileMode(0600), fi.Mode().Perm(); want != have { + t.Fatalf("private key file %s: permissions: want: %o; have: %o", keyPath, want, have) + } + certData, err := ioutil.ReadFile(certPath) require.NoError(t, err) keyData, err := ioutil.ReadFile(keyPath) diff --git a/ui/packages/consul-ui/app/components/buttons/index.scss b/ui/packages/consul-ui/app/components/buttons/index.scss index d7112051d..6bf35de27 100644 --- a/ui/packages/consul-ui/app/components/buttons/index.scss +++ b/ui/packages/consul-ui/app/components/buttons/index.scss @@ -13,6 +13,7 @@ button.type-cancel { @extend %secondary-button; } .with-confirmation .type-delete, +.modal-dialog .type-delete, %app-view-content form button[type='button'].type-delete { @extend %dangerous-button; } diff --git a/ui/packages/consul-ui/app/components/error-state/README.mdx b/ui/packages/consul-ui/app/components/error-state/README.mdx index 252b41937..2397d4dde 100644 --- a/ui/packages/consul-ui/app/components/error-state/README.mdx +++ b/ui/packages/consul-ui/app/components/error-state/README.mdx @@ -7,8 +7,11 @@ for more details. Using this component for all of our errors means we can show a consistent error page for generic errors. -This component show slighltly different visuals and copy depending on the -`status` of the error (the status is generally a HTTP error code) +This component show slightly different visuals and copy depending on the +`status` of the error (the status is generally a HTTP error code). + +Please note: The examples below use a `hash` for demonstration purposes, you'll +probably just be using an `error` object in real-life. ## Arguments @@ -17,12 +20,26 @@ This component show slighltly different visuals and copy depending on the | `login` | `Function` | `undefined` | A login action to call when the login button is pressed (if not provided no login button will be shown | | `error` | `Object` | `undefined` | 'Consul UI error shaped' JSON `{status: String, message: String, detail: String}` | +Specifically 403 errors **always** use the same header/body copy, this is hardcoded in and not currently overridable. + ```hbs preview-template ``` +Other StatusCodes have a global default text but these *are* overridable by using the message/detail properties of the Consul UI shaped errors. + +```hbs preview-template + +``` + As with `EmptyState` you can optionally chose to show a login button using the `@login` argument. diff --git a/ui/packages/consul-ui/app/components/error-state/index.hbs b/ui/packages/consul-ui/app/components/error-state/index.hbs index d32ad7c0e..d05bb30b6 100644 --- a/ui/packages/consul-ui/app/components/error-state/index.hbs +++ b/ui/packages/consul-ui/app/components/error-state/index.hbs @@ -4,43 +4,63 @@ @login={{@login}} > -

{{or @error.message "Consul returned an error"}}

+

+ {{or @error.message "Consul returned an error"}} +

{{#if @error.status }} -

Error {{@error.status}}

+

+ Error {{@error.status}} +

{{/if}} - {{#if error.detail}} -

- {{error.detail}} -

- {{else}} -

+

+ {{#if @error.detail}} + {{@error.detail}} + {{else}} You may have visited a URL that is loading an unknown resource, so you can try going back to the root or try re-submitting your ACL Token/SecretID by going back to ACLs. -

- {{/if}} + {{/if}} +

{{else}} -

You are not authorized

+

+ You are not authorized +

-

Error 403

+

+ Error {{@error.status}} +

@@ -49,10 +69,20 @@

diff --git a/ui/packages/consul-ui/app/components/modal-dialog/README.mdx b/ui/packages/consul-ui/app/components/modal-dialog/README.mdx index 0fbcbde88..c249255c0 100644 --- a/ui/packages/consul-ui/app/components/modal-dialog/README.mdx +++ b/ui/packages/consul-ui/app/components/modal-dialog/README.mdx @@ -3,30 +3,8 @@ class: ember --- # ModalDialog -## Arguments - -| Argument | Type | Default | Description | -| --- | --- | --- | --- | -| `onopen` | `Function` | `undefined` | A function to call when the modal has opened | -| `onclose` | `Function` | `undefined` | A function to call when the modal has closed | -| `aria` | `Object` | `undefined` | A `hash` of aria properties used in the component, currently only label is supported | - -## Exports - -| Name | Type | Description | -| --- | --- | --- | -| `open` | `Function` | Opens the modal dialog | -| `close` | `Function` | Closes the modal dialog | - -Works in tandem with `` to render modals. First of all ensure -you have a modal layer on the page (it doesn't have to be in the same -template) - -```hbs - -``` - -Then all modals will be rendered into the `` for example: +Consul UIs modal component is a thin wrapper around the excellent `a11y-dialog`. The +most common usage will be something like the below: ```hbs preview-template ``` +All modals work in tandem with `` to render modals. First of all ensure +you have a modal layer on the page (it doesn't have to be in the same +template) + +```hbs + +``` + +Then all modals will be rendered into the ``. + +## Arguments + +| Argument | Type | Default | Description | +| --- | --- | --- | --- | +| `onopen` | `Function` | `undefined` | A function to call when the modal has opened | +| `onclose` | `Function` | `undefined` | A function to call when the modal has closed | +| `aria` | `Object` | `undefined` | A `hash` of aria properties used in the component, currently only label is supported | +| `open` | `Boolean` | `false` | Whether the modal should be initialized in its 'open' state. Useful if the modal should be controlled via handlebars conditionals. Please note this argument it not yet reactive, i.e. it is only checked on component insert not attribute update. An improvement here would be to respect this value during the update of the attribute. | + +## Exports + +| Name | Type | Description | +| --- | --- | --- | +| `open` | `Function` | Opens the modal dialog | +| `close` | `Function` | Closes the modal dialog | + + diff --git a/ui/packages/consul-ui/app/components/modal-dialog/index.js b/ui/packages/consul-ui/app/components/modal-dialog/index.js index 49e93b0d2..c7e1776fe 100644 --- a/ui/packages/consul-ui/app/components/modal-dialog/index.js +++ b/ui/packages/consul-ui/app/components/modal-dialog/index.js @@ -11,6 +11,9 @@ export default Component.extend(Slotted, { this.dialog = new A11yDialog($el); this.dialog.on('hide', () => this.onclose({ target: $el })); this.dialog.on('show', () => this.onopen({ target: $el })); + if (this.open) { + this.dialog.show(); + } }, disconnect: function($el) { this.dialog.destroy(); diff --git a/ui/packages/consul-ui/app/routes/dc/services/show/instances.js b/ui/packages/consul-ui/app/routes/dc/services/show/instances.js index 139124a7d..5c27d6497 100644 --- a/ui/packages/consul-ui/app/routes/dc/services/show/instances.js +++ b/ui/packages/consul-ui/app/routes/dc/services/show/instances.js @@ -7,7 +7,7 @@ export default class InstancesRoute extends Route { source: 'source', searchproperty: { as: 'searchproperty', - empty: [['Name', 'Tags', 'ID', 'Address', 'Port', 'Service.Meta', 'Node.Meta']], + empty: [['Name', 'Node', 'Tags', 'ID', 'Address', 'Port', 'Service.Meta', 'Node.Meta']], }, search: { as: 'filter', diff --git a/ui/packages/consul-ui/app/routing/route.js b/ui/packages/consul-ui/app/routing/route.js index 02b1edf2d..2eaabc836 100644 --- a/ui/packages/consul-ui/app/routing/route.js +++ b/ui/packages/consul-ui/app/routing/route.js @@ -1,7 +1,6 @@ import Route from '@ember/routing/route'; import { get, setProperties, action } from '@ember/object'; import { inject as service } from '@ember/service'; -import HTTPError from 'consul-ui/utils/http/error'; // paramsFor import { routes } from 'consul-ui/router'; @@ -50,27 +49,6 @@ export default class BaseRoute extends Route { } } - /** - * Inspects a custom `abilities` array on the router for this route. Every - * abililty needs to 'pass' for the route not to throw a 403 error. Anything - * more complex then this (say ORs) should use a single ability and perform - * the OR logic in the test for the ability. Note, this ability check happens - * before any calls to the backend for this model/route. - */ - async beforeModel() { - // remove any references to index as it is the same as the root routeName - const routeName = this.routeName - .split('.') - .filter(item => item !== 'index') - .join('.'); - const abilities = get(routes, `${routeName}._options.abilities`) || []; - if (abilities.length > 0) { - if (!abilities.every(ability => this.permissions.can(ability))) { - throw new HTTPError('403'); - } - } - } - /** * By default any empty string query parameters should remove the query * parameter from the URL. This is the most common behavior if you don't diff --git a/ui/packages/consul-ui/app/search/predicates/service-instance.js b/ui/packages/consul-ui/app/search/predicates/service-instance.js index 1792f6372..0f059e580 100644 --- a/ui/packages/consul-ui/app/search/predicates/service-instance.js +++ b/ui/packages/consul-ui/app/search/predicates/service-instance.js @@ -1,5 +1,6 @@ export default { Name: item => item.Name, + Node: item => item.Node.Node, Tags: item => item.Service.Tags || [], ID: item => item.Service.ID || '', Address: item => item.Address || '', diff --git a/ui/packages/consul-ui/app/services/repository/permission.js b/ui/packages/consul-ui/app/services/repository/permission.js index dd87209ae..5d6bc8084 100644 --- a/ui/packages/consul-ui/app/services/repository/permission.js +++ b/ui/packages/consul-ui/app/services/repository/permission.js @@ -155,10 +155,10 @@ export default class PermissionService extends RepositoryService { // This temporary measure should be removed again once https://github.com/hashicorp/consul/issues/11098 // has been resolved this.permissions.forEach(item => { - if(['key', 'node', 'service', 'intentions', 'session'].includes(item.Resource)) { + if (['key', 'node', 'service', 'intention', 'session'].includes(item.Resource)) { item.Allow = true; } - }) + }); /**/ return this.permissions; } diff --git a/ui/packages/consul-ui/app/templates/dc/acls/policies/-form.hbs b/ui/packages/consul-ui/app/templates/dc/acls/policies/-form.hbs index 1b75737bb..bb362b978 100644 --- a/ui/packages/consul-ui/app/templates/dc/acls/policies/-form.hbs +++ b/ui/packages/consul-ui/app/templates/dc/acls/policies/-form.hbs @@ -48,6 +48,7 @@ /> - + + +

+ {{#if (gt items.length 0)}} + No nodes found + {{else}} + Welcome to Nodes + {{/if}} +

+

There don't seem to be any registered nodes, or you may not have access to view nodes yet. diff --git a/ui/packages/consul-ui/app/templates/dc/services/index.hbs b/ui/packages/consul-ui/app/templates/dc/services/index.hbs index 27342bf3c..3d6e04933 100644 --- a/ui/packages/consul-ui/app/templates/dc/services/index.hbs +++ b/ui/packages/consul-ui/app/templates/dc/services/index.hbs @@ -105,7 +105,7 @@ as |sort filters items partition nspace|}} >

- {{#if (gt services.length 0)}} + {{#if (gt items.length 0)}} No services found {{else}} Welcome to Services @@ -114,7 +114,7 @@ as |sort filters items partition nspace|}}

- {{#if (gt services.length 0)}} + {{#if (gt items.length 0)}} No services where found matching that search, or you may not have access to view the services you are searching for. {{else}} There don't seem to be any registered services, or you may not have access to view services yet. diff --git a/ui/packages/consul-ui/app/templates/dc/services/show/intentions/index.hbs b/ui/packages/consul-ui/app/templates/dc/services/show/intentions/index.hbs index 87d2f9649..003d915f6 100644 --- a/ui/packages/consul-ui/app/templates/dc/services/show/intentions/index.hbs +++ b/ui/packages/consul-ui/app/templates/dc/services/show/intentions/index.hbs @@ -89,7 +89,9 @@ as |route|> - +

{{#if (gt items.length 0)}} diff --git a/ui/packages/consul-ui/tests/acceptance/dc/intentions/index.feature b/ui/packages/consul-ui/tests/acceptance/dc/intentions/index.feature index 29db2c9b7..d5d888f67 100644 --- a/ui/packages/consul-ui/tests/acceptance/dc/intentions/index.feature +++ b/ui/packages/consul-ui/tests/acceptance/dc/intentions/index.feature @@ -23,7 +23,12 @@ Feature: dc / intentions / index dc: dc-1 --- Then the url should be /dc-1/intentions - And I don't see create + And I see create +# We currently hardcode intention write to true until the API does what we need +# Once we can use this as we need we'll be able to un-hardcode And this test +# will fail again, at which point we can remove the above assertion and +# uncomment the below one + # And I don't see create Scenario: Viewing intentions in the listing live updates Given 1 datacenter model with the value "dc-1" Given 3 intention models diff --git a/ui/packages/consul-ui/tests/pages/dc/acls/policies/edit.js b/ui/packages/consul-ui/tests/pages/dc/acls/policies/edit.js index d3123ce2f..6324b3ede 100644 --- a/ui/packages/consul-ui/tests/pages/dc/acls/policies/edit.js +++ b/ui/packages/consul-ui/tests/pages/dc/acls/policies/edit.js @@ -9,7 +9,7 @@ export default function(visitable, submitable, deletable, cancelable, clickable, datacenter: clickable('[name="policy[Datacenters]"]'), deleteModal: { resetScope: true, - scope: '[data-test-delete-modal]', + scope: '[data-test-delete-modal]:not([aria-hidden="true"])', ...deletable({}), }, }; diff --git a/ui/packages/consul-ui/translations/common/en-us.yaml b/ui/packages/consul-ui/translations/common/en-us.yaml index fe1f462b4..e5700effe 100644 --- a/ui/packages/consul-ui/translations/common/en-us.yaml +++ b/ui/packages/consul-ui/translations/common/en-us.yaml @@ -30,6 +30,8 @@ consul: terminating-gateway: Terminating Gateway mesh-gateway: Mesh Gateway status: Health Status + service.meta: Service Meta + node.meta: Node Meta service-name: Service Name node-name: Node Name accessorid: AccessorID diff --git a/website/components/footer/style.css b/website/components/footer/style.css index 26edf1933..5f04743af 100644 --- a/website/components/footer/style.css +++ b/website/components/footer/style.css @@ -2,7 +2,6 @@ padding: 25px 0 17px 0; flex-shrink: 0; display: flex; - border-top: 1px solid var(--gray-5); & .g-grid-container { display: flex; diff --git a/website/components/io-card-container/index.tsx b/website/components/io-card-container/index.tsx new file mode 100644 index 000000000..e71ab886e --- /dev/null +++ b/website/components/io-card-container/index.tsx @@ -0,0 +1,82 @@ +import * as React from 'react' +import classNames from 'classnames' +import Button from '@hashicorp/react-button' +import IoCard, { IoCardProps } from 'components/io-card' +import s from './style.module.css' + +interface IoCardContaianerProps { + theme?: 'light' | 'dark' + heading?: string + description?: string + label?: string + cta?: { + url: string + text: string + } + cardsPerRow: 3 | 4 + cards: Array +} + +export default function IoCardContaianer({ + theme = 'light', + heading, + description, + label, + cta, + cardsPerRow = 3, + cards, +}: IoCardContaianerProps): React.ReactElement { + return ( +
+ {heading || description ? ( +
+ {heading ?

{heading}

: null} + {description ?

{description}

: null} +
+ ) : null} + {cards.length ? ( + <> + {label || cta ? ( +
+ {label ?

{label}

: null} + {cta ? ( +
+ ) : null} +
    + {cards.map((card, index) => { + return ( + // Index is stable + // eslint-disable-next-line react/no-array-index-key +
  • + +
  • + ) + })} +
+ + ) : null} +
+ ) +} diff --git a/website/components/io-card-container/style.module.css b/website/components/io-card-container/style.module.css new file mode 100644 index 000000000..b7b9b08d2 --- /dev/null +++ b/website/components/io-card-container/style.module.css @@ -0,0 +1,114 @@ +.cardContainer { + position: relative; + + & + .cardContainer { + margin-top: 64px; + + @media (--medium-up) { + margin-top: 132px; + } + } +} + +.header { + margin: 0 auto 64px; + text-align: center; + max-width: 600px; +} + +.heading { + margin: 0; + composes: g-type-display-2 from global; + + @nest .dark & { + color: var(--white); + } +} + +.description { + margin: 8px 0 0; + composes: g-type-body-large from global; + + @nest .dark & { + color: var(--gray-5); + } +} + +.subHeader { + margin: 0 0 32px; + display: flex; + align-items: center; + justify-content: space-between; + + @nest .dark & { + color: var(--gray-5); + } +} + +.label { + margin: 0; + composes: g-type-display-4 from global; +} + +.cardList { + list-style: none; + + --minCol: 250px; + --columns: var(--length); + + position: relative; + gap: 32px; + padding: 0; + + @media (--small) { + display: flex; + overflow-x: auto; + -ms-overflow-style: none; + scrollbar-width: none; + margin: 0; + padding: 6px 24px; + left: 50%; + margin-left: -50vw; + width: 100vw; + + /* This is to ensure there is overflow padding right on mobile. */ + &::after { + content: ''; + display: block; + width: 1px; + flex-shrink: 0; + } + } + + @media (--medium-up) { + display: grid; + grid-template-columns: repeat(var(--columns), minmax(var(--minCol), 1fr)); + } + + &.threeUp { + @media (--medium-up) { + --columns: 3; + --minCol: 0; + } + } + + &.fourUp { + @media (--medium-up) { + --columns: 3; + --minCol: 0; + } + + @media (--large) { + --columns: 4; + } + } + + & > li { + display: flex; + + @media (--small) { + flex-shrink: 0; + width: 250px; + } + } +} diff --git a/website/components/io-card/index.tsx b/website/components/io-card/index.tsx new file mode 100644 index 000000000..64baa4081 --- /dev/null +++ b/website/components/io-card/index.tsx @@ -0,0 +1,124 @@ +import * as React from 'react' +import Link from 'next/link' +import InlineSvg from '@hashicorp/react-inline-svg' +import classNames from 'classnames' +import { IconArrowRight24 } from '@hashicorp/flight-icons/svg-react/arrow-right-24' +import { IconExternalLink24 } from '@hashicorp/flight-icons/svg-react/external-link-24' +import { productLogos } from './product-logos' +import s from './style.module.css' + +export interface IoCardProps { + variant?: 'light' | 'gray' | 'dark' + products?: Array<{ + name: keyof typeof productLogos + }> + link: { + url: string + type: 'inbound' | 'outbound' + } + inset?: 'none' | 'sm' | 'md' + eyebrow?: string + heading?: string + description?: string + children?: React.ReactNode +} + +function IoCard({ + variant = 'light', + products, + link, + inset = 'md', + eyebrow, + heading, + description, + children, +}: IoCardProps): React.ReactElement { + const LinkWrapper = ({ className, children }) => + link.type === 'inbound' ? ( + + {children} + + ) : ( + + {children} + + ) + + return ( +
+ + {children ? ( + children + ) : ( + <> + {eyebrow ? {eyebrow} : null} + {heading ? {heading} : null} + {description ? {description} : null} + + )} +
+ {products && ( +
    + {products.map(({ name }, index) => { + const key = name.toLowerCase() + const version = variant === 'dark' ? 'neutral' : 'color' + return ( + // eslint-disable-next-line react/no-array-index-key +
  • + +
  • + ) + })} +
+ )} + + {link.type === 'inbound' ? ( + + ) : ( + + )} + +
+
+
+ ) +} + +interface EyebrowProps { + children: string +} + +function Eyebrow({ children }: EyebrowProps) { + return

{children}

+} + +interface HeadingProps { + as?: 'h2' | 'h3' | 'h4' + children: React.ReactNode +} + +function Heading({ as: Component = 'h2', children }: HeadingProps) { + return {children} +} + +interface DescriptionProps { + children: string +} + +function Description({ children }: DescriptionProps) { + return

{children}

+} + +IoCard.Eyebrow = Eyebrow +IoCard.Heading = Heading +IoCard.Description = Description + +export default IoCard diff --git a/website/components/io-card/product-logos.ts b/website/components/io-card/product-logos.ts new file mode 100644 index 000000000..9c24e3bf4 --- /dev/null +++ b/website/components/io-card/product-logos.ts @@ -0,0 +1,34 @@ +export const productLogos = { + boundary: { + color: require('@hashicorp/mktg-logos/product/boundary/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/boundary/logomark/white.svg?include'), + }, + consul: { + color: require('@hashicorp/mktg-logos/product/consul/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/consul/logomark/white.svg?include'), + }, + nomad: { + color: require('@hashicorp/mktg-logos/product/nomad/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/nomad/logomark/white.svg?include'), + }, + packer: { + color: require('@hashicorp/mktg-logos/product/packer/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/packer/logomark/white.svg?include'), + }, + terraform: { + color: require('@hashicorp/mktg-logos/product/terraform/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/terraform/logomark/white.svg?include'), + }, + vagrant: { + color: require('@hashicorp/mktg-logos/product/vagrant/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/vagrant/logomark/white.svg?include'), + }, + vault: { + color: require('@hashicorp/mktg-logos/product/vault/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/vault/logomark/white.svg?include'), + }, + waypoint: { + color: require('@hashicorp/mktg-logos/product/waypoint/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/waypoint/logomark/white.svg?include'), + }, +} diff --git a/website/components/io-card/style.module.css b/website/components/io-card/style.module.css new file mode 100644 index 000000000..44df36ced --- /dev/null +++ b/website/components/io-card/style.module.css @@ -0,0 +1,148 @@ +.card { + /* Radii */ + --token-radius: 6px; + + /* Spacing */ + --token-spacing-03: 8px; + --token-spacing-04: 16px; + --token-spacing-05: 24px; + --token-spacing-06: 32px; + + /* Elevations */ + --token-elevation-mid: 0 2px 3px rgba(101, 106, 118, 0.1), + 0 8px 16px -10px rgba(101, 106, 118, 0.2); + --token-elevation-high: 0 2px 3px rgba(101, 106, 118, 0.15), + 0 16px 16px -10px rgba(101, 106, 118, 0.2); + + /* Transition */ + --token-transition: ease-in-out 0.2s; + + display: flex; + flex-direction: column; + flex-grow: 1; + min-height: 300px; + + & a { + display: flex; + flex-direction: column; + flex-grow: 1; + border-radius: var(--token-radius); + box-shadow: 0 0 0 1px rgba(38, 53, 61, 0.1), var(--token-elevation-mid); + transition: var(--token-transition); + transition-property: background-color, box-shadow; + + &:hover { + box-shadow: 0 0 0 2px rgba(38, 53, 61, 0.15), var(--token-elevation-high); + cursor: pointer; + } + + /* Variants */ + &.dark { + background-color: var(--gray-1); + + &:hover { + background-color: var(--gray-2); + } + } + + &.gray { + background-color: #f9f9fa; + } + + &.light { + background-color: var(--white); + } + + /* Spacing */ + &.none { + padding: 0; + } + + &.sm { + padding: var(--token-spacing-05); + } + + &.md { + padding: var(--token-spacing-06); + } + } +} + +.eyebrow { + margin: 0; + composes: g-type-label-small from global; + color: var(--gray-3); + + @nest .dark & { + color: var(--gray-5); + } +} + +.heading { + margin: 0; + composes: g-type-display-5 from global; + color: var(--black); + + @nest * + & { + margin-top: var(--token-spacing-05); + } + + @nest .dark & { + color: var(--white); + } +} + +.description { + margin: 0; + composes: g-type-body-small from global; + color: var(--gray-3); + + @nest * + & { + margin-top: var(--token-spacing-03); + } + + @nest .dark & { + color: var(--gray-5); + } +} + +.footer { + margin-top: auto; + display: flex; + justify-content: space-between; + align-items: flex-end; + padding-top: 32px; +} + +.products { + display: flex; + gap: 8px; + margin: 0; + padding: 0; + + & > li { + width: 32px; + height: 32px; + display: grid; + place-items: center; + } + + & .logo { + display: flex; + + & svg { + width: 32px; + height: 32px; + } + } +} + +.linkType { + margin-left: auto; + display: flex; + color: var(--black); + + @nest .dark & { + color: var(--white); + } +} diff --git a/website/components/io-dialog/index.tsx b/website/components/io-dialog/index.tsx new file mode 100644 index 000000000..14298b305 --- /dev/null +++ b/website/components/io-dialog/index.tsx @@ -0,0 +1,47 @@ +import * as React from 'react' +import { DialogOverlay, DialogContent, DialogOverlayProps } from '@reach/dialog' +import { AnimatePresence, motion } from 'framer-motion' +import s from './style.module.css' + +export interface IoDialogProps extends DialogOverlayProps { + label: string +} + +export default function IoDialog({ + isOpen, + onDismiss, + children, + label, +}: IoDialogProps): React.ReactElement { + const AnimatedDialogOverlay = motion(DialogOverlay) + return ( + + {isOpen && ( + +
+ + + + {children} + + +
+
+ )} +
+ ) +} diff --git a/website/components/io-dialog/style.module.css b/website/components/io-dialog/style.module.css new file mode 100644 index 000000000..306619ac8 --- /dev/null +++ b/website/components/io-dialog/style.module.css @@ -0,0 +1,62 @@ +.dialogOverlay { + background-color: rgba(0, 0, 0, 0.75); + height: 100%; + left: 0; + overflow-y: auto; + position: fixed; + top: 0; + width: 100%; + z-index: 666666667 /* higher than global nav */; +} + +.dialogWrapper { + display: grid; + min-height: 100vh; + padding: 24px; + place-items: center; +} + +.dialogContent { + background-color: var(--gray-1); + color: var(--white); + max-width: 800px; + outline: none; + overflow-y: auto; + padding: 24px; + position: relative; + width: 100%; + + @media (min-width: 768px) { + padding: 48px; + } +} + +.dialogClose { + appearance: none; + background-color: transparent; + border: 0; + composes: g-type-display-5 from global; + cursor: pointer; + margin: 0; + padding: 0; + position: absolute; + color: var(--white); + right: 24px; + top: 24px; + z-index: 1; + + @media (min-width: 768px) { + right: 48px; + top: 48px; + } + + @nest html[dir='rtl'] & { + left: 24px; + right: auto; + + @media (min-width: 768px) { + left: 48px; + right: auto; + } + } +} diff --git a/website/components/io-home-call-to-action/index.tsx b/website/components/io-home-call-to-action/index.tsx new file mode 100644 index 000000000..7296b361b --- /dev/null +++ b/website/components/io-home-call-to-action/index.tsx @@ -0,0 +1,39 @@ +import ReactCallToAction from '@hashicorp/react-call-to-action' +import { Products } from '@hashicorp/platform-product-meta' +import s from './style.module.css' + +interface IoHomeCallToActionProps { + brand: Products + heading: string + content: string + links: Array<{ + text: string + url: string + }> +} + +export default function IoHomeCallToAction({ + brand, + heading, + content, + links, +}: IoHomeCallToActionProps) { + return ( +
+ { + return { + text, + url, + type: index === 1 ? 'inbound' : null, + } + })} + /> +
+ ) +} diff --git a/website/components/io-home-call-to-action/style.module.css b/website/components/io-home-call-to-action/style.module.css new file mode 100644 index 000000000..76cb03446 --- /dev/null +++ b/website/components/io-home-call-to-action/style.module.css @@ -0,0 +1,12 @@ +.callToAction { + margin: 60px auto; + background-image: linear-gradient(52.3deg, #2c2d2f 39.83%, #626264 96.92%); + + @media (--medium-up) { + margin: 120px auto; + } + + & > * { + background-color: transparent; + } +} diff --git a/website/components/io-home-case-studies/index.tsx b/website/components/io-home-case-studies/index.tsx new file mode 100644 index 000000000..3155749e2 --- /dev/null +++ b/website/components/io-home-case-studies/index.tsx @@ -0,0 +1,81 @@ +import * as React from 'react' +import Image from 'next/image' +import { IconExternalLink16 } from '@hashicorp/flight-icons/svg-react/external-link-16' +import { IconArrowRight16 } from '@hashicorp/flight-icons/svg-react/arrow-right-16' +import s from './style.module.css' + +interface IoHomeCaseStudiesProps { + isInternalLink: (link: string) => boolean + heading: string + description: string + primary: Array<{ + thumbnail: { + url: string + alt: string + } + link: string + heading: string + }> + secondary: Array<{ + link: string + heading: string + }> +} + +export default function IoHomeCaseStudies({ + isInternalLink, + heading, + description, + primary, + secondary, +}: IoHomeCaseStudiesProps): React.ReactElement { + return ( +
+
+
+

{heading}

+

{description}

+
+
+ + + +
+
+
+ ) +} diff --git a/website/components/io-home-case-studies/style.module.css b/website/components/io-home-case-studies/style.module.css new file mode 100644 index 000000000..63ff3102f --- /dev/null +++ b/website/components/io-home-case-studies/style.module.css @@ -0,0 +1,170 @@ +.root { + position: relative; + margin: 60px auto; + max-width: 1600px; + + @media (--medium-up) { + margin: 120px auto; + } +} + +.container { + composes: g-grid-container from global; +} + +.header { + margin-bottom: 32px; + + @media (--medium-up) { + max-width: calc(100% * 5 / 12); + } +} + +.heading { + margin: 0; + composes: g-type-display-3 from global; +} + +.description { + margin: 8px 0 0; + composes: g-type-body from global; + color: var(--gray-3); +} + +.caseStudies { + --columns: 1; + + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + + @media (--medium-up) { + --columns: 12; + } +} + +.primary { + --columns: 1; + + grid-column: 1 / -1; + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + + @media (--medium-up) { + --columns: 2; + } + + @media (--large) { + grid-column: 1 / 9; + } +} + +.primaryItem { + display: flex; +} + +.card { + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: flex-end; + padding: 32px; + box-shadow: 0 8px 16px -10px rgba(101, 106, 118, 0.2); + background-color: #000; + border-radius: 6px; + color: var(--white); + transition: ease-in-out 0.2s; + transition-property: box-shadow; + min-height: 300px; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10; + border-radius: 6px; + background-image: linear-gradient( + to bottom, + rgba(0, 0, 0, 0), + rgba(0, 0, 0, 0.45) + ); + transition: opacity ease-in-out 0.2s; + } + + &:hover { + box-shadow: 0 2px 3px rgba(101, 106, 118, 0.15), + 0 16px 16px -10px rgba(101, 106, 118, 0.2); + + &::before { + opacity: 0.75; + } + } +} + +.cardThumbnail { + transition: transform 0.4s; + + @nest .card:hover & { + transform: scale(1.04); + } +} + +.cardHeading { + margin: 0; + composes: g-type-display-4 from global; + z-index: 10; +} + +.secondary { + grid-column: 1 / -1; + list-style: none; + margin: 0; + padding: 0; + + @media (--large) { + margin-top: -32px; + grid-column: 9 / -1; + } +} + +.secondaryItem { + border-bottom: 1px solid var(--gray-5); +} + +.link { + display: flex; + width: 100%; + color: var(--black); +} + +.linkInner { + display: flex; + width: 100%; + justify-content: space-between; + padding-top: 32px; + padding-bottom: 32px; + transition: transform ease-in-out 0.2s; + + @nest .link:hover & { + transform: translateX(4px); + } + + & svg { + margin-top: 6px; + flex-shrink: 0; + } +} + +.linkHeading { + margin: 0 32px 0 0; + composes: g-type-display-6 from global; +} diff --git a/website/components/io-home-feature/index.tsx b/website/components/io-home-feature/index.tsx new file mode 100644 index 000000000..f3e910fcd --- /dev/null +++ b/website/components/io-home-feature/index.tsx @@ -0,0 +1,80 @@ +import * as React from 'react' +import Image from 'next/image' +import Link from 'next/link' +import { IconArrowRight16 } from '@hashicorp/flight-icons/svg-react/arrow-right-16' +import s from './style.module.css' + +export interface IoHomeFeatureProps { + isInternalLink: (link: string) => boolean + link?: string + image: { + url: string + alt: string + } + heading: string + description: string +} + +export default function IoHomeFeature({ + isInternalLink, + link, + image, + heading, + description, +}: IoHomeFeatureProps): React.ReactElement { + return ( + +
+ {image.alt} +
+
+

{heading}

+

{description}

+ {link ? ( + + Learn more{' '} + + + + + ) : null} +
+
+ ) +} + +interface IoHomeFeatureWrapProps { + isInternalLink: (link: string) => boolean + href: string + children: React.ReactNode +} + +function IoHomeFeatureWrap({ + isInternalLink, + href, + children, +}: IoHomeFeatureWrapProps) { + if (!href) { + return
{children}
+ } + + if (isInternalLink(href)) { + return ( + + {children} + + ) + } + + return ( + + {children} + + ) +} diff --git a/website/components/io-home-feature/style.module.css b/website/components/io-home-feature/style.module.css new file mode 100644 index 000000000..70c2cc510 --- /dev/null +++ b/website/components/io-home-feature/style.module.css @@ -0,0 +1,79 @@ +.feature { + display: flex; + align-items: center; + flex-direction: column; + padding: 32px; + gap: 24px 64px; + border-radius: 6px; + background-color: #f9f9fa; + color: var(--black); + box-shadow: 0 2px 3px rgba(101, 106, 118, 0.1), + 0 8px 16px -10px rgba(101, 106, 118, 0.2); + + @media (--medium-up) { + flex-direction: row; + } +} + +.featureLink { + transition: box-shadow ease-in-out 0.2s; + + &:hover { + box-shadow: 0 2px 3px rgba(101, 106, 118, 0.15), + 0 16px 16px -10px rgba(101, 106, 118, 0.2); + } +} + +.featureMedia { + flex-shrink: 0; + display: flex; + width: 100%; + border-radius: 6px; + overflow: hidden; + border: 1px solid var(--gray-5); + + @media (--medium-up) { + width: 300px; + } + + @media (--large) { + width: 400px; + } + + & > * { + width: 100%; + } +} + +.featureContent { + max-width: 520px; +} + +.featureHeading { + margin: 0; + composes: g-type-display-4 from global; +} + +.featureDescription { + margin: 8px 0 24px; + composes: g-type-body-small from global; + color: var(--gray-3); +} + +.featureCta { + display: inline-flex; + align-items: center; + + & > span { + display: flex; + margin-left: 12px; + + & > svg { + transition: transform 0.2s; + } + } + + @nest .feature:hover & span svg { + transform: translateX(2px); + } +} diff --git a/website/components/io-home-hero/index.tsx b/website/components/io-home-hero/index.tsx new file mode 100644 index 000000000..fabaafd37 --- /dev/null +++ b/website/components/io-home-hero/index.tsx @@ -0,0 +1,135 @@ +import * as React from 'react' +import { Products } from '@hashicorp/platform-product-meta' +import Button from '@hashicorp/react-button' +import classNames from 'classnames' +import s from './style.module.css' + +interface IoHomeHeroProps { + pattern: string + brand: Products | 'neutral' + heading: string + description: string + ctas: Array<{ + title: string + link: string + }> + cards: Array +} + +export default function IoHomeHero({ + pattern, + brand, + heading, + description, + ctas, + cards, +}: IoHomeHeroProps) { + const [loaded, setLoaded] = React.useState(false) + + React.useEffect(() => { + setTimeout(() => { + setLoaded(true) + }, 250) + }, []) + + return ( +
+ +
+
+

{heading}

+

{description}

+ {ctas && ( +
+ {ctas.map((cta, index) => { + return ( +
+ )} +
+ {cards && ( +
+ {cards.map((card, index) => { + return ( + + ) + })} +
+ )} +
+
+ ) +} + +interface IoHomeHeroCardProps { + index?: number + heading: string + description: string + cta: { + title: string + link: string + brand?: 'neutral' | Products + } + subText: string +} + +function IoHomeHeroCard({ + index, + heading, + description, + cta, + subText, +}: IoHomeHeroCardProps): React.ReactElement { + return ( +
+

{heading}

+

{description}

+
+ ) +} diff --git a/website/components/io-home-hero/style.module.css b/website/components/io-home-hero/style.module.css new file mode 100644 index 000000000..c7f47026f --- /dev/null +++ b/website/components/io-home-hero/style.module.css @@ -0,0 +1,148 @@ +.hero { + position: relative; + padding-top: 64px; + padding-bottom: 64px; + background: linear-gradient(180deg, #f9f9fa 0%, #fff 28.22%, #fff 100%); + + @media (--medium-up) { + padding-top: 128px; + padding-bottom: 128px; + } +} + +.pattern { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + max-width: 1600px; + width: 100%; + margin: auto; + + @media (--medium-up) { + background-image: var(--pattern); + background-repeat: no-repeat; + background-position: top right; + } +} + +.container { + --columns: 1; + + composes: g-grid-container from global; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 48px 32px; + + @media (--medium-up) { + --columns: 12; + } +} + +.content { + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 1 / 6; + } + + & > * { + max-width: 415px; + } +} + +.heading { + margin: 0; + composes: g-type-display-1 from global; +} + +.description { + margin: 8px 0 0; + composes: g-type-body-small from global; + color: var(--gray-3); +} + +.ctas { + margin-top: 24px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; +} + +.cards { + --columns: 1; + + grid-column: 1 / -1; + align-self: start; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + + @media (min-width: 600px) { + --columns: 2; + } + + @media (--medium-up) { + --columns: 1; + + grid-column: 7 / -1; + } + + @media (--large) { + --columns: 2; + + grid-column: 6 / -1; + } +} + +.card { + --token-radius: 6px; + --token-elevation-mid: 0 2px 3px rgba(101, 106, 118, 0.1), + 0 8px 16px -10px rgba(101, 106, 118, 0.2); + + opacity: 0; + padding: 40px 32px; + display: flex; + align-items: flex-start; + flex-direction: column; + flex-grow: 1; + background-color: var(--white); + border-radius: var(--token-radius); + box-shadow: 0 0 0 1px rgba(38, 53, 61, 0.1), var(--token-elevation-mid); + + @nest .loaded & { + animation-name: slideIn; + animation-duration: 0.5s; + animation-delay: calc(var(--index) * 0.1s); + animation-fill-mode: forwards; + } +} + +.cardHeading { + margin: 0; + composes: g-type-display-4 from global; +} + +.cardDescription { + margin: 8px 0 16px; + composes: g-type-display-6 from global; +} + +.cardSubText { + margin: 32px 0 0; + composes: g-type-body-small from global; + color: var(--gray-3); +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(50px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/website/components/io-home-in-practice/index.tsx b/website/components/io-home-in-practice/index.tsx new file mode 100644 index 000000000..6e145b2e9 --- /dev/null +++ b/website/components/io-home-in-practice/index.tsx @@ -0,0 +1,86 @@ +import * as React from 'react' +import Image from 'next/image' +import Button from '@hashicorp/react-button' +import { Products } from '@hashicorp/platform-product-meta' +import { IoCardProps } from 'components/io-card' +import IoCardContainer from 'components/io-card-container' +import s from './style.module.css' + +interface IoHomeInPracticeProps { + brand: Products + pattern: string + heading: string + description: string + cards: Array + cta: { + heading: string + description: string + link: string + image: { + url: string + alt: string + width: number + height: number + } + } +} + +export default function IoHomeInPractice({ + brand, + pattern, + heading, + description, + cards, + cta, +}: IoHomeInPracticeProps) { + return ( +
+
+ + + {cta.heading ? ( +
+
+

{cta.heading}

+ {cta.description ? ( +

{cta.description}

+ ) : null} + {cta.link ? ( +
+ {cta.image?.url ? ( +
+ {cta.image.alt} +
+ ) : null} +
+ ) : null} +
+
+ ) +} diff --git a/website/components/io-home-in-practice/style.module.css b/website/components/io-home-in-practice/style.module.css new file mode 100644 index 000000000..13ed2bfd9 --- /dev/null +++ b/website/components/io-home-in-practice/style.module.css @@ -0,0 +1,98 @@ +.inPractice { + position: relative; + margin: 60px auto; + padding: 64px 0; + max-width: 1600px; + + @media (--medium-up) { + padding: 80px 0; + margin: 120px auto; + } + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--black); + background-image: var(--pattern); + background-repeat: no-repeat; + background-size: 50%; + background-position: top 200px left; + + @media (--large) { + border-radius: 6px; + left: 24px; + right: 24px; + background-size: 35%; + background-position: top 64px left; + } + } +} + +.container { + composes: g-grid-container from global; +} + +.inPracticeCta { + --columns: 1; + + position: relative; + margin-top: 64px; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 64px 32px; + + @media (--medium-up) { + --columns: 12; + } + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + bottom: -64px; + background-image: radial-gradient( + 42.33% 42.33% at 50% 100%, + #363638 0%, + #000 100% + ); + + @media (--medium-up) { + bottom: -80px; + } + } +} + +.inPracticeCtaContent { + position: relative; + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 1 / 5; + } +} + +.inPracticeCtaMedia { + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 6 / -1; + } +} + +.inPracticeCtaHeading { + margin: 0; + color: var(--white); + composes: g-type-display-3 from global; +} + +.inPracticeCtaDescription { + margin: 8px 0 32px; + color: var(--gray-5); + composes: g-type-body from global; +} diff --git a/website/components/io-home-intro/index.tsx b/website/components/io-home-intro/index.tsx new file mode 100644 index 000000000..c8081b4f7 --- /dev/null +++ b/website/components/io-home-intro/index.tsx @@ -0,0 +1,155 @@ +import * as React from 'react' +import Image from 'next/image' +import classNames from 'classnames' +import { Products } from '@hashicorp/platform-product-meta' +import Button from '@hashicorp/react-button' +import IoVideoCallout, { + IoHomeVideoCalloutProps, +} from 'components/io-video-callout' +import IoHomeFeature, { IoHomeFeatureProps } from 'components/io-home-feature' +import s from './style.module.css' + +interface IoHomeIntroProps { + isInternalLink: (link: string) => boolean + brand: Products + heading: string + description: string + features?: Array + offerings?: { + image: { + src: string + width: number + height: number + alt: string + } + list: Array<{ + heading: string + description: string + }> + cta?: { + title: string + link: string + } + } + video?: IoHomeVideoCalloutProps +} + +export default function IoHomeIntro({ + isInternalLink, + brand, + heading, + description, + features, + offerings, + video, +}: IoHomeIntroProps) { + return ( +
+
+
+
+

{heading}

+

{description}

+
+
+
+ + {features ? ( +
    + {features.map((feature, index) => { + return ( + // Index is stable + // eslint-disable-next-line react/no-array-index-key +
  • +
    + +
    +
  • + ) + })} +
+ ) : null} + + {offerings ? ( +
+ {offerings.image ? ( +
+ {offerings.image.alt} +
+ ) : null} +
+
    + {offerings.list.map((offering, index) => { + return ( + // Index is stable + // eslint-disable-next-line react/no-array-index-key +
  • +

    + {offering.heading} +

    +

    + {offering.description} +

    +
  • + ) + })} +
+ {offerings.cta ? ( +
+
+ ) : null} +
+
+ ) : null} + + {video ? ( +
+ +
+ ) : null} +
+ ) +} diff --git a/website/components/io-home-intro/style.module.css b/website/components/io-home-intro/style.module.css new file mode 100644 index 000000000..6227a49ba --- /dev/null +++ b/website/components/io-home-intro/style.module.css @@ -0,0 +1,169 @@ +.root { + position: relative; + margin-bottom: 60px; + + @media (--medium-up) { + margin-bottom: 120px; + } + + &.withOfferings:not(.withFeatures)::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: radial-gradient( + 93.55% 93.55% at 50% 0%, + var(--gray-6) 0%, + rgba(242, 242, 243, 0) 100% + ); + + @media (--large) { + border-radius: 6px; + left: 24px; + right: 24px; + } + } +} + +.container { + composes: g-grid-container from global; +} + +.header { + padding-top: 64px; + padding-bottom: 64px; + text-align: center; + + @nest .withFeatures & { + background-color: var(--brand); + } + + @nest .withFeatures.consul & { + color: var(--white); + } +} + +.headerInner { + margin: auto; + + @media (--medium-up) { + max-width: calc(100% * 7 / 12); + } +} + +.heading { + margin: 0; + composes: g-type-display-2 from global; +} + +.description { + margin: 24px 0 0; + composes: g-type-body-large from global; + + @nest .withOfferings:not(.withFeatures) & { + color: var(--gray-3); + } +} + +/* + * Features + */ + +.features { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 32px; + + & li:first-of-type { + background-image: linear-gradient( + to bottom, + var(--brand) 50%, + var(--white) 50% + ); + } +} + +/* + * Offerings + */ + +.offerings { + --columns: 1; + + composes: g-grid-container from global; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 64px 32px; + + @media (--medium-up) { + --columns: 12; + } + + @nest .features + & { + margin-top: 60px; + + @media (--medium-up) { + margin-top: 120px; + } + } +} + +.offeringsMedia { + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 1 / 6; + } +} + +.offeringsContent { + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 7 / -1; + } +} + +.offeringsList { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 32px; + + @media (--small) { + grid-template-columns: repeat(1, 1fr); + } +} + +.offeringsListHeading { + margin: 0; + composes: g-type-display-4 from global; +} + +.offeringsListDescription { + margin: 16px 0 0; + composes: g-type-body-small from global; +} + +.offeringsCta { + margin-top: 48px; +} + +/* + * Video + */ + +.video { + margin-top: 60px; + composes: g-grid-container from global; + + @media (--medium-up) { + margin-top: 120px; + } +} diff --git a/website/components/io-home-pre-footer/index.tsx b/website/components/io-home-pre-footer/index.tsx new file mode 100644 index 000000000..98127443d --- /dev/null +++ b/website/components/io-home-pre-footer/index.tsx @@ -0,0 +1,79 @@ +import * as React from 'react' +import classNames from 'classnames' +import { Products } from '@hashicorp/platform-product-meta' +import { IconArrowRight16 } from '@hashicorp/flight-icons/svg-react/arrow-right-16' +import s from './style.module.css' + +interface IoHomePreFooterProps { + brand: Products + heading: string + description: string + ctas: [IoHomePreFooterCard, IoHomePreFooterCard, IoHomePreFooterCard] +} + +export default function IoHomePreFooter({ + brand, + heading, + description, + ctas, +}: IoHomePreFooterProps) { + return ( +
+
+
+

{heading}

+

{description}

+
+
+ {ctas.map((cta, index) => { + return ( + + ) + })} +
+
+
+ ) +} + +interface IoHomePreFooterCard { + brand?: string + link: string + heading: string + description: string + cta: string +} + +function IoHomePreFooterCard({ + brand, + link, + heading, + description, + cta, +}: IoHomePreFooterCard): React.ReactElement { + return ( + +

{heading}

+

{description}

+ + {cta} + +
+ ) +} diff --git a/website/components/io-home-pre-footer/style.module.css b/website/components/io-home-pre-footer/style.module.css new file mode 100644 index 000000000..1273e2087 --- /dev/null +++ b/website/components/io-home-pre-footer/style.module.css @@ -0,0 +1,122 @@ +.preFooter { + margin: 60px auto; +} + +.container { + --columns: 1; + + composes: g-grid-container from global; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + + @media (--medium-up) { + --columns: 12; + } +} + +.content { + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 1 / 6; + } + + @media (--large) { + grid-column: 1 / 4; + } +} + +.heading { + margin: 0; + composes: g-type-display-1 from global; +} + +.description { + margin: 24px 0 0; + composes: g-type-body from global; + color: var(--gray-3); +} + +.cards { + grid-column: 1 / -1; + + --columns: 1; + + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + + @media (--medium-up) { + --columns: 3; + + grid-column: 1 / -1; + } + + @media (--large) { + grid-column: 5 / -1; + } +} + +.card { + display: flex; + flex-direction: column; + flex-grow: 1; + padding: 32px 24px; + background-color: var(--primary); + color: var(--black); + border-radius: 6px; + box-shadow: 0 2px 3px rgba(101, 106, 118, 0.1), + 0 8px 16px -10px rgba(101, 106, 118, 0.2); + transition: ease-in-out 0.2s; + transition-property: box-shadow; + + &:hover { + box-shadow: 0 2px 3px rgba(101, 106, 118, 0.15), + 0 16px 16px -10px rgba(101, 106, 118, 0.2); + } + + &:nth-of-type(1) { + color: var(--white); + + @nest .vault & { + color: var(--black); + } + } + + &:nth-of-type(2) { + background-color: var(--secondary); + } + + &:nth-of-type(3) { + background-color: var(--gray-6); + } +} + +.cardHeading { + margin: 0; + composes: g-type-display-4 from global; +} + +.cardDescription { + margin: 8px 0 0; + padding-bottom: 48px; + color: inherit; + composes: g-type-display-6 from global; +} + +.cardCta { + margin-top: auto; + display: inline-flex; + align-items: center; + composes: g-type-buttons-and-standalone-links from global; + + & svg { + margin-left: 12px; + transition: transform 0.2s; + } + + @nest .card:hover & svg { + transform: translate(2px); + } +} diff --git a/website/components/io-usecase-call-to-action/index.tsx b/website/components/io-usecase-call-to-action/index.tsx new file mode 100644 index 000000000..252be27f1 --- /dev/null +++ b/website/components/io-usecase-call-to-action/index.tsx @@ -0,0 +1,69 @@ +import Image from 'next/image' +import * as React from 'react' +import classNames from 'classnames' +import Button from '@hashicorp/react-button' +import s from './style.module.css' + +interface IoUsecaseCallToActionProps { + brand: string + theme?: 'light' | 'dark' + heading: string + description: string + links: Array<{ + text: string + url: string + }> + pattern: string +} + +export default function IoUsecaseCallToAction({ + brand, + theme, + heading, + description, + links, + pattern, +}: IoUsecaseCallToActionProps): React.ReactElement { + return ( +
+

{heading}

+
+

{description}

+
+ {links.map((link, index) => { + return ( +
+
+
+ +
+
+ ) +} diff --git a/website/components/io-usecase-call-to-action/style.module.css b/website/components/io-usecase-call-to-action/style.module.css new file mode 100644 index 000000000..1afcb903d --- /dev/null +++ b/website/components/io-usecase-call-to-action/style.module.css @@ -0,0 +1,66 @@ +.callToAction { + --columns: 1; + + position: relative; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 0 32px; + padding: 32px; + background-color: var(--background-color); + border-radius: 6px; + + &.light { + color: var(--black); + } + + &.dark { + color: var(--white); + } + + @media (--medium-up) { + --columns: 12; + + padding: 0; + } +} + +.heading { + grid-column: 1 / -1; + margin: 0 0 16px; + composes: g-type-display-3 from global; + + @media (--medium-up) { + grid-column: 1 / 6; + padding: 88px 32px 88px 64px; + } +} + +.content { + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 6 / 11; + padding: 88px 0; + } +} + +.description { + margin: 0 0 32px; + composes: g-type-body-large from global; +} + +.links { + display: flex; + flex-wrap: wrap; + gap: 16px 32px; +} + +.pattern { + position: relative; + display: none; + + @media (--medium-up) { + grid-column: 11 / -1; + display: flex; + } +} diff --git a/website/components/io-usecase-customer/index.tsx b/website/components/io-usecase-customer/index.tsx new file mode 100644 index 000000000..288b953b8 --- /dev/null +++ b/website/components/io-usecase-customer/index.tsx @@ -0,0 +1,86 @@ +import * as React from 'react' +import Image from 'next/image' +import Button from '@hashicorp/react-button' +import s from './style.module.css' + +interface IoUsecaseCustomerProps { + media: { + src: string + width: string + height: string + alt: string + } + logo: { + src: string + width: string + height: string + alt: string + } + heading: string + description: string + stats?: Array<{ + value: string + key: string + }> + link: string +} + +export default function IoUsecaseCustomer({ + media, + logo, + heading, + description, + stats, + link, +}: IoUsecaseCustomerProps): React.ReactElement { + return ( +
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/alt-text */} + +
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/alt-text */} + +
+ Customer case study +
+

{heading}

+

{description}

+ {link ? ( +
+
+ ) : null} +
+
+ {stats.length > 0 ? ( +
    + {stats.map(({ key, value }, index) => { + return ( + // Index is stable + // eslint-disable-next-line react/no-array-index-key +
  • +

    {value}

    +

    {key}

    +
  • + ) + })} +
+ ) : null} +
+
+ ) +} diff --git a/website/components/io-usecase-customer/style.module.css b/website/components/io-usecase-customer/style.module.css new file mode 100644 index 000000000..b88156073 --- /dev/null +++ b/website/components/io-usecase-customer/style.module.css @@ -0,0 +1,119 @@ +.customer { + position: relative; + background-color: var(--black); + color: var(--white); + padding-bottom: 64px; + + @media (--medium-up) { + padding-bottom: 132px; + } +} + +.container { + composes: g-grid-container from global; +} + +.columns { + --columns: 1; + + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 64px 32px; + + @media (--medium-up) { + --columns: 12; + } +} + +.media { + margin-top: -64px; + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 1 / 7; + } +} + +.content { + grid-column: 1 / -1; + + @media (--medium-up) { + padding-top: 64px; + grid-column: 8 / -1; + } +} + +.eyebrow { + display: flex; + align-items: center; +} + +.eyebrowLogo { + display: flex; + max-width: 120px; +} + +.eyebrowLabel { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 12px; + margin-left: 12px; + border-left: 1px solid var(--gray-5); + align-self: center; + composes: g-type-label-small-strong from global; +} + +.heading { + margin: 32px 0 24px; + composes: g-type-display-2 from global; +} + +.description { + margin: 0; + composes: g-type-body from global; +} + +.cta { + margin-top: 32px; +} + +.stats { + --columns: 1; + + list-style: none; + margin: 64px 0 0; + padding: 0; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + + @media (--medium-up) { + --columns: 12; + + margin-top: 132px; + } + + & > li { + border-top: 1px solid var(--gray-2); + grid-column: span 4; + } +} + +.value { + margin: 0; + padding-top: 32px; + font-family: var(--font-display); + font-size: 50px; + font-weight: 700; + line-height: 1; + + @media (--large) { + font-size: 80px; + } +} + +.key { + margin: 12px 0 0; + composes: g-type-display-4 from global; + color: var(--gray-3); +} diff --git a/website/components/io-usecase-hero/index.tsx b/website/components/io-usecase-hero/index.tsx new file mode 100644 index 000000000..4838678e8 --- /dev/null +++ b/website/components/io-usecase-hero/index.tsx @@ -0,0 +1,41 @@ +import * as React from 'react' +import Image from 'next/image' +import s from './style.module.css' + +interface IoUsecaseHeroProps { + eyebrow: string + heading: string + description: string + pattern?: string +} + +export default function IoUsecaseHero({ + eyebrow, + heading, + description, + pattern, +}: IoUsecaseHeroProps): React.ReactElement { + return ( +
+
+
+ {pattern ? ( + + ) : null} +
+
+

{eyebrow}

+

{heading}

+

{description}

+
+
+
+ ) +} diff --git a/website/components/io-usecase-hero/pattern.svg b/website/components/io-usecase-hero/pattern.svg new file mode 100644 index 000000000..f4b1ef3af --- /dev/null +++ b/website/components/io-usecase-hero/pattern.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/components/io-usecase-hero/style.module.css b/website/components/io-usecase-hero/style.module.css new file mode 100644 index 000000000..5fd729c8e --- /dev/null +++ b/website/components/io-usecase-hero/style.module.css @@ -0,0 +1,83 @@ +.hero { + position: relative; + max-width: 1600px; + margin-right: auto; + margin-left: auto; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: radial-gradient( + 95.97% 95.97% at 50% 100%, + #f2f2f3 0%, + rgba(242, 242, 243, 0) 100% + ); + + @media (--medium-up) { + border-radius: 6px; + left: 24px; + right: 24px; + } + } +} + +.container { + @media (--medium-up) { + display: grid; + grid-template-columns: 1fr max-content 1fr; + gap: 32px; + } +} + +.pattern { + margin-left: 24px; + transform: translateY(24px); + position: relative; + display: flex; + flex-direction: column; + justify-content: flex-end; + + @media (--small) { + display: none; + } + + @media (--medium) { + & > * { + display: none !important; + } + } +} + +.content { + position: relative; + max-width: 520px; + width: 100%; + margin-right: auto; + margin-left: auto; + padding: 64px 24px; + + @media (--medium-up) { + padding-top: 132px; + padding-bottom: 132px; + } +} + +.eyebrow { + margin: 0; + composes: g-type-label-strong from global; +} + +.heading { + margin: 24px 0; + composes: g-type-display-1 from global; +} + +.description { + margin: 0; + composes: g-type-body-large from global; + color: var(--gray-2); +} diff --git a/website/components/io-usecase-section/index.tsx b/website/components/io-usecase-section/index.tsx new file mode 100644 index 000000000..11ed7917f --- /dev/null +++ b/website/components/io-usecase-section/index.tsx @@ -0,0 +1,81 @@ +import * as React from 'react' +import { Products } from '@hashicorp/platform-product-meta' +import classNames from 'classnames' +import Image from 'next/image' +import Button from '@hashicorp/react-button' +import s from './style.module.css' + +interface IoUsecaseSectionProps { + brand?: Products | 'neutral' + bottomIsFlush?: boolean + eyebrow: string + heading: string + description: string + media?: { + src: string + width: string + height: string + alt: string + } + cta?: { + text: string + link: string + } +} + +export default function IoUsecaseSection({ + brand = 'neutral', + bottomIsFlush = false, + eyebrow, + heading, + description, + media, + cta, +}: IoUsecaseSectionProps): React.ReactElement { + return ( +
+
+

{eyebrow}

+
+
+

{heading}

+ {media?.src ? ( +
+ ) : null} + {cta?.link && cta?.text ? ( +
+
+ ) : null} +
+
+ {media?.src ? ( + // eslint-disable-next-line jsx-a11y/alt-text + + ) : ( +
+ )} +
+
+
+
+ ) +} diff --git a/website/components/io-usecase-section/style.module.css b/website/components/io-usecase-section/style.module.css new file mode 100644 index 000000000..a2b56d1f5 --- /dev/null +++ b/website/components/io-usecase-section/style.module.css @@ -0,0 +1,106 @@ +.section { + position: relative; + max-width: 1600px; + margin-right: auto; + margin-left: auto; + padding-top: 64px; + padding-bottom: 64px; + + @media (--medium-up) { + padding-top: 132px; + padding-bottom: 132px; + } + + & + .section { + padding-bottom: 132px; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--gray-6); + opacity: 0.4; + + @media (--medium-up) { + border-radius: 6px; + left: 24px; + right: 24px; + } + } + } + + &.isFlush { + padding-bottom: 96px; + + @media (--medium-up) { + padding-bottom: 164px; + } + + &::before { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + } +} + +.container { + composes: g-grid-container from global; +} + +.columns { + --columns: 1; + + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + + @media (--medium-up) { + --columns: 12; + } +} + +.column { + &:nth-child(1) { + @media (--medium-up) { + grid-column: 1 / 7; + } + } + + &:nth-child(2) { + @media (--medium-up) { + grid-column: 8 / -1; + padding-top: 16px; + } + } +} + +.eyebrow { + margin: 0; + composes: g-type-display-5 from global; +} + +.heading { + margin: 16px 0 32px; + padding-bottom: 32px; + composes: g-type-display-3 from global; + border-bottom: 1px solid var(--black); +} + +.description { + composes: g-type-body from global; + + & > p { + margin: 0; + + & + p { + margin-top: 16px; + } + } +} + +.cta { + margin-top: 32px; +} diff --git a/website/components/io-video-callout/index.tsx b/website/components/io-video-callout/index.tsx new file mode 100644 index 000000000..7889348d8 --- /dev/null +++ b/website/components/io-video-callout/index.tsx @@ -0,0 +1,80 @@ +import * as React from 'react' +import Image from 'next/image' +import ReactPlayer from 'react-player' +import VisuallyHidden from '@reach/visually-hidden' +import IoDialog from 'components/io-dialog' +import PlayIcon from './play-icon' +import s from './style.module.css' + +export interface IoHomeVideoCalloutProps { + youtubeId: string + thumbnail: string + heading: string + description: string + person: { + avatar: string + name: string + description: string + } +} + +export default function IoVideoCallout({ + youtubeId, + thumbnail, + heading, + description, + person, +}: IoHomeVideoCalloutProps): React.ReactElement { + const [showDialog, setShowDialog] = React.useState(false) + const showVideo = () => setShowDialog(true) + const hideVideo = () => setShowDialog(false) + return ( + <> +
+ +
+

{heading}

+

{description}

+ {person && ( +
+ {person.avatar ? ( +
+ {`${person.name} +
+ ) : null} +
+

{person.name}

+

{person.description}

+
+
+ )} +
+
+ +

{heading}

+
+ +
+
+ + ) +} diff --git a/website/components/io-video-callout/play-icon.tsx b/website/components/io-video-callout/play-icon.tsx new file mode 100644 index 000000000..37395ba2b --- /dev/null +++ b/website/components/io-video-callout/play-icon.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' + +export default function PlayIcon(): React.ReactElement { + return ( + + + + + ) +} diff --git a/website/components/io-video-callout/style.module.css b/website/components/io-video-callout/style.module.css new file mode 100644 index 000000000..815601ff0 --- /dev/null +++ b/website/components/io-video-callout/style.module.css @@ -0,0 +1,128 @@ +.videoCallout { + --columns: 1; + + margin: 0; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + background-color: var(--black); + border-radius: 6px; + overflow: hidden; + + @media (--medium-up) { + --columns: 12; + } +} + +.thumbnail { + position: relative; + display: grid; + place-items: center; + grid-column: 1 / -1; + background-color: transparent; + border: 0; + cursor: pointer; + padding: 96px 32px; + min-height: 300px; + + @media (--medium-up) { + grid-column: 1 / 7; + } + + @media (--large) { + grid-column: 1 / 9; + } + + & > svg { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1; + + @media (--small) { + width: 52px; + height: 52px; + } + } + + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #000; + opacity: 0.45; + transition: opacity ease-in-out 0.2s; + } + + &:hover::after { + opacity: 0.2; + } +} + +.content { + padding: 32px; + grid-column: 1 / -1; + + @media (--medium-up) { + padding: 80px 32px; + grid-column: 7 / -1; + } + + @media (--large) { + grid-column: 9 / -1; + } +} + +.heading { + margin: 0; + composes: g-type-display-4 from global; + color: var(--white); +} + +.description { + margin: 8px 0 0; + composes: g-type-body-small from global; + color: var(--white); +} + +.person { + margin-top: 64px; + display: flex; + align-items: center; + gap: 16px; +} + +.personThumbnail { + display: flex; + border-radius: 9999px; + overflow: hidden; +} + +.personName { + margin: 0; + composes: g-type-body-strong from global; + color: var(--white); +} + +.personDescription { + margin: 4px 0 0; + composes: g-type-label-strong from global; + color: var(--gray-3); +} + +.videoHeading { + margin-top: 0; + margin-bottom: 32px; + padding-right: 100px; + composes: g-type-display-4 from global; +} + +.video { + position: relative; + background-color: var(--gray-2); + aspect-ratio: 16 / 9; +} diff --git a/website/components/subnav/index.jsx b/website/components/subnav/index.jsx index 219545e33..4a70b89c1 100644 --- a/website/components/subnav/index.jsx +++ b/website/components/subnav/index.jsx @@ -1,14 +1,15 @@ import Subnav from '@hashicorp/react-subnav' -import subnavItems from '../../data/subnav' import { useRouter } from 'next/router' +import s from './style.module.css' -export default function ConsulSubnav() { +export default function ConsulSubnav({ menuItems }) { const router = useRouter() return ( diff --git a/website/components/subnav/style.module.css b/website/components/subnav/style.module.css new file mode 100644 index 000000000..5cb3cbccd --- /dev/null +++ b/website/components/subnav/style.module.css @@ -0,0 +1,3 @@ +.subnav { + border-top: 1px solid transparent; +} diff --git a/website/content/api-docs/agent/index.mdx b/website/content/api-docs/agent/index.mdx index 8062593f9..5bcd4f2da 100644 --- a/website/content/api-docs/agent/index.mdx +++ b/website/content/api-docs/agent/index.mdx @@ -679,8 +679,8 @@ $ curl \ ## Force Leave and Shutdown -This endpoint instructs the agent to force a node into the `left` state. If a -node fails unexpectedly, then it will be in a `failed` state. Once in the +This endpoint instructs the agent to force a node into the `left` state in the +LAN and WAN gossip pools. If a node fails unexpectedly, then it will be in a `failed` state. Once in the `failed` state, Consul will attempt to reconnect, and the services and checks belonging to that node will not be cleaned up. Forcing a node into the `left` state allows its old entries to be removed. @@ -710,6 +710,14 @@ The table below shows this endpoint's support for - `node` `(string: )` - Specifies the name of the node to be forced into `left` state. This is specified as part of the URL. +- `prune` `(bool: false)` - Specifies whether to forcibly remove the node from the list of members. + Pruning a node in the `left` or `failed` state removes it from the list altogether. + This is specified as part of the URL as a query parameter. Added in Consul 1.6.2. + +- `wan` `(bool: false)` - Specifies the node should only be removed from the WAN + gossip pool. This is specified as part of the URL as a query parameter. Added + in Consul 1.11.0. + ### Sample Request ```shell-session diff --git a/website/content/api-docs/agent/service.mdx b/website/content/api-docs/agent/service.mdx index 5ecc0d00b..b475b36e4 100644 --- a/website/content/api-docs/agent/service.mdx +++ b/website/content/api-docs/agent/service.mdx @@ -769,7 +769,7 @@ The table below shows this endpoint's support for ### Parameters - `service_id` `(string: )` - Specifies the ID of the service to - deregister. This is specifi### Parameters + deregister. This is specified as part of the URL. - `ns` `(string: "")` - Specifies the namespace in which to deregister the service. This value can be specified as the `ns` URL query diff --git a/website/content/commands/force-leave.mdx b/website/content/commands/force-leave.mdx index 80b31f458..cbfb21082 100644 --- a/website/content/commands/force-leave.mdx +++ b/website/content/commands/force-leave.mdx @@ -61,4 +61,7 @@ consul force-leave server1.us-east1 #### Command Options - `-prune` - Removes failed or left agent from the list of - members entirely + members entirely. Added in Consul 1.6.2. + +- `-wan` - Exclusively leave the agent from the WAN gossip pool. Added in Consul + 1.11.0. diff --git a/website/content/docs/connect/gateways/index.mdx b/website/content/docs/connect/gateways/index.mdx index 31fdb4f10..dafdcb092 100644 --- a/website/content/docs/connect/gateways/index.mdx +++ b/website/content/docs/connect/gateways/index.mdx @@ -7,31 +7,29 @@ description: >- # Gateways -Gateways provide connectivity into, out of, and between Consul service meshes. +This topic provides an overview of the gateway features shipped with Consul. Gateways provide connectivity into, out of, and between Consul service meshes. You can configure the following types of gateways: -- Enable service-to-service traffic between Consul datacenters with [mesh gateways](#mesh-gateways). -- Accept traffic from outside the Consul service mesh to services in the mesh with [ingress gateways](#ingress-gateways). -- Route traffic from services in the Consul service mesh to external services with [terminating gateways](#terminating-gateways). +- [Mesh gateways](#mesh-gateways) enable service-to-service traffic between Consul datacenters or between Consul admin partitions. They also enable datacenters to be federated across wide area networks. +- [Ingress gateways](#ingress-gateways) enable services to accept traffic from outside the Consul service mesh. +- [Terminating gateways](#terminating-gateways) enable you to route traffic from services in the Consul service mesh to external services. ## Mesh Gateways -> **1.6.0+:** This feature is available in Consul versions 1.6.0 and newer. -Mesh gateways enable routing of service mesh traffic between different Consul datacenters. Those datacenters can reside +Mesh gateways enable service mesh traffic to be routed between different Consul datacenters and admin partitions. The datacenters or partitions can reside in different clouds or runtime environments where general interconnectivity between all services in all datacenters -isn't feasible. One scenario where this is useful is when connecting networks with overlapping IP address space. +isn't feasible. -These gateways operate by sniffing the SNI header out of the mTLS connection and then routing the connection to the -appropriate destination based on the server name requested. The data within the mTLS session is not decrypted by -the Gateway. +They operate by sniffing and extracting the server name indication (SNI) header from the service mesh session and routing the connection to the appropriate destination based on the server name requested. The gateway does not decrypt the data within the mTLS session. -As of Consul 1.8.0, mesh gateways can also forward gossip and RPC traffic between Consul servers. -This is enabled by [WAN federation via mesh gateways](/docs/connect/gateways/wan-federation-via-mesh-gateways). +Mesh gateways enable the following scenarios: -For more information about mesh gateways, review the [complete documentation](/docs/connect/gateways/mesh-gateway) -and the [mesh gateway tutorial](https://learn.hashicorp.com/tutorials/consul/service-mesh-gateways). +* **Federate multiple datacenters across a WAN**. Since Consul 1.8.0, mesh gateways can forward gossip and RPC traffic between Consul servers. See [WAN federation via mesh gateways](/docs/connect/gateways/wan-federation-via-mesh-gateways) for additional information. +* **Service-to-service communication across datacenters**. Refer to [Enabling Service-to-service Traffic Accross Datacenters](/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters) for additional information. +* **Service-to-service communication across admin partitions**. Since Consul 1.11.0, you can create administrative boundaries for single Consul deployements called "admin partitions". You can use mesh gateways to facilitate cross-partition communication. Refer to [Enabling Service-to-service Traffic Accross Admin Partitions](/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions) for additional information. -![Mesh Gateway Architecture](/img/mesh-gateways.png) +-> **Mesh gateway tutorial**: Follow the [mesh gateway tutorial](https://learn.hashicorp.com/tutorials/consul/service-mesh-gateways) to learn concepts associated with mesh gateways. ## Ingress Gateways diff --git a/website/content/docs/connect/gateways/ingress-gateway.mdx b/website/content/docs/connect/gateways/ingress-gateway.mdx index 50d654233..72c98125e 100644 --- a/website/content/docs/connect/gateways/ingress-gateway.mdx +++ b/website/content/docs/connect/gateways/ingress-gateway.mdx @@ -1,10 +1,9 @@ --- layout: docs -page_title: External <> Internal Services - Ingress Gateways +page_title: Using Ingress Gateways to Connect External Traffic to Internal Services description: >- - An ingress gateway enables ingress traffic from services outside the Consul - service mesh to services inside the Consul service mesh. This section details - how to use Envoy and describes how you can plug in a gateway of your choice. + This topic describes how ingress gateways enable traffic from external services to reach services inside the Consul service mesh. + It provides guidance on how to use Envoy and how to plug into your preferred gateway. --- # Ingress Gateways diff --git a/website/content/docs/connect/gateways/mesh-gateway/index.mdx b/website/content/docs/connect/gateways/mesh-gateway/index.mdx deleted file mode 100644 index 533821c5d..000000000 --- a/website/content/docs/connect/gateways/mesh-gateway/index.mdx +++ /dev/null @@ -1,215 +0,0 @@ ---- -layout: docs -page_title: Connect Datacenters - Mesh Gateways -description: >- - A Mesh Gateway enables better routing of a Connect service's data to upstreams - in other datacenters. This section details how to use Envoy and describes how - you can plug in a gateway of your choice. ---- - -# Mesh Gateways - --> **1.6.0+:** This feature is available in Consul versions 1.6.0 and newer. - -Mesh gateways enable routing of Connect traffic between different Consul datacenters. Those datacenters -can reside in different clouds or runtime environments where general interconnectivity between all services -in all datacenters isn't feasible. These gateways operate by sniffing the SNI header out of the Connect session -and then route the connection to the appropriate destination based on the server name requested. The data -within the mTLS session is not decrypted by the Gateway. - -![Mesh Gateway Architecture](/img/mesh-gateways.png) - -For a complete example of how to connect services across datacenters, -review the [mesh gateway tutorial](https://learn.hashicorp.com/tutorials/consul/service-mesh-gateways). - -## Prerequisites - -Each mesh gateway needs three things: - -1. A local Consul agent to manage its configuration. -2. General network connectivity to all services within its local Consul datacenter. -3. General network connectivity to all mesh gateways within remote Consul datacenters. - -Mesh gateways also require that your Consul datacenters are configured correctly: - -- Consul version 1.6.0 or newer is required. -- Consul [Connect](/docs/agent/options#connect) must be enabled in both datacenters. -- Each of your [datacenters](/docs/agent/options#datacenter) must have a unique name. -- Your datacenters must be [WAN joined](https://learn.hashicorp.com/tutorials/consul/federarion-gossip-wan). -- The [primary datacenter](/docs/agent/options#primary_datacenter) must be set to the same value in both datacenters. This specifies which datacenter is the authority for Connect certificates and is required for services in all datacenters to establish mutual TLS with each other. -- [gRPC](/docs/agent/options#grpc_port) must be enabled. -- If you want to [enable gateways globally](/docs/connect/mesh-gateway#enabling-gateways-globally) you must enable [centralized configuration](/docs/agent/options#enable_central_service_config). - -Currently, Envoy is the only proxy with mesh gateway capabilities in Consul. - -- Mesh gateway proxies receive their configuration through Consul, which - automatically generates it based on the proxy's registration. Currently Consul - can only translate mesh gateway registration information into Envoy - configuration, therefore the proxies acting as mesh gateways must be Envoy. - -- Sidecar proxies that send traffic to an upstream service through a gateway - need to know the location of that gateway. They discover the gateway based on - their sidecar proxy registrations. Consul can only translate the gateway - registration information into Envoy configuration, so any sidecars that send - upstream traffic through a gateway must be Envoy. - -Sidecar proxies that don't send upstream traffic through a gateway aren't -affected when you deploy gateways. If you are using Consul's built-in proxy as a -Connect sidecar it will continue to work for intra-datacenter traffic and will -receive incoming traffic even if that traffic has passed through a gateway. - -## Modes of Operation - -Each upstream of a Connect proxy can be configured to be routed through a mesh gateway. Depending on -your network, the proxy's connection to the gateway can happen in one of the following modes -illustrated in the diagram above: - -- `local` - In this mode the Connect proxy makes its outbound connection to a gateway running in the - same datacenter. That gateway is then responsible for ensuring the data gets forwarded along to - gateways in the destination datacenter. This is the mode of operation depicted in the diagram at - the beginning of the page. - -- `remote` - In this mode the Connect proxy makes its outbound connection to a gateway running in the - destination datacenter. That gateway will then forward the data to the final destination service. - -- `none` - In this mode, no gateway is used and a Connect proxy makes its outbound connections directly - to the destination services. - -## Mesh Gateway Configuration - -Mesh gateways are defined similarly to other services registered with Consul, with two exceptions. -The first is that the [service kind](/api/agent/service#kind) must be "mesh-gateway". Second, -the mesh gateway service definition may contain a `Proxy.Config` entry, just like a -Connect proxy service, to define opaque configuration parameters useful for the actual proxy software. -For Envoy there are some supported [gateway options](/docs/connect/proxies/envoy#gateway-options) as well as -[escape-hatch overrides](/docs/connect/proxies/envoy#escape-hatch-overrides). - --> **Note:** If ACLs are enabled, a token granting `service:write` for the gateway's service name -and `service:read` for all services in the datacenter must be added to the gateway's service definition. -These permissions authorize the token to route communications for other Connect services but does not -allow decrypting any of their communications. - -## Connect Proxy Configuration - -Configuring a Connect Proxy to use gateways is as simple as setting its mode of operation. This can be done -in several different places allowing for global to more fine grained control. If the gateway mode is configured -in multiple locations the order of precedence is as follows - -1. Upstream Definition -2. Service Instance Definition -3. Centralized `service-defaults` configuration entry -4. Centralized `proxy-defaults` configuration entry. - -### Enabling Gateways Globally - -The following `proxy-defaults` configuration will enable gateways for all Connect services in the `local` mode. - -```hcl -Kind = "proxy-defaults" -Name = "global" -MeshGateway { - Mode = "local" -} -``` - -### Enabling Gateways Per-Service - -The following `service-defaults` configuration will enable gateways for all Connect services with the name "web". - -```hcl -Kind = "service-defaults" -Name = "web" -MeshGateway { - Mode = "local" -} -``` - -### Enabling Gateways for a Service Instance - -The following [Proxy Service Registration](/docs/connect/registration/service-registration) -definition will enable gateways for the service instance in the `remote` mode. - -```hcl -service { - name = "web-sidecar-proxy" - kind = "connect-proxy" - port = 8181 - proxy { - destination_service_name = "web" - mesh_gateway { - mode = "remote" - } - upstreams = [ - { - destination_name = "api" - datacenter = "secondary" - local_bind_port = 10000 - } - ] - } -} -``` - -Or alternatively inline with the service definition: - -```hcl -service { - name = "web" - port = 8181 - connect { - sidecar_service { - proxy { - mesh_gateway { - mode = "remote" - } - upstreams = [ - { - destination_name = "api" - datacenter = "secondary" - local_bind_port = 10000 - } - ] - } - } - } -} -``` - -### Enabling Gateways for a Proxy Upstream - -The following service definition will enable gateways in the `local` mode for one upstream, the `remote` mode -for a second upstream and will disable gateways for a third upstream. - -```hcl -service { - name = "web-sidecar-proxy" - kind = "connect-proxy" - port = 8181 - proxy { - destination_service_name = "web" - upstreams = [ - { - destination_name = "api" - local_bind_port = 10000 - mesh_gateway { - mode = "remote" - } - }, - { - destination_name = "db" - local_bind_port = 10001 - mesh_gateway { - mode = "local" - } - }, - { - destination_name = "logging" - local_bind_port = 10002 - mesh_gateway { - mode = "none" - } - }, - ] - } -} -``` diff --git a/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters.mdx b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters.mdx new file mode 100644 index 000000000..6add2cee0 --- /dev/null +++ b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters.mdx @@ -0,0 +1,267 @@ +--- +layout: docs +page_title: Service-to-service Traffic Across Datacenters +description: >- + This topic describes how to configure mesh gateways to route a service's data to upstreams + in other datacenters. It describes how to use Envoy and how you can integrate with your preferred gateway. +--- + +# Service-to-service Traffic Across Datacenters + +-> **1.6.0+:** This feature is available in Consul versions 1.6.0 and newer. + +Mesh gateways enable service mesh traffic to be routed between different Consul datacenters. +Datacenters can reside in different clouds or runtime environments where general interconnectivity between all services +in all datacenters isn't feasible. + +Mesh gateways operate by sniffing and extracting the server name indication (SNI) header from the service mesh session and routing the connection to the appropriate destination based on the server name requested. The gateway does not decrypt the data within the mTLS session. + +The following diagram describes the architecture for using mesh gateways for cross-datacenter communication: + +![Mesh Gateway Architecture](/img/mesh-gateways.png) + +-> **Mesh Gateway Tutorial**: Follow the [mesh gateway tutorial](https://learn.hashicorp.com/tutorials/consul/service-mesh-gateways) to learn important concepts associated with using mesh gateways for connecting services across datacenters. + +## Prerequisites + +Ensure that your Consul environment meets the following requirements. + +### Consul + +* Consul version 1.6.0 or newer. +* A local Consul agent is required to manage its configuration. +* Consul [Connect](/docs/agent/options#connect) must be enabled in both datacenters. +* Each [datacenter](/docs/agent/options#datacenter) must have a unique name. +* Each datacenters must be [WAN joined](https://learn.hashicorp.com/tutorials/consul/federarion-gossip-wan). +* The [primary datacenter](/docs/agent/options#primary_datacenter) must be set to the same value in both datacenters. This specifies which datacenter is the authority for Connect certificates and is required for services in all datacenters to establish mutual TLS with each other. +* [gRPC](/docs/agent/options#grpc_port) must be enabled. +* If you want to [enable gateways globally](/docs/connect/mesh-gateway#enabling-gateways-globally) you must enable [centralized configuration](/docs/agent/options#enable_central_service_config). + +### Network + +* General network connectivity to all services within its local Consul datacenter. +* General network connectivity to all mesh gateways within remote Consul datacenters. + +### Proxy + +Envoy is the only proxy with mesh gateway capabilities in Consul. + +Mesh gateway proxies receive their configuration through Consul, which automatically generates it based on the proxy's registration. +Consul can only translate mesh gateway registration information into Envoy configuration. + +Sidecar proxies that send traffic to an upstream service through a gateway need to know the location of that gateway. They discover the gateway based on their sidecar proxy registrations. Consul can only translate the gateway registration information into Envoy configuration. + +Sidecar proxies that do not send upstream traffic through a gateway are not affected when you deploy gateways. If you are using Consul's built-in proxy as a Connect sidecar it will continue to work for intra-datacenter traffic and will receive incoming traffic even if that traffic has passed through a gateway. + +## Configuration + +Configure the following settings to register the mesh gateway as a service in Consul. + +* Specify `mesh-gateway` in the `kind` field to register the gateway with Consul. +* Configure the `proxy.upstreams` parameters to route traffic to the correct service, namespace, and datacenter. Refer to the [`upstreams` documentation](/docs/connect/registration/service-registration#upstream-configuration-reference) for details. The service `proxy.upstreams.destination_name` is always required. The `proxy.upstreams.datacenter` must be configured to enable cross-datacenter traffic. The `proxy.upstreams.destination_namespace` configuration is only necessary if the destination service is in a different namespace. +* Define the `Proxy.Config` settings using opaque parameters compatible with your proxy (i.e., Envoy). For Envoy, refer to the [Gateway Options](/docs/connect/proxies/envoy#gateway-options) and [Escape-hatch Overrides](/docs/connect/proxies/envoy#escape-hatch-overrides) documentation for additional configuration information. +* If ACLs are enabled, a token granting `service:write` for the gateway's service name and `service:read` for all services in the datacenter or partition must be added to the gateway's service definition. These permissions authorize the token to route communications for other Consul service mesh services, but does not allow decrypting any of their communications. + +### Modes + +Each upstream associated with a service mesh proxy can be configured so that it is routed through a mesh gateway. +Depending on your network, the proxy's connection to the gateway can operate in one of the following modes (refer to the [mesh-architecture-diagram](#mesh-architecture-diagram)): + +* `none` - (Default) No gateway is used and a service mesh connect proxy makes its outbound connections directly + to the destination services. + +* `local` - The service mesh connect proxy makes an outbound connection to a gateway running in the + same datacenter. That gateway is responsible for ensuring that the data is forwarded to gateways in the destination datacenter. + Refer to the flow labeled `local` in the [mesh-architecture-diagram](#mesh-architecture-diagram). + +* `remote` - The service mesh proxy makes an outbound connection to a gateway running in the destination datacenter. + The gateway forwards the data to the final destination service. + Refer to the flow labeled `remote` in the [mesh-architecture-diagram](#mesh-architecture-diagram). + +### Connect Proxy Configuration + +Set the proxy to the preferred [mode](#modes) to configure the service mesh proxy. You can specify the mode globally or within child configurations to control proxy behaviors at a lower level. Consul recognizes the following order of precedence if the gateway mode is configured in multiple locations the order of precedence: + +1. Upstream definition (highest priority) +2. Service instance definition +3. Centralized `service-defaults` configuration entry +4. Centralized `proxy-defaults` configuration entry + +## Example Configurations + +Use the following example configurations to help you understand some of the common scenarios. + +### Enabling Gateways Globally + +The following `proxy-defaults` configuration will enable gateways for all Connect services in the `local` mode. + + + +```hcl +Kind = "proxy-defaults" +Name = "global" +MeshGateway { + Mode = "local" +} +``` + +```yaml +Kind: proxy-defaults +MeshGateway: +- Mode: local +Name: global +``` + + +### Enabling Gateways Per Service + +The following `service-defaults` configuration will enable gateways for all Connect services with the name `web`. + + + +```hcl +Kind = "service-defaults" +Name = "web" +MeshGateway { + Mode = "local" +} +``` + +```yaml +Kind: service-defaults +MeshGateway: +- Mode: local +Name: web +``` + + + +### Enabling Gateways for a Service Instance + +The following [Proxy Service Registration](/docs/connect/registration/service-registration) +definition will enable gateways for the service instance in the `remote` mode. + + + +```hcl +service { + name = "web-sidecar-proxy" + kind = "connect-proxy" + port = 8181 + proxy { + destination_service_name = "web" + mesh_gateway { + mode = "remote" + } + upstreams = [ + { + destination_name = "api" + datacenter = "secondary" + local_bind_port = 10000 + } + ] + } +} + +# Or alternatively inline with the service definition: + +service { + name = "web" + port = 8181 + connect { + sidecar_service { + proxy { + mesh_gateway { + mode = "remote" + } + upstreams = [ + { + destination_name = "api" + datacenter = "secondary" + local_bind_port = 10000 + } + ] + } + } + } +} +``` + +```yaml +service: +- kind: connect-proxy + name: web-sidecar-proxy + port: 8181 + proxy: + - destination_service_name: web + mesh_gateway: + - mode: remote + upstreams: + - datacenter: secondary + destination_name: api + local_bind_port: 100 +``` + + + +### Enabling Gateways for a Proxy Upstream + +The following service definition will enable gateways in the `local` mode for one upstream, the `remote` mode for a second upstream and will disable gateways for a third upstream. + + + +```hcl +service { + name = "web-sidecar-proxy" + kind = "connect-proxy" + port = 8181 + proxy { + destination_service_name = "web" + upstreams = [ + { + destination_name = "api" + local_bind_port = 10000 + mesh_gateway { + mode = "remote" + } + }, + { + destination_name = "db" + local_bind_port = 10001 + mesh_gateway { + mode = "local" + } + }, + { + destination_name = "logging" + local_bind_port = 10002 + mesh_gateway { + mode = "none" + } + }, + ] + } +} +``` +```yaml +service: +- kind: connect-proxy + name: web-sidecar-proxy + port: 8181 + proxy: + - destination_service_name: web + upstreams: + - destination_name: api + local_bind_port: 10000 + mesh_gateway: + - mode: remote + - destination_name: db + local_bind_port: 10001 + mesh_gateway: + - mode: local + - destination_name: logging + local_bind_port: 10002 + mesh_gateway: + - mode: none + ``` + \ No newline at end of file diff --git a/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx new file mode 100644 index 000000000..bc529496f --- /dev/null +++ b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx @@ -0,0 +1,244 @@ +--- +layout: docs +page_title: Service-to-service Traffic Across Partitions +description: >- + This topic describes how to configure mesh gateways to route a service's data to upstreams + in other partitions. It describes how to use Envoy and how you can integrate with your preferred gateway. +--- + +# Service-to-service Traffic Across Partitions + +-> **Consul Enterprise 1.11.0+:** Admin partitions are supported in Consul Enterprise versions 1.11.0 and newer. + +Mesh gateways enable you to route service mesh traffic between different Consul [admin partitions](/docs/enteprise/admin-partitions). +Partitions can reside in different clouds or runtime environments where general interconnectivity between all services +in all partitions isn't feasible. + +Mesh gateways operate by sniffing and extracting the server name indication (SNI) header from the service mesh session and routing the connection to the appropriate destination based on the server name requested. The gateway does not decrypt the data within the mTLS session. + +## Prerequisites + +Ensure that your Consul environment meets the following requirements. + +### Consul + +* Consul Enterprise version 1.11.0 or newer. +* A local Consul agent is required to manage its configuration. +* Consul service mesh must be enabled in all partitions. Refer to the [`connect` documentation](/docs/agent/options#connect) for details. +* Each partition must have a unique name. Refer to the [admin partitions documentation](/docs/enteprise/admin-partitions) for details. +* If you want to [enable gateways globally](/docs/connect/mesh-gateway#enabling-gateways-globally) you must enable [centralized configuration](/docs/agent/options#enable_central_service_config). + +### Proxy + +Envoy is the only proxy with mesh gateway capabilities in Consul. + +Mesh gateway proxies receive their configuration through Consul, which automatically generates it based on the proxy's registration. +Consul can only translate mesh gateway registration information into Envoy configuration. + +Sidecar proxies that send traffic to an upstream service through a gateway need to know the location of that gateway. They discover the gateway based on their sidecar proxy registrations. Consul can only translate the gateway registration information into Envoy configuration. + +Sidecar proxies that do not send upstream traffic through a gateway are not affected when you deploy gateways. If you are using Consul's built-in proxy as a Connect sidecar it will continue to work for intra-datacenter traffic and will receive incoming traffic even if that traffic has passed through a gateway. + +## Configuration + +Configure the following settings to register the mesh gateway as a service in Consul. + +* Specify `mesh-gateway` in the `kind` field to register the gateway with Consul. +* Configure the `proxy.upstreams` parameters to route traffic to the correct service, namespace, and partition. Refer to the [`upstreams` documentation](/docs/connect/registration/service-registration#upstream-configuration-reference) for details. The service `proxy.upstreams.destination_name` is always required. The `proxy.upstreams.destination_partition` must be configured to enable cross-partition traffic. The `proxy.upstreams.destination_namespace` configuration is only necessary if the destination service is in a different namespace. +* Configure the `exported-services` configuration entry to enable Consul to export services contained in an admin partition to one or more additional partitions. Refer to the [Exported Services documentation](/docs/connect/config-entries/exported-services) for details. +* Define the `Proxy.Config` settings using opaque parameters compatible with your proxy, i.e., Envoy. For Envoy, refer to the [Gateway Options](/docs/connect/proxies/envoy#gateway-options) and [Escape-hatch Overrides](/docs/connect/proxies/envoy#escape-hatch-overrides) documentation for additional configuration information. +* If ACLs are enabled, a token granting `service:write` for the gateway's service name and `service:read` for all services in the datacenter or partition must be added to the gateway's service definition. These permissions authorize the token to route communications for other Consul service mesh services, but does not allow decrypting any of their communications. + +### Modes + +Each upstream associated with a service mesh proxy can be configured so that it is routed through a mesh gateway. +Depending on your network, the proxy's connection to the gateway can operate in one of the following modes: + +* `none` - (Default) No gateway is used and a service mesh connect proxy makes its outbound connections directly + to the destination services. + +* `local` - The service mesh connect proxy makes an outbound connection to a gateway running in the same datacenter. The gateway at the outbound connection is responsible for ensuring that the data is forwarded to gateways in the destination partition. + +* `remote` - The service mesh connect proxy makes an outbound connection to a gateway running in the destination datacenter. + The gateway forwards the data to the final destination service. + +### Connect Proxy Configuration + +Set the proxy to the preferred [mode](#modes) to configure the service mesh proxy. You can specify the mode globally or within child configurations to control proxy behaviors at a lower level. Consul recognizes the following order of precedence if the gateway mode is configured in multiple locations the order of precedence: + +1. Upstream definition (highest priority) +2. Service instance definition +3. Centralized `service-defaults` configuration entry +4. Centralized `proxy-defaults` configuration entry + +## Example Configurations + +Use the following example configurations to help you understand some of the common scenarios. + +### Enabling Gateways Globally + +The following `proxy-defaults` configuration will enable gateways for all Connect services in the `local` mode. + + + +```hcl +Kind = "proxy-defaults" +Name = "global" +MeshGateway { + Mode = "local" +} +``` + +```yaml +Kind: proxy-defaults +MeshGateway: +- Mode: local +Name: global +``` + + + +### Enabling Gateways Per Service + +The following `service-defaults` configuration will enable gateways for all Connect services with the name `web`. + + + +```hcl +Kind = "service-defaults" +Name = "web" +MeshGateway { + Mode = "local" +} +``` + +```yaml +Kind: service-defaults +MeshGateway: +- Mode: local +Name: web +``` + + +### Enabling Gateways for a Service Instance + +The following [Proxy Service Registration](/docs/connect/registration/service-registration) +definition will enable gateways for `web` service instances in the `finance` partition. + + + +```hcl +service { + name = "web-sidecar-proxy" + kind = "connect-proxy" + port = 8181 + proxy { + destination_service_name = "web" + mesh_gateway { + mode = "local" + } + upstreams = [ + { + destination_partition = "finance" + destination_namespace = "default" + destination_type = "service" + destination_name = "billing" + local_bind_port = 9090 + } + ] + } +} +``` + +```yaml +service: +- kind: connect-proxy + name: web-sidecar-proxy + port: 8181 + proxy: + - destination_service_name: web + mesh_gateway: + - mode: local + upstreams: + - destination_name: billing + destination_namespace: default + destination_partition: finance + destination_type: service + local_bind_port: 9090 +``` + + +### Enabling Gateways for a Proxy Upstream + +The following service definition will enable gateways in `local` mode for three different partitions. Note that each service exists in the same namepace, but are separated by admin partition. + + + +```hcl +service { + name = "web-sidecar-proxy" + kind = "connect-proxy" + port = 8181 + proxy { + destination_service_name = "web" + upstreams = [ + { + destination_name = "api" + destination_namespace = "dev" + destination_partition = "api" + local_bind_port = 10000 + mesh_gateway { + mode = "local" + } + }, + { + destination_name = "db" + destination_namespace = "dev" + destination_partition = "db" + local_bind_port = 10001 + mesh_gateway { + mode = "local" + } + }, + { + destination_name = "logging" + destination_namespace = "dev" + destination_partition = "logging" + local_bind_port = 10002 + mesh_gateway { + mode = "local" + } + }, + ] + } +} +``` + +```yaml +service: +- kind: connect-proxy + name: web-sidecar-proxy + port: 8181 + proxy: + - destination_service_name: web + upstreams: + - destination_name: api + destination_namespace: dev + destination_partition: api + local_bind_port: 10000 + mesh_gateway: + - mode: local + - destination_name: db + destination_namespace: dev + destination_partition: db + local_bind_port: 10001 + mesh_gateway: + - mode: local + - destination_name: logging + destination_namespace: dev + destination_partition: logging + local_bind_port: 10002 + mesh_gateway: + - mode: local +``` + \ No newline at end of file diff --git a/website/content/docs/enterprise/admin-partitions.mdx b/website/content/docs/enterprise/admin-partitions.mdx index ab8c2aabf..3a88138d9 100644 --- a/website/content/docs/enterprise/admin-partitions.mdx +++ b/website/content/docs/enterprise/admin-partitions.mdx @@ -187,7 +187,7 @@ Verify that your Consul deployment meets the [Kubernetes Requirements](#kubernet ``` 1. Create the workload configuration for client nodes in your cluster. Create a configuration for each admin partition. In the following example, the external IP address and the Kubernetes authentication method IP address from the previous steps have been applied: - + ```yaml @@ -252,7 +252,7 @@ You can log into the Consul UI to verify that the partitions appear as expected. 1. If ACLs are enabled, you will need the partitions ACL token, which can be read from the Kubernetes secret. The token is an encoded string that must be decoded in base64, e.g.: ```shell-session - kubectl get secret server-consul-bootstrap-acl-token -o json | jq -r .data.token | base64 -d - + kubectl get secret server-consul-bootstrap-acl-token --template "{{ .data.token | base64decode }}" ``` The example command gets the token using the secret name configured in the values file (`bootstrap.secretName`), decodes the secret, and prints the usable token to the console in JSON format. diff --git a/website/content/docs/k8s/connect/connect-ca-provider.mdx b/website/content/docs/k8s/connect/connect-ca-provider.mdx index baddd34c3..de1bca22a 100644 --- a/website/content/docs/k8s/connect/connect-ca-provider.mdx +++ b/website/content/docs/k8s/connect/connect-ca-provider.mdx @@ -26,8 +26,8 @@ To configure the Vault Connect Provider please see [Vault as the Service Mesh Ce ~> **NOTE:** The following instructions are only valid for Consul-k8s 0.37.0 and prior. Below we will go over the process for configuring Vault as the Connect CA. -However, other providers can be configured similarly by providing the appropriate `ca_config` -and `ca_provider` values for the provider you're using. +However, other providers can similarly be configured during initial bootstrap of the cluster +by providing the appropriate [`ca_config`] and [`ca_provider`] values for the provider you're using. ## Configuring Vault as a Connect CA (Consul K8s 0.37.0 and earlier) @@ -55,8 +55,9 @@ kubectl create secret generic vault-ca --from-file vault.ca=/path/to/your/vault/ And then reference it like this in the provider configuration: -```shell-session -$ cat vault-config.json + + +```json { "connect": [ { @@ -75,6 +76,8 @@ $ cat vault-config.json } ``` + + This example configuration file is pointing to a Vault instance running in the same Kubernetes cluster, which has been deployed with TLS enabled. Note that the `ca_file` is pointing to the file location based on the Kubernetes secret for the Vault CA that we have created before. @@ -94,6 +97,8 @@ $ kubectl create secret generic vault-config --from-file=config=vault-config.jso We will provide this secret and the Vault CA secret, to the Consul server via the `server.extraVolumes` Helm value. + + ```yaml global: name: consul @@ -112,6 +117,8 @@ We will provide this secret and the Vault CA secret, to the Consul server via th enabled: true ``` + + Finally, [install](/docs/k8s/installation/install#installing-consul) the Helm chart using the above config file: ```shell-session @@ -121,7 +128,7 @@ $ helm install consul -f config.yaml hashicorp/consul Verify that the CA provider is set correctly: ```shell-session -$ kubectl exec consul-server-0 -- curl -s http://localhost:8500/v1/connect/ca/configuration | jq . +$ kubectl exec consul-server-0 -- curl -s http://localhost:8500/v1/connect/ca/configuration\?pretty { "Provider": "vault", "Config": { @@ -149,6 +156,8 @@ for which this configuration is intended. You will similarly need to create a Vault token and a Kubernetes secret with Vault's CA in each secondary Kubernetes cluster. + + ```json { "connect": [ @@ -168,6 +177,8 @@ Vault's CA in each secondary Kubernetes cluster. } ``` + + Note that all secondary datacenters need to have access to the same Vault instance as the primary. ### Manually Rotating Vault Tokens @@ -177,11 +188,16 @@ then you will need to manually renew or rotate the Vault token before it expires #### Rotating Vault Token -Once the cluster is running, subsequent changes to the `ca_provider` config are **ignored**–even if `consul reload` is run or the servers are restarted. +The [`ca_config`] and [`ca_provider`] options defined in the Consul agent +configuration are only used when initially bootstrapping the cluster. Once the +cluster is running, subsequent changes to the [`ca_provider`] config are **ignored**–even if `consul reload` is run or the servers are restarted. -To update any settings under this key, you must use Consul's [Update CA Configuration](/api/connect/ca#update-ca-configuration) API or the [`consul connect ca set-config`](/commands/connect/ca#set-config) command. +To update any settings under these keys, you must use Consul's [Update CA Configuration](/api/connect/ca#update-ca-configuration) API or the [`consul connect ca set-config`](/commands/connect/ca#set-config) command. #### Renewing Vault Token To renew the Vault token, use the [`vault token renew`](https://www.vaultproject.io/docs/commands/token/renew) CLI command or API. + +[`ca_config`]: /docs/agent/options#connect_ca_config +[`ca_provider`]: /docs/agent/options#connect_ca_provider diff --git a/website/content/docs/k8s/installation/vault/index.mdx b/website/content/docs/k8s/installation/vault/index.mdx index 0943c3c92..9008d5bd4 100644 --- a/website/content/docs/k8s/installation/vault/index.mdx +++ b/website/content/docs/k8s/installation/vault/index.mdx @@ -38,9 +38,8 @@ injector: - `global.tls.serverAdditionalIPSans` is not currently configurable and must be manually added to the server certificate in Vault. - Mesh gateway is not currently supported. - Multi-DC Federation is not currently supported. -- Certificate rotation is not currently supported, ensure the TTL for your certificates is sufficiently long. Should your certificates -expire it will be necessary to issue a `consul reload` on each server. -- CA rotation is not currently supported. +- Certificate rotation for Server TLS certs is not currently supported through the Helm chart. Ensure the TTL for your Server TLS certificates are sufficiently long. Should your certificates expire it will be necessary to issue a `consul reload` on each server after issuing new Server TLS certs from Vault. +- CA rotation is not currently supported through the Helm chart and must be manually rotated. ## Next Steps diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index 2b55f25c3..c4b07305a 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -170,7 +170,7 @@ $ consul-k8s status metrics: defaultEnableMerging: true defaultEnabled: true - enableGatewayMetrics: trueU + enableGatewayMetrics: true controller: enabled: true global: diff --git a/website/content/docs/security/acl/acl-system.mdx b/website/content/docs/security/acl/acl-system.mdx index 927e0838d..8f951d004 100644 --- a/website/content/docs/security/acl/acl-system.mdx +++ b/website/content/docs/security/acl/acl-system.mdx @@ -69,16 +69,53 @@ If the ACL system becomes inoperable, you can follow the ### ACL Policies -An ACL policy is a named set of rules and is composed of the following elements: +An ACL policy (not to be confused with [policy dispositions](/docs/security/acl/acl-rules#policy-dispositions)) is a named set of rules and several attributes that define the policy domain. The ID is generated when the policy is created, but you can specify the attributes when creating the policy. Refer to the [ACL policy command line](https://www.consul.io/commands/acl/policy) documentation or [ACL policy API](/api-docs/acl/policies) documentation for additional information on how to create policies. -- **ID** - The policy's auto-generated public identifier. -- **Name** - A unique meaningful name for the policy. -- **Description** - A human readable description of the policy. (Optional) -- **Rules** - Set of rules granting or denying permissions. See the [Rule Specification](/docs/acl/acl-rules#rule-specification) documentation for more details. -- **Datacenters** - A list of datacenters the policy is valid within. -- **Namespace** - - The namespace this policy resides within. (Added in Consul Enterprise 1.7.0) +ACL policies can have the following attributes: --> **Consul Enterprise Namespacing** - Rules defined in a policy in any namespace other than `default` will be [restricted](/docs/acl/acl-rules#namespace-rules) to being able to grant a subset of the overall privileges and only affecting that single namespace. +| Attribute | Description | Required | Default | +| --- | --- | --- | --- | +| `ID` | The policy's auto-generated public identifier. | N/A | N/A | +| `name` | Unique name for the policy. | Required | none | +| `description` | Human readable description of the policy. | Optional | none | +| `rules` | Set of rules granting or denying permissions. See the [Rule Specification](/docs/acl/acl-rules#rule-specification) documentation for more details. | Optional | none | +| `datacenter` | Datacenter in which the policy is valid. More than one datacenter can be specified. | Optional | none | +| `namespace` | Namespace in which the policy is valid. Added in Consul Enterprise 1.7.0. | Optional | `default` | +| `partition` | Admin partition in which the policy is valid. Added in Consul Enterprise 1.11.0 | Optional | `default` | + +-> **Non-default Namespaces and Partitions** - Rules defined in a policy tied to an namespace or admin partition other than `default` can only grant a subset of privileges that affect the namespace or partition. See [Namespace Rules](/docs/acl/acl-rules#namespace-rules) and [Admin Partition Rules](/docs/acl/acl-rules#admin-partition-rules) for additional information. + +You can view the current ACL policies on the command line or through the API. The following example demonstrates the command line usage: + +```shell-session +$ consul acl policy list -format json -token +[ + { + "ID": "56595ec1-52e4-d6de-e566-3b78696d5459", + "Name": "b-policy", + "Description": "", + "Datacenters": null, + "Hash": "ULwaXlI6Ecqb9YSPegXWgVL1LlwctY9TeeAOhp5HGBA=", + "CreateIndex": 126, + "ModifyIndex": 126, + "Namespace": "default", + "Partition": "default" + }, + { + "ID": "00000000-0000-0000-0000-000000000001", + "Name": "global-management", + "Description": "Builtin Policy that grants unlimited access", + "Datacenters": null, + "Hash": "W1bQuDAlAlxEb4ZWwnVHplnt3I5oPKOZJQITh79Xlog=", + "CreateIndex": 70, + "ModifyIndex": 70, + "Namespace": "default", + "Partition": "default" + } +] +``` + +Note that the `Hash`, `CreateIndex`, and `ModifyIndex` attributes are also printed. These attributes are printed for all responses and are not specific to ACL policies. #### Builtin Policies @@ -130,8 +167,7 @@ node_prefix "" { The [API documentation for roles](/api/acl/roles#sample-payload) has some examples of using a service identity. --> **Consul Enterprise Namespacing** - Service Identity rules will be scoped to the single namespace that -the corresponding ACL Token or Role resides within. +-> **Service Scope for Namespace and Admin Partition** - Service identity rules in Consul Enterprise are scoped to the namespace or admin partition within which the corresponding ACL token or role resides. ### ACL Node Identities @@ -179,26 +215,66 @@ of the following elements: - **Service Identity Set** - The list of service identities that are applicable for the role. - **Namespace** - The namespace this policy resides within. (Added in Consul Enterprise 1.7.0) --> **Consul Enterprise Namespacing** - Roles may only link to policies defined in the same namespace as the role itself. +-> **Linking Roles to Policies in Consul Enterprise** - Roles can only be linked to policies that are defined in the same namespace and admin partition. ### ACL Tokens -ACL tokens are used to determine if the caller is authorized to perform an action. An ACL token is composed of the following -elements: +Consul uses ACL tokens to determine if the caller is authorized to perform an action. An ACL token is composed of several attributes that you can specify when creating the token. Refer to the [ACL token command line](https://www.consul.io/commands/acl/token) documentation or [ACL token API](/api-docs/acl/tokens) documentation for additional information on how to create tokens.: - **Accessor ID** - The token's public identifier. - **Secret ID** -The bearer token used when making requests to Consul. -- **Description** - A human readable description of the token. (Optional) - **Policy Set** - The list of policies that are applicable for the token. -- **Role Set** - The list of roles that are applicable for the token. (Added in Consul 1.5.0) -- **Service Identity Set** - The list of service identities that are applicable for the token. (Added in Consul 1.5.0) -- **Locality** - Indicates whether the token should be local to the datacenter it was created within or created in - the primary datacenter and globally replicated. -- **Expiration Time** - The time at which this token is revoked. (Optional; Added in Consul 1.5.0) -- **Namespace** - The namespace this policy resides within. (Added in Consul Enterprise 1.7.0) +- **Role Set** - The list of roles that are applicable for the token. Added in Consul 1.5.0. +- **Service Identity Set** - The list of service identities that are applicable for the token. Added in Consul 1.5.0 +- **Local** - Indicates whether the token is local to the datacenter in which it was created. The attribute also can specify if the token was created in the primary datacenter and globally replicated. +- **CreateTime** - Timestamp indicating when the token was created. +- **Expiration Time** - The time at which this token is revoked. This attribute is option when creating a token. Added in Consul 1.5.0. +- **Namespace** - The namespace in which the token resides. Added in Consul Enterprise 1.7.0. +- **Partition** - The partition in which the token resides. Added in Consul Enterprise 1.11.0. --> **Consul Enterprise Namespacing** - Tokens may only link to policies and roles defined in the same namespace as -the token itself. +-> **Linking Tokens to Policies in Consul Enterprise** - Tokens can only be linked to policies that are defined in the same namespace and admin partition. + +You can view the current ACL tokens on the command line or through the API. The following example demonstrates the command line usage: + +```shell-session +$ consul acl token list -format json -token +[ + { + "CreateIndex": 75, + "ModifyIndex": 75, + "AccessorID": "c3274caa-fbe4-b457-f4af-c05ba89a048d", + "SecretID": "105c016a-ae9c-2006-ce23-4ef8823ba2af", + "Description": "Bootstrap Token (Global Management)", + "Policies": [ + { + "ID": "00000000-0000-0000-0000-000000000001", + "Name": "global-management" + } + ], + "Local": false, + "CreateTime": "2021-12-16T10:22:08.906291-08:00", + "Hash": "Wda9obh/gvreyTbVhbyJ3ipX0M/apF4kpqowPQQx+u8=", + "Legacy": false, + "Namespace": "default", + "Partition": "default" + }, + { + "CreateIndex": 71, + "ModifyIndex": 71, + "AccessorID": "00000000-0000-0000-0000-000000000002", + "SecretID": "anonymous", + "Description": "Anonymous Token", + "Local": false, + "CreateTime": "2021-12-16T10:21:11.996298-08:00", + "Hash": "tgCOyeidw+oaoZXQ9mHy6+EnY7atKoGaBzg2ndTwXl0=", + "Legacy": false, + "Namespace": "default", + "Partition": "default" + } +] +``` + +Note that the `CreateIndex`, `ModifyIndex`, and `Hash` attributes are also printed. These attributes are printed for all responses and are not specific to ACL tokens. #### Builtin Tokens diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index f4caf7a58..77a5ef1ff 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -277,24 +277,28 @@ "path": "connect/gateways" }, { - "title": "Connect Datacenters - Mesh Gateways", + "title": "Mesh Gateways", "routes": [ - { - "title": "Overview", - "path": "connect/gateways/mesh-gateway" - }, { "title": "WAN Federation", "path": "connect/gateways/mesh-gateway/wan-federation-via-mesh-gateways" + }, + { + "title": "Enabling Service-to-service Traffic Across Datacenters", + "path": "connect/gateways/mesh-gateway/service-to-service-traffic-datacenters" + }, + { + "title": "Enabling Service-to-service Traffic Across Admin Partitions", + "path": "connect/gateways/mesh-gateway/service-to-service-traffic-partitions" } ] }, { - "title": "External <> Internal Services - Ingress Gateways", + "title": "Ingress Gateways", "path": "connect/gateways/ingress-gateway" }, { - "title": "Internal <> External Services - Terminating Gateways", + "title": "Terminating Gateways", "path": "connect/gateways/terminating-gateway" } ] diff --git a/website/data/subnav.js b/website/data/subnav.js deleted file mode 100644 index cc14febd2..000000000 --- a/website/data/subnav.js +++ /dev/null @@ -1,56 +0,0 @@ -export default [ - { text: 'Overview', url: '/' }, - { - text: 'Use Cases', - submenu: [ - { - text: 'Service Discovery and Health Checking', - url: '/use-cases/service-discovery-and-health-checking', - }, - { - text: 'Network Infrastructure Automation', - url: '/use-cases/network-infrastructure-automation', - }, - { - text: 'Multi-Platform Service Mesh', - url: '/use-cases/multi-platform-service-mesh', - }, - { - text: 'Consul on Kubernetes', - url: '/consul-on-kubernetes', - }, - ], - }, - { - text: 'Enterprise', - url: - 'https://www.hashicorp.com/products/consul/?utm_source=oss&utm_medium=header-nav&utm_campaign=consul', - type: 'outbound', - }, - 'divider', - { - text: 'Tutorials', - url: 'https://learn.hashicorp.com/consul', - type: 'outbound', - }, - { - text: 'Docs', - url: '/docs', - type: 'inbound', - }, - { - text: 'API', - url: '/api-docs', - type: 'inbound', - }, - { - text: 'CLI', - url: '/commands', - type: 'inbound,', - }, - { - text: 'Community', - url: '/community', - type: 'inbound', - }, -] diff --git a/website/data/version.js b/website/data/version.js index 7f1d5d8c8..9797bab9f 100644 --- a/website/data/version.js +++ b/website/data/version.js @@ -1 +1 @@ -export default '1.10.4' +export default '1.11.1' diff --git a/website/layouts/standard/index.tsx b/website/layouts/standard/index.tsx new file mode 100644 index 000000000..38bb3f8c5 --- /dev/null +++ b/website/layouts/standard/index.tsx @@ -0,0 +1,76 @@ +import query from './query.graphql' +import ProductSubnav from 'components/subnav' +import Footer from 'components/footer' +import { open } from '@hashicorp/react-consent-manager' + +export default function StandardLayout(props: Props): React.ReactElement { + const { useCaseNavItems } = props.data + + return ( + <> + { + return { + text: item.text, + url: `/use-cases/${item.url}`, + } + }), + ].sort((a, b) => a.text.localeCompare(b.text)), + }, + { + text: 'Enterprise', + url: + 'https://www.hashicorp.com/products/consul/?utm_source=oss&utm_medium=header-nav&utm_campaign=consul', + type: 'outbound', + }, + 'divider', + { + text: 'Tutorials', + url: 'https://learn.hashicorp.com/consul', + type: 'outbound', + }, + { + text: 'Docs', + url: '/docs', + type: 'inbound', + }, + { + text: 'API', + url: '/api-docs', + type: 'inbound', + }, + { + text: 'CLI', + url: '/commands', + type: 'inbound,', + }, + { + text: 'Community', + url: '/community', + type: 'inbound', + }, + ]} + /> + {props.children} +