Merge branch 'main' of github.com:hashicorp/consul into docs/service-mesh-config-entries-add-partitions--1.11.0

pulling main into the this branch
This commit is contained in:
trujillo-adam 2021-12-22 13:12:08 -08:00
commit 5835d18664
163 changed files with 9796 additions and 4499 deletions

4
.changelog/11774.txt Normal file
View File

@ -0,0 +1,4 @@
```release-note:bug
ui: Differentiate between Service Meta and Node Meta when choosing search fields
in Service Instance listings
```

3
.changelog/11781.txt Normal file
View File

@ -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.
```

5
.changelog/11868.txt Normal file
View File

@ -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
```

4
.changelog/11891.txt Normal file
View File

@ -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
```

3
.changelog/11892.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
ui: Ensure a login buttons appear for some error states, plus text amends
```

33
.github/scripts/metrics_checker.sh vendored Executable file
View File

@ -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

View File

@ -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

View File

@ -6,12 +6,13 @@ import (
"testing" "testing"
"time" "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/raft"
"github.com/hashicorp/serf/serf" "github.com/hashicorp/serf/serf"
"github.com/stretchr/testify/require" "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) { func TestAutopilot_IdempotentShutdown(t *testing.T) {
@ -19,7 +20,7 @@ func TestAutopilot_IdempotentShutdown(t *testing.T) {
t.Skip("too slow for testing.Short") t.Skip("too slow for testing.Short")
} }
dir1, s1 := testServerWithConfig(t, nil) dir1, s1 := testServerWithConfig(t)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
retry.Run(t, func(r *retry.R) { r.Check(waitForLeader(s1)) }) retry.Run(t, func(r *retry.R) { r.Check(waitForLeader(s1)) })

View File

@ -1599,7 +1599,7 @@ func TestIntentionList_acl(t *testing.T) {
t.Parallel() t.Parallel()
dir1, s1 := testServerWithConfig(t, testServerACLConfig(nil)) dir1, s1 := testServerWithConfig(t, testServerACLConfig)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
codec := rpcClient(t, s1) codec := rpcClient(t, s1)

View File

@ -1784,7 +1784,7 @@ func TestInternal_GatewayIntentions_aclDeny(t *testing.T) {
t.Skip("too slow for testing.Short") t.Skip("too slow for testing.Short")
} }
dir1, s1 := testServerWithConfig(t, testServerACLConfig(nil)) dir1, s1 := testServerWithConfig(t, testServerACLConfig)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
codec := rpcClient(t, s1) codec := rpcClient(t, s1)

View File

@ -66,21 +66,12 @@ func testTLSCertificates(serverName string) (cert string, key string, cacert str
return cert, privateKey, ca, nil return cert, privateKey, ca, nil
} }
// testServerACLConfig wraps another arbitrary Config altering callback // testServerACLConfig setup some common ACL configurations.
// to setup some common ACL configurations. A new callback func will func testServerACLConfig(c *Config) {
// be returned that has the original callback invoked after setting c.PrimaryDatacenter = "dc1"
// up all of the ACL configurations (so they can still be overridden) c.ACLsEnabled = true
func testServerACLConfig(cb func(*Config)) func(*Config) { c.ACLInitialManagementToken = TestDefaultMasterToken
return func(c *Config) { c.ACLResolverSettings.ACLDefaultPolicy = "deny"
c.PrimaryDatacenter = "dc1"
c.ACLsEnabled = true
c.ACLInitialManagementToken = TestDefaultMasterToken
c.ACLResolverSettings.ACLDefaultPolicy = "deny"
if cb != nil {
cb(c)
}
}
} }
func configureTLS(config *Config) { func configureTLS(config *Config) {
@ -164,8 +155,6 @@ func testServerConfig(t *testing.T) (string, *Config) {
config.ServerHealthInterval = 50 * time.Millisecond config.ServerHealthInterval = 50 * time.Millisecond
config.AutopilotInterval = 100 * time.Millisecond config.AutopilotInterval = 100 * time.Millisecond
config.Build = "1.7.2"
config.CoordinateUpdatePeriod = 100 * time.Millisecond config.CoordinateUpdatePeriod = 100 * time.Millisecond
config.LeaveDrainTime = 1 * time.Millisecond config.LeaveDrainTime = 1 * time.Millisecond
@ -187,14 +176,12 @@ func testServerConfig(t *testing.T) (string, *Config) {
return dir, config return dir, config
} }
// Deprecated: use testServerWithConfig instead. It does the same thing and more.
func testServer(t *testing.T) (string, *Server) { func testServer(t *testing.T) (string, *Server) {
return testServerWithConfig(t, func(c *Config) { return testServerWithConfig(t)
c.Datacenter = "dc1"
c.PrimaryDatacenter = "dc1"
c.Bootstrap = true
})
} }
// Deprecated: use testServerWithConfig
func testServerDC(t *testing.T, dc string) (string, *Server) { func testServerDC(t *testing.T, dc string) (string, *Server) {
return testServerWithConfig(t, func(c *Config) { return testServerWithConfig(t, func(c *Config) {
c.Datacenter = dc 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) { func testServerDCBootstrap(t *testing.T, dc string, bootstrap bool) (string, *Server) {
return testServerWithConfig(t, func(c *Config) { return testServerWithConfig(t, func(c *Config) {
c.Datacenter = dc 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) { func testServerDCExpect(t *testing.T, dc string, expect int) (string, *Server) {
return testServerWithConfig(t, func(c *Config) { return testServerWithConfig(t, func(c *Config) {
c.Datacenter = dc 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) { func testServerWithConfig(t *testing.T, configOpts ...func(*Config)) (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) {
var dir string var dir string
var srv *Server 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) { retry.RunWith(retry.ThreeTimes(), t, func(r *retry.R) {
var config *Config var config *Config
dir, config = testServerConfig(t) dir, config = testServerConfig(t)
if cb != nil { for _, fn := range configOpts {
cb(config) fn(config)
} }
// Apply config to copied fields because many tests only set the old // 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. // 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) { func testACLServerWithConfig(t *testing.T, cb func(*Config), initReplicationToken bool) (string, *Server, rpc.ClientCodec) {
dir, srv := testServerWithConfig(t, testServerACLConfig(cb)) opts := []func(*Config){testServerACLConfig}
t.Cleanup(func() { srv.Shutdown() }) if cb != nil {
opts = append(opts, cb)
}
dir, srv := testServerWithConfig(t, opts...)
if initReplicationToken { if initReplicationToken {
// setup some tokens here so we get less warnings in the logs // 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) codec := rpcClient(t, srv)
t.Cleanup(func() { codec.Close() })
return dir, srv, codec return dir, srv, codec
} }
@ -1284,7 +1266,11 @@ func TestServer_Expect_NonVoters(t *testing.T) {
} }
t.Parallel() 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 os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()

View File

@ -4,9 +4,10 @@ import (
"os" "os"
"testing" "testing"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/testrpc" "github.com/hashicorp/consul/testrpc"
"github.com/stretchr/testify/require"
) )
func TestLeader_SystemMetadata_CRUD(t *testing.T) { func TestLeader_SystemMetadata_CRUD(t *testing.T) {
@ -32,10 +33,10 @@ func TestLeader_SystemMetadata_CRUD(t *testing.T) {
state := srv.fsm.State() state := srv.fsm.State()
// Initially has no entries // Initially has one entry for virtual-ips feature flag
_, entries, err := state.SystemMetadataList(nil) _, entries, err := state.SystemMetadataList(nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, entries, 0) require.Len(t, entries, 1)
// Create 3 // Create 3
require.NoError(t, srv.setSystemMetadataKey("key1", "val1")) require.NoError(t, srv.setSystemMetadataKey("key1", "val1"))
@ -52,12 +53,13 @@ func TestLeader_SystemMetadata_CRUD(t *testing.T) {
_, entries, err = state.SystemMetadataList(nil) _, entries, err = state.SystemMetadataList(nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, entries, 3) require.Len(t, entries, 4)
require.Equal(t, map[string]string{ require.Equal(t, map[string]string{
"key1": "val1", structs.SystemMetadataVirtualIPsEnabled: "true",
"key2": "val2", "key1": "val1",
"key3": "", "key2": "val2",
"key3": "",
}, mapify(entries)) }, mapify(entries))
// Update one and delete one. // Update one and delete one.
@ -66,10 +68,11 @@ func TestLeader_SystemMetadata_CRUD(t *testing.T) {
_, entries, err = state.SystemMetadataList(nil) _, entries, err = state.SystemMetadataList(nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, entries, 2) require.Len(t, entries, 3)
require.Equal(t, map[string]string{ require.Equal(t, map[string]string{
"key2": "val2", structs.SystemMetadataVirtualIPsEnabled: "true",
"key3": "val3", "key2": "val2",
"key3": "val3",
}, mapify(entries)) }, mapify(entries))
} }

View File

@ -83,7 +83,7 @@ func (c *cmd) Run(args []string) int {
} }
c.UI.Output("==> Saved " + certFileName) 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()) c.UI.Error(err.Error())
return 1 return 1
} }

View File

@ -3,6 +3,7 @@ package create
import ( import (
"crypto" "crypto"
"crypto/x509" "crypto/x509"
"io/fs"
"io/ioutil" "io/ioutil"
"os" "os"
"strings" "strings"
@ -120,6 +121,14 @@ func expectFiles(t *testing.T, caPath, keyPath string) (*x509.Certificate, crypt
require.FileExists(t, caPath) require.FileExists(t, caPath)
require.FileExists(t, keyPath) 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) caData, err := ioutil.ReadFile(caPath)
require.NoError(t, err) require.NoError(t, err)
keyData, err := ioutil.ReadFile(keyPath) keyData, err := ioutil.ReadFile(keyPath)

View File

@ -196,7 +196,7 @@ func (c *cmd) Run(args []string) int {
} }
c.UI.Output("==> Saved " + certFileName) 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()) c.UI.Error(err.Error())
return 1 return 1
} }

View File

@ -3,6 +3,7 @@ package create
import ( import (
"crypto" "crypto"
"crypto/x509" "crypto/x509"
"io/fs"
"io/ioutil" "io/ioutil"
"net" "net"
"os" "os"
@ -242,6 +243,14 @@ func expectFiles(t *testing.T, certPath, keyPath string) (*x509.Certificate, cry
require.FileExists(t, certPath) require.FileExists(t, certPath)
require.FileExists(t, keyPath) 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) certData, err := ioutil.ReadFile(certPath)
require.NoError(t, err) require.NoError(t, err)
keyData, err := ioutil.ReadFile(keyPath) keyData, err := ioutil.ReadFile(keyPath)

View File

@ -13,6 +13,7 @@ button.type-cancel {
@extend %secondary-button; @extend %secondary-button;
} }
.with-confirmation .type-delete, .with-confirmation .type-delete,
.modal-dialog .type-delete,
%app-view-content form button[type='button'].type-delete { %app-view-content form button[type='button'].type-delete {
@extend %dangerous-button; @extend %dangerous-button;
} }

View File

@ -7,8 +7,11 @@ for more details.
Using this component for all of our errors means we can show a consistent Using this component for all of our errors means we can show a consistent
error page for generic errors. error page for generic errors.
This component show slighltly different visuals and copy depending on the This component show slightly different visuals and copy depending on the
`status` of the error (the status is generally a HTTP error code) `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 ## 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 | | `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}` | | `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 ```hbs preview-template
<ErrorState <ErrorState
@error={{hash status='403'}} @error={{hash status='403'}}
/> />
``` ```
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
<ErrorState
@error={{hash
status='404'
message="`message` is what is shown in the header"
detail="`detail` is what shown in the body"
}}
/>
```
As with `EmptyState` you can optionally chose to show a login button using the As with `EmptyState` you can optionally chose to show a login button using the
`@login` argument. `@login` argument.

View File

@ -4,43 +4,63 @@
@login={{@login}} @login={{@login}}
> >
<BlockSlot @name="header"> <BlockSlot @name="header">
<h2>{{or @error.message "Consul returned an error"}}</h2> <h2>
{{or @error.message "Consul returned an error"}}
</h2>
</BlockSlot> </BlockSlot>
{{#if @error.status }} {{#if @error.status }}
<BlockSlot @name="subheader"> <BlockSlot @name="subheader">
<h3 data-test-status={{@error.status}}>Error {{@error.status}}</h3> <h3
data-test-status={{@error.status}}
>
Error {{@error.status}}
</h3>
</BlockSlot> </BlockSlot>
{{/if}} {{/if}}
<BlockSlot @name="body"> <BlockSlot @name="body">
{{#if error.detail}} <p>
<p> {{#if @error.detail}}
{{error.detail}} {{@error.detail}}
</p> {{else}}
{{else}}
<p>
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. 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.
</p> {{/if}}
{{/if}} </p>
</BlockSlot> </BlockSlot>
<BlockSlot @name="actions"> <BlockSlot @name="actions">
<li class="back-link"> <li class="back-link">
<a data-test-home rel="home" href={{href-to 'index'}}>Go back</a> <Action
data-test-home
@href={{href-to 'index'}}
>
Go back
</Action>
</li> </li>
<li class="docs-link"> <li class="docs-link">
<a href="{{env 'CONSUL_DOCS_URL'}}" rel="noopener noreferrer" target="_blank">Read the documentation</a> <Action
@href="{{env 'CONSUL_DOCS_URL'}}"
@external={{true}}
>
Read the documentation
</Action>
</li> </li>
</BlockSlot> </BlockSlot>
</EmptyState> </EmptyState>
{{else}} {{else}}
<EmptyState <EmptyState
class="status-403" class={{concat "status-" @error.status}}
@login={{@login}} @login={{@login}}
> >
<BlockSlot @name="header"> <BlockSlot @name="header">
<h2 data-test-status={{@error.status}}>You are not authorized</h2> <h2
data-test-status={{@error.status}}
>
You are not authorized
</h2>
</BlockSlot> </BlockSlot>
<BlockSlot @name="subheader"> <BlockSlot @name="subheader">
<h3>Error 403</h3> <h3>
Error {{@error.status}}
</h3>
</BlockSlot> </BlockSlot>
<BlockSlot @name="body"> <BlockSlot @name="body">
<p> <p>
@ -49,10 +69,20 @@
</BlockSlot> </BlockSlot>
<BlockSlot @name="actions"> <BlockSlot @name="actions">
<li class="docs-link"> <li class="docs-link">
<a href="{{env 'CONSUL_DOCS_URL'}}/acl/index.html" rel="noopener noreferrer" target="_blank">Read the documentation</a> <Action
@href="{{env 'CONSUL_DOCS_URL'}}/acl/index.html"
@external={{true}}
>
Read the documentation
</Action>
</li> </li>
<li class="learn-link"> <li class="learn-link">
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/security-networking/production-acls" rel="noopener noreferrer" target="_blank">Follow the guide</a> <Action
@href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/security-networking/production-acls"
@external={{true}}
>
Follow the guide
</Action>
</li> </li>
</BlockSlot> </BlockSlot>
</EmptyState> </EmptyState>

View File

@ -3,30 +3,8 @@ class: ember
--- ---
# ModalDialog # ModalDialog
## Arguments Consul UIs modal component is a thin wrapper around the excellent `a11y-dialog`. The
most common usage will be something like the below:
| 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 `<ModalLayer />` 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
<ModalLayer />
```
Then all modals will be rendered into the `<ModalLayer />` for example:
```hbs preview-template ```hbs preview-template
<ModalDialog <ModalDialog
@ -67,3 +45,30 @@ as |modal|>
</button> </button>
``` ```
All modals work in tandem with `<ModalLayer />` 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
<ModalLayer />
```
Then all modals will be rendered into the `<ModalLayer />`.
## 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 |

View File

@ -11,6 +11,9 @@ export default Component.extend(Slotted, {
this.dialog = new A11yDialog($el); this.dialog = new A11yDialog($el);
this.dialog.on('hide', () => this.onclose({ target: $el })); this.dialog.on('hide', () => this.onclose({ target: $el }));
this.dialog.on('show', () => this.onopen({ target: $el })); this.dialog.on('show', () => this.onopen({ target: $el }));
if (this.open) {
this.dialog.show();
}
}, },
disconnect: function($el) { disconnect: function($el) {
this.dialog.destroy(); this.dialog.destroy();

View File

@ -7,7 +7,7 @@ export default class InstancesRoute extends Route {
source: 'source', source: 'source',
searchproperty: { searchproperty: {
as: '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: { search: {
as: 'filter', as: 'filter',

View File

@ -1,7 +1,6 @@
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { get, setProperties, action } from '@ember/object'; import { get, setProperties, action } from '@ember/object';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import HTTPError from 'consul-ui/utils/http/error';
// paramsFor // paramsFor
import { routes } from 'consul-ui/router'; 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 * 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 * parameter from the URL. This is the most common behavior if you don't

View File

@ -1,5 +1,6 @@
export default { export default {
Name: item => item.Name, Name: item => item.Name,
Node: item => item.Node.Node,
Tags: item => item.Service.Tags || [], Tags: item => item.Service.Tags || [],
ID: item => item.Service.ID || '', ID: item => item.Service.ID || '',
Address: item => item.Address || '', Address: item => item.Address || '',

View File

@ -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 // This temporary measure should be removed again once https://github.com/hashicorp/consul/issues/11098
// has been resolved // has been resolved
this.permissions.forEach(item => { 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; item.Allow = true;
} }
}) });
/**/ /**/
return this.permissions; return this.permissions;
} }

View File

@ -48,6 +48,7 @@
<ModalDialog <ModalDialog
data-test-delete-modal data-test-delete-modal
@onclose={{action cancel}} @onclose={{action cancel}}
@open={{true}}
@aria={{hash @aria={{hash
label="Policy in Use" label="Policy in Use"
}} }}

View File

@ -90,7 +90,18 @@ as |route|>
/> />
</collection.Collection> </collection.Collection>
<collection.Empty> <collection.Empty>
<EmptyState> <EmptyState
@login={{route.model.app.login.open}}
>
<BlockSlot @name="header">
<h2>
{{#if (gt items.length 0)}}
No nodes found
{{else}}
Welcome to Nodes
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body"> <BlockSlot @name="body">
<p> <p>
There don't seem to be any registered nodes, or you may not have access to view nodes yet. There don't seem to be any registered nodes, or you may not have access to view nodes yet.

View File

@ -105,7 +105,7 @@ as |sort filters items partition nspace|}}
> >
<BlockSlot @name="header"> <BlockSlot @name="header">
<h2> <h2>
{{#if (gt services.length 0)}} {{#if (gt items.length 0)}}
No services found No services found
{{else}} {{else}}
Welcome to Services Welcome to Services
@ -114,7 +114,7 @@ as |sort filters items partition nspace|}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="body"> <BlockSlot @name="body">
<p> <p>
{{#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. No services where found matching that search, or you may not have access to view the services you are searching for.
{{else}} {{else}}
There don't seem to be any registered services, or you may not have access to view services yet. There don't seem to be any registered services, or you may not have access to view services yet.

View File

@ -89,7 +89,9 @@ as |route|>
</Consul::Intention::List> </Consul::Intention::List>
</collection.Collection> </collection.Collection>
<collection.Empty> <collection.Empty>
<EmptyState> <EmptyState
@login={{route.model.app.login.open}}
>
<BlockSlot @name="header"> <BlockSlot @name="header">
<h2> <h2>
{{#if (gt items.length 0)}} {{#if (gt items.length 0)}}

View File

@ -23,7 +23,12 @@ Feature: dc / intentions / index
dc: dc-1 dc: dc-1
--- ---
Then the url should be /dc-1/intentions 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 Scenario: Viewing intentions in the listing live updates
Given 1 datacenter model with the value "dc-1" Given 1 datacenter model with the value "dc-1"
Given 3 intention models Given 3 intention models

View File

@ -9,7 +9,7 @@ export default function(visitable, submitable, deletable, cancelable, clickable,
datacenter: clickable('[name="policy[Datacenters]"]'), datacenter: clickable('[name="policy[Datacenters]"]'),
deleteModal: { deleteModal: {
resetScope: true, resetScope: true,
scope: '[data-test-delete-modal]', scope: '[data-test-delete-modal]:not([aria-hidden="true"])',
...deletable({}), ...deletable({}),
}, },
}; };

View File

@ -30,6 +30,8 @@ consul:
terminating-gateway: Terminating Gateway terminating-gateway: Terminating Gateway
mesh-gateway: Mesh Gateway mesh-gateway: Mesh Gateway
status: Health Status status: Health Status
service.meta: Service Meta
node.meta: Node Meta
service-name: Service Name service-name: Service Name
node-name: Node Name node-name: Node Name
accessorid: AccessorID accessorid: AccessorID

View File

@ -2,7 +2,6 @@
padding: 25px 0 17px 0; padding: 25px 0 17px 0;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
border-top: 1px solid var(--gray-5);
& .g-grid-container { & .g-grid-container {
display: flex; display: flex;

View File

@ -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<IoCardProps>
}
export default function IoCardContaianer({
theme = 'light',
heading,
description,
label,
cta,
cardsPerRow = 3,
cards,
}: IoCardContaianerProps): React.ReactElement {
return (
<div className={classNames(s.cardContainer, s[theme])}>
{heading || description ? (
<header className={s.header}>
{heading ? <h2 className={s.heading}>{heading}</h2> : null}
{description ? <p className={s.description}>{description}</p> : null}
</header>
) : null}
{cards.length ? (
<>
{label || cta ? (
<header className={s.subHeader}>
{label ? <h3 className={s.label}>{label}</h3> : null}
{cta ? (
<Button
title={cta.text}
url={cta.url}
linkType="inbound"
theme={{
brand: 'neutral',
variant: 'tertiary',
background: theme,
}}
/>
) : null}
</header>
) : null}
<ul
className={classNames(
s.cardList,
cardsPerRow === 3 && s.threeUp,
cardsPerRow === 4 && s.fourUp
)}
style={
{
'--length': cards.length,
} as React.CSSProperties
}
>
{cards.map((card, index) => {
return (
// Index is stable
// eslint-disable-next-line react/no-array-index-key
<li key={index}>
<IoCard variant={theme} {...card} />
</li>
)
})}
</ul>
</>
) : null}
</div>
)
}

View File

@ -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;
}
}
}

View File

@ -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' ? (
<Link href={link.url}>
<a className={className}>{children}</a>
</Link>
) : (
<a
className={className}
href={link.url}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
)
return (
<article className={classNames(s.card)}>
<LinkWrapper className={classNames(s[variant], s[inset])}>
{children ? (
children
) : (
<>
{eyebrow ? <Eyebrow>{eyebrow}</Eyebrow> : null}
{heading ? <Heading>{heading}</Heading> : null}
{description ? <Description>{description}</Description> : null}
</>
)}
<footer className={s.footer}>
{products && (
<ul className={s.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
<li key={index}>
<InlineSvg
className={s.logo}
src={productLogos[key][version]}
/>
</li>
)
})}
</ul>
)}
<span className={s.linkType}>
{link.type === 'inbound' ? (
<IconArrowRight24 />
) : (
<IconExternalLink24 />
)}
</span>
</footer>
</LinkWrapper>
</article>
)
}
interface EyebrowProps {
children: string
}
function Eyebrow({ children }: EyebrowProps) {
return <p className={s.eyebrow}>{children}</p>
}
interface HeadingProps {
as?: 'h2' | 'h3' | 'h4'
children: React.ReactNode
}
function Heading({ as: Component = 'h2', children }: HeadingProps) {
return <Component className={s.heading}>{children}</Component>
}
interface DescriptionProps {
children: string
}
function Description({ children }: DescriptionProps) {
return <p className={s.description}>{children}</p>
}
IoCard.Eyebrow = Eyebrow
IoCard.Heading = Heading
IoCard.Description = Description
export default IoCard

View File

@ -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'),
},
}

View File

@ -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);
}
}

View File

@ -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 (
<AnimatePresence>
{isOpen && (
<AnimatedDialogOverlay
className={s.dialogOverlay}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onDismiss={onDismiss}
>
<div className={s.dialogWrapper}>
<motion.div
initial={{ y: 50 }}
animate={{ y: 0 }}
exit={{ y: 50 }}
transition={{ min: 0, max: 100, bounceDamping: 8 }}
style={{ width: '100%', maxWidth: 800 }}
>
<DialogContent className={s.dialogContent} aria-label={label}>
<button onClick={onDismiss} className={s.dialogClose}>
Close
</button>
{children}
</DialogContent>
</motion.div>
</div>
</AnimatedDialogOverlay>
)}
</AnimatePresence>
)
}

View File

@ -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;
}
}
}

View File

@ -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 (
<div className={s.callToAction}>
<ReactCallToAction
variant="compact"
heading={heading}
content={content}
product={brand}
theme="dark"
links={links.map(({ text, url }, index) => {
return {
text,
url,
type: index === 1 ? 'inbound' : null,
}
})}
/>
</div>
)
}

View File

@ -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;
}
}

View File

@ -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 (
<section className={s.root}>
<div className={s.container}>
<header className={s.header}>
<h2 className={s.heading}>{heading}</h2>
<p className={s.description}>{description}</p>
</header>
<div className={s.caseStudies}>
<ul className={s.primary}>
{primary.map((item, index) => {
return (
<li key={index} className={s.primaryItem}>
<a className={s.card} href={item.link}>
<h3 className={s.cardHeading}>{item.heading}</h3>
<Image
className={s.cardThumbnail}
src={item.thumbnail.url}
layout="fill"
objectFit="cover"
alt={item.thumbnail.alt}
/>
</a>
</li>
)
})}
</ul>
<ul className={s.secondary}>
{secondary.map((item, index) => {
return (
<li key={index} className={s.secondaryItem}>
<a className={s.link} href={item.link}>
<span className={s.linkInner}>
<h3 className={s.linkHeading}>{item.heading}</h3>
{isInternalLink(item.link) ? (
<IconArrowRight16 />
) : (
<IconExternalLink16 />
)}
</span>
</a>
</li>
)
})}
</ul>
</div>
</div>
</section>
)
}

View File

@ -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;
}

View File

@ -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 (
<IoHomeFeatureWrap isInternalLink={isInternalLink} href={link}>
<div className={s.featureMedia}>
<Image
src={image.url}
width={400}
height={200}
layout="responsive"
alt={image.alt}
/>
</div>
<div className={s.featureContent}>
<h3 className={s.featureHeading}>{heading}</h3>
<p className={s.featureDescription}>{description}</p>
{link ? (
<span className={s.featureCta} aria-hidden={true}>
Learn more{' '}
<span>
<IconArrowRight16 />
</span>
</span>
) : null}
</div>
</IoHomeFeatureWrap>
)
}
interface IoHomeFeatureWrapProps {
isInternalLink: (link: string) => boolean
href: string
children: React.ReactNode
}
function IoHomeFeatureWrap({
isInternalLink,
href,
children,
}: IoHomeFeatureWrapProps) {
if (!href) {
return <div className={s.feature}>{children}</div>
}
if (isInternalLink(href)) {
return (
<Link href={href}>
<a className={s.feature}>{children}</a>
</Link>
)
}
return (
<a className={s.feature} href={href}>
{children}
</a>
)
}

View File

@ -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);
}
}

View File

@ -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<IoHomeHeroCardProps>
}
export default function IoHomeHero({
pattern,
brand,
heading,
description,
ctas,
cards,
}: IoHomeHeroProps) {
const [loaded, setLoaded] = React.useState(false)
React.useEffect(() => {
setTimeout(() => {
setLoaded(true)
}, 250)
}, [])
return (
<header
className={classNames(s.hero, loaded && s.loaded)}
style={
{
'--pattern': `url(${pattern})`,
} as React.CSSProperties
}
>
<span className={s.pattern} />
<div className={s.container}>
<div className={s.content}>
<h1 className={s.heading}>{heading}</h1>
<p className={s.description}>{description}</p>
{ctas && (
<div className={s.ctas}>
{ctas.map((cta, index) => {
return (
<Button
key={index}
title={cta.title}
url={cta.link}
linkType="inbound"
theme={{
brand: 'neutral',
variant: 'tertiary',
background: 'light',
}}
/>
)
})}
</div>
)}
</div>
{cards && (
<div className={s.cards}>
{cards.map((card, index) => {
return (
<IoHomeHeroCard
key={index}
index={index}
heading={card.heading}
description={card.description}
cta={{
brand: index === 0 ? 'neutral' : brand,
title: card.cta.title,
link: card.cta.link,
}}
subText={card.subText}
/>
)
})}
</div>
)}
</div>
</header>
)
}
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 (
<article
className={s.card}
style={
{
'--index': index,
} as React.CSSProperties
}
>
<h2 className={s.cardHeading}>{heading}</h2>
<p className={s.cardDescription}>{description}</p>
<Button
title={cta.title}
url={cta.link}
theme={{
variant: 'primary',
brand: cta.brand,
}}
/>
<p className={s.cardSubText}>{subText}</p>
</article>
)
}

View File

@ -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);
}
}

View File

@ -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<IoCardProps>
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 (
<section
className={s.inPractice}
style={
{
'--pattern': `url(${pattern})`,
} as React.CSSProperties
}
>
<div className={s.container}>
<IoCardContainer
theme="dark"
heading={heading}
description={description}
cardsPerRow={3}
cards={cards}
/>
{cta.heading ? (
<div className={s.inPracticeCta}>
<div className={s.inPracticeCtaContent}>
<h3 className={s.inPracticeCtaHeading}>{cta.heading}</h3>
{cta.description ? (
<p className={s.inPracticeCtaDescription}>{cta.description}</p>
) : null}
{cta.link ? (
<Button
title="Learn more"
url={cta.link}
theme={{
brand: brand,
}}
/>
) : null}
</div>
{cta.image?.url ? (
<div className={s.inPracticeCtaMedia}>
<Image
src={cta.image.url}
width={cta.image.width}
height={cta.image.height}
alt={cta.image.alt}
/>
</div>
) : null}
</div>
) : null}
</div>
</section>
)
}

View File

@ -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;
}

View File

@ -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<IoHomeFeatureProps>
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 (
<section
className={classNames(
s.root,
s[brand],
features && s.withFeatures,
offerings && s.withOfferings
)}
style={
{
'--brand': `var(--${brand})`,
} as React.CSSProperties
}
>
<header className={s.header}>
<div className={s.container}>
<div className={s.headerInner}>
<h2 className={s.heading}>{heading}</h2>
<p className={s.description}>{description}</p>
</div>
</div>
</header>
{features ? (
<ul className={s.features}>
{features.map((feature, index) => {
return (
// Index is stable
// eslint-disable-next-line react/no-array-index-key
<li key={index}>
<div className={s.container}>
<IoHomeFeature
isInternalLink={isInternalLink}
image={{
url: feature.image.url,
alt: feature.image.alt,
}}
heading={feature.heading}
description={feature.description}
link={feature.link}
/>
</div>
</li>
)
})}
</ul>
) : null}
{offerings ? (
<div className={s.offerings}>
{offerings.image ? (
<div className={s.offeringsMedia}>
<Image
src={offerings.image.src}
width={offerings.image.width}
height={offerings.image.height}
alt={offerings.image.alt}
/>
</div>
) : null}
<div className={s.offeringsContent}>
<ul className={s.offeringsList}>
{offerings.list.map((offering, index) => {
return (
// Index is stable
// eslint-disable-next-line react/no-array-index-key
<li key={index}>
<h3 className={s.offeringsListHeading}>
{offering.heading}
</h3>
<p className={s.offeringsListDescription}>
{offering.description}
</p>
</li>
)
})}
</ul>
{offerings.cta ? (
<div className={s.offeringsCta}>
<Button
title={offerings.cta.title}
url={offerings.cta.link}
theme={{
brand: 'neutral',
}}
/>
</div>
) : null}
</div>
</div>
) : null}
{video ? (
<div className={s.video}>
<IoVideoCallout
youtubeId={video.youtubeId}
thumbnail={video.thumbnail}
heading={video.heading}
description={video.description}
person={{
name: video.person.name,
description: video.person.description,
avatar: video.person.avatar,
}}
/>
</div>
) : null}
</section>
)
}

View File

@ -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;
}
}

View File

@ -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 (
<div className={classNames(s.preFooter, s[brand])}>
<div className={s.container}>
<div className={s.content}>
<h2 className={s.heading}>{heading}</h2>
<p className={s.description}>{description}</p>
</div>
<div className={s.cards}>
{ctas.map((cta, index) => {
return (
<IoHomePreFooterCard
key={index}
brand={brand}
link={cta.link}
heading={cta.heading}
description={cta.description}
cta={cta.cta}
/>
)
})}
</div>
</div>
</div>
)
}
interface IoHomePreFooterCard {
brand?: string
link: string
heading: string
description: string
cta: string
}
function IoHomePreFooterCard({
brand,
link,
heading,
description,
cta,
}: IoHomePreFooterCard): React.ReactElement {
return (
<a
href={link}
className={s.card}
style={
{
'--primary': `var(--${brand})`,
'--secondary': `var(--${brand}-secondary)`,
} as React.CSSProperties
}
>
<h3 className={s.cardHeading}>{heading}</h3>
<p className={s.cardDescription}>{description}</p>
<span className={s.cardCta}>
{cta} <IconArrowRight16 />
</span>
</a>
)
}

View File

@ -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);
}
}

View File

@ -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 (
<div
className={classNames(s.callToAction, s[theme])}
style={
{
'--background-color': `var(--${brand})`,
} as React.CSSProperties
}
>
<h2 className={s.heading}>{heading}</h2>
<div className={s.content}>
<p className={s.description}>{description}</p>
<div className={s.links}>
{links.map((link, index) => {
return (
<Button
// Index is stable
// eslint-disable-next-line react/no-array-index-key
key={index}
title={link.text}
url={link.url}
theme={{
brand: 'neutral',
variant: index === 0 ? 'primary' : 'secondary',
background: theme,
}}
/>
)
})}
</div>
</div>
<div className={s.pattern}>
<Image
src={pattern}
layout="fill"
objectFit="cover"
objectPosition="center left"
alt=""
/>
</div>
</div>
)
}

View File

@ -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;
}
}

View File

@ -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 (
<section className={s.customer}>
<div className={s.container}>
<div className={s.columns}>
<div className={s.media}>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<Image {...media} layout="responsive" />
</div>
<div className={s.content}>
<div className={s.eyebrow}>
<div className={s.eyebrowLogo}>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<Image {...logo} />
</div>
<span className={s.eyebrowLabel}>Customer case study</span>
</div>
<h2 className={s.heading}>{heading}</h2>
<p className={s.description}>{description}</p>
{link ? (
<div className={s.cta}>
<Button
title="Read more"
url={link}
theme={{
brand: 'neutral',
variant: 'secondary',
background: 'dark',
}}
/>
</div>
) : null}
</div>
</div>
{stats.length > 0 ? (
<ul className={s.stats}>
{stats.map(({ key, value }, index) => {
return (
// Index is stable
// eslint-disable-next-line react/no-array-index-key
<li key={index}>
<p className={s.value}>{value}</p>
<p className={s.key}>{key}</p>
</li>
)
})}
</ul>
) : null}
</div>
</section>
)
}

View File

@ -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);
}

View File

@ -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 (
<header className={s.hero}>
<div className={s.container}>
<div className={s.pattern}>
{pattern ? (
<Image
src={pattern}
layout="responsive"
width={420}
height={500}
priority={true}
alt=""
/>
) : null}
</div>
<div className={s.content}>
<p className={s.eyebrow}>{eyebrow}</p>
<h1 className={s.heading}>{heading}</h1>
<p className={s.description}>{description}</p>
</div>
</div>
</header>
)
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -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);
}

View File

@ -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 (
<section
className={classNames(s.section, s[brand], bottomIsFlush && s.isFlush)}
>
<div className={s.container}>
<p className={s.eyebrow}>{eyebrow}</p>
<div className={s.columns}>
<div className={s.column}>
<h2 className={s.heading}>{heading}</h2>
{media?.src ? (
<div
className={s.description}
dangerouslySetInnerHTML={{
__html: description,
}}
/>
) : null}
{cta?.link && cta?.text ? (
<div className={s.cta}>
<Button
title={cta.text}
url={cta.link}
theme={{
brand: brand,
}}
/>
</div>
) : null}
</div>
<div className={s.column}>
{media?.src ? (
// eslint-disable-next-line jsx-a11y/alt-text
<Image {...media} />
) : (
<div
className={s.description}
dangerouslySetInnerHTML={{
__html: description,
}}
/>
)}
</div>
</div>
</div>
</section>
)
}

View File

@ -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;
}

View File

@ -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 (
<>
<figure className={s.videoCallout}>
<button className={s.thumbnail} onClick={showVideo}>
<VisuallyHidden>Play video</VisuallyHidden>
<PlayIcon />
<Image src={thumbnail} layout="fill" objectFit="cover" alt="" />
</button>
<figcaption className={s.content}>
<h3 className={s.heading}>{heading}</h3>
<p className={s.description}>{description}</p>
{person && (
<div className={s.person}>
{person.avatar ? (
<div className={s.personThumbnail}>
<Image
src={person.avatar}
width={52}
height={52}
alt={`${person.name} avatar`}
/>
</div>
) : null}
<div>
<p className={s.personName}>{person.name}</p>
<p className={s.personDescription}>{person.description}</p>
</div>
</div>
)}
</figcaption>
</figure>
<IoDialog
isOpen={showDialog}
onDismiss={hideVideo}
label={`${heading} video}`}
>
<h2 className={s.videoHeading}>{heading}</h2>
<div className={s.video}>
<ReactPlayer
url={`https://www.youtube.com/watch?v=${youtubeId}`}
width="100%"
height="100%"
playing={true}
controls={true}
/>
</div>
</IoDialog>
</>
)
}

View File

@ -0,0 +1,23 @@
import * as React from 'react'
export default function PlayIcon(): React.ReactElement {
return (
<svg
width="96"
height="96"
viewBox="0 0 96 96"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="48" cy="48" r="48" fill="#fff" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="m63.254 46.653-22.75-14.4a1.647 1.647 0 0 0-1.657-.057c-.522.28-.847.82-.847 1.405V62.4c0 .584.325 1.123.847 1.403a1.639 1.639 0 0 0 1.657-.057l22.75-14.4c.465-.294.746-.802.746-1.346 0-.545-.281-1.052-.746-1.347Z"
fill="#fff"
stroke="#000"
strokeWidth="2"
/>
</svg>
)
}

View File

@ -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;
}

View File

@ -1,14 +1,15 @@
import Subnav from '@hashicorp/react-subnav' import Subnav from '@hashicorp/react-subnav'
import subnavItems from '../../data/subnav'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import s from './style.module.css'
export default function ConsulSubnav() { export default function ConsulSubnav({ menuItems }) {
const router = useRouter() const router = useRouter()
return ( return (
<Subnav <Subnav
className={s.subnav}
hideGithubStars={true} hideGithubStars={true}
titleLink={{ titleLink={{
text: 'consul', text: 'HashiCorp Consul',
url: '/', url: '/',
}} }}
ctaLinks={[ ctaLinks={[
@ -22,11 +23,14 @@ export default function ConsulSubnav() {
text: 'Try HCP Consul', text: 'Try HCP Consul',
url: url:
'https://cloud.hashicorp.com/?utm_source=consul_io&utm_content=top_nav_consul', 'https://cloud.hashicorp.com/?utm_source=consul_io&utm_content=top_nav_consul',
theme: {
brand: 'consul',
},
}, },
]} ]}
currentPath={router.asPath} currentPath={router.asPath}
menuItemsAlign="right" menuItemsAlign="right"
menuItems={subnavItems} menuItems={menuItems}
constrainWidth constrainWidth
matchOnBasePath matchOnBasePath
/> />

View File

@ -0,0 +1,3 @@
.subnav {
border-top: 1px solid transparent;
}

View File

@ -679,8 +679,8 @@ $ curl \
## Force Leave and Shutdown ## Force Leave and Shutdown
This endpoint instructs the agent to force a node into the `left` state. If a This endpoint instructs the agent to force a node into the `left` state in the
node fails unexpectedly, then it will be in a `failed` state. Once 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 `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` belonging to that node will not be cleaned up. Forcing a node into the `left`
state allows its old entries to be removed. state allows its old entries to be removed.
@ -710,6 +710,14 @@ The table below shows this endpoint's support for
- `node` `(string: <required>)` - Specifies the name of the node to be forced into `left` state. This is specified as part of the URL. - `node` `(string: <required>)` - 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 ### Sample Request
```shell-session ```shell-session

View File

@ -769,7 +769,7 @@ The table below shows this endpoint's support for
### Parameters ### Parameters
- `service_id` `(string: <required>)` - Specifies the ID of the service to - `service_id` `(string: <required>)` - Specifies the ID of the service to
deregister. This is specifi### Parameters deregister. This is specified as part of the URL.
- `ns` `(string: "")` <EnterpriseAlert inline /> - Specifies the namespace in which - `ns` `(string: "")` <EnterpriseAlert inline /> - Specifies the namespace in which
to deregister the service. This value can be specified as the `ns` URL query to deregister the service. This value can be specified as the `ns` URL query

View File

@ -61,4 +61,7 @@ consul force-leave server1.us-east1
#### Command Options #### Command Options
- `-prune` - Removes failed or left agent from the list of - `-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.

View File

@ -7,31 +7,29 @@ description: >-
# Gateways # 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). - [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.
- Accept traffic from outside the Consul service mesh to services in the mesh with [ingress gateways](#ingress-gateways). - [Ingress gateways](#ingress-gateways) enable services to accept traffic from outside the Consul service mesh.
- Route traffic from services in the Consul service mesh to external services with [terminating gateways](#terminating-gateways). - [Terminating gateways](#terminating-gateways) enable you to route traffic from services in the Consul service mesh to external services.
## Mesh Gateways ## Mesh Gateways
-> **1.6.0+:** This feature is available in Consul versions 1.6.0 and newer. -> **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 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 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.
appropriate destination based on the server name requested. The data within the mTLS session is not decrypted by
the Gateway.
As of Consul 1.8.0, mesh gateways can also forward gossip and RPC traffic between Consul servers. Mesh gateways enable the following scenarios:
This is enabled by [WAN federation via mesh gateways](/docs/connect/gateways/wan-federation-via-mesh-gateways).
For more information about mesh gateways, review the [complete documentation](/docs/connect/gateways/mesh-gateway) * **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.
and the [mesh gateway tutorial](https://learn.hashicorp.com/tutorials/consul/service-mesh-gateways). * **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 ## Ingress Gateways

View File

@ -1,10 +1,9 @@
--- ---
layout: docs layout: docs
page_title: External <> Internal Services - Ingress Gateways page_title: Using Ingress Gateways to Connect External Traffic to Internal Services
description: >- description: >-
An ingress gateway enables ingress traffic from services outside the Consul This topic describes how ingress gateways enable traffic from external services to reach services inside the Consul service mesh.
service mesh to services inside the Consul service mesh. This section details It provides guidance on how to use Envoy and how to plug into your preferred gateway.
how to use Envoy and describes how you can plug in a gateway of your choice.
--- ---
# Ingress Gateways # Ingress Gateways

View File

@ -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"
}
},
]
}
}
```

View File

@ -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:<a name="mesh-architecture-diagram"/>
![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.
<CodeTabs heading="Example: Enabling gateways globally.">
```hcl
Kind = "proxy-defaults"
Name = "global"
MeshGateway {
Mode = "local"
}
```
```yaml
Kind: proxy-defaults
MeshGateway:
- Mode: local
Name: global
```
</CodeTabs>
### Enabling Gateways Per Service
The following `service-defaults` configuration will enable gateways for all Connect services with the name `web`.
<CodeTabs heading="Example: Enabling gateways per service.">
```hcl
Kind = "service-defaults"
Name = "web"
MeshGateway {
Mode = "local"
}
```
```yaml
Kind: service-defaults
MeshGateway:
- Mode: local
Name: web
```
</CodeTabs>
### 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.
<CodeTabs heading="Example: Enabling gateways for a service instance.">
```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
```
</CodeTabs>
### 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.
<CodeTabs heading="Example: Enabling gateways for a proxy 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
```
</CodeTabs>

View File

@ -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.
<CodeTabs heading="Example: Enabling gateways globally.">
```hcl
Kind = "proxy-defaults"
Name = "global"
MeshGateway {
Mode = "local"
}
```
```yaml
Kind: proxy-defaults
MeshGateway:
- Mode: local
Name: global
```
</CodeTabs>
### Enabling Gateways Per Service
The following `service-defaults` configuration will enable gateways for all Connect services with the name `web`.
<CodeTabs heading="Example: Enabling gateways per service.">
```hcl
Kind = "service-defaults"
Name = "web"
MeshGateway {
Mode = "local"
}
```
```yaml
Kind: service-defaults
MeshGateway:
- Mode: local
Name: web
```
</CodeTabs>
### 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.
<CodeTabs heading="Example: Enabling gateways for a service instance.">
```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
```
</CodeTabs>
### 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.
<CodeTabs heading="Example: Enabling gateways for a proxy upstream.">
```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
```
</CodeTabs>

View File

@ -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: 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:
<CodeTabs heading="clients.yaml"> <CodeTabs heading="client.yaml">
<CodeBlockConfig lineNumbers> <CodeBlockConfig lineNumbers>
```yaml ```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.: 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 ```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. 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.

View File

@ -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. ~> **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. 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` However, other providers can similarly be configured during initial bootstrap of the cluster
and `ca_provider` values for the provider you're using. 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) ## 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: And then reference it like this in the provider configuration:
```shell-session <CodeBlockConfig filename="vault-config.json" highlight="10">
$ cat vault-config.json
```json
{ {
"connect": [ "connect": [
{ {
@ -75,6 +76,8 @@ $ cat vault-config.json
} }
``` ```
</CodeBlockConfig>
This example configuration file is pointing to a Vault instance running in the same Kubernetes cluster, 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 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. 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 We will provide this secret and the Vault CA secret, to the Consul server via the
`server.extraVolumes` Helm value. `server.extraVolumes` Helm value.
<CodeBlockConfig filename="config.yaml" highlight="4-13">
```yaml ```yaml
global: global:
name: consul name: consul
@ -112,6 +117,8 @@ We will provide this secret and the Vault CA secret, to the Consul server via th
enabled: true enabled: true
``` ```
</CodeBlockConfig>
Finally, [install](/docs/k8s/installation/install#installing-consul) the Helm chart using the above config file: Finally, [install](/docs/k8s/installation/install#installing-consul) the Helm chart using the above config file:
```shell-session ```shell-session
@ -121,7 +128,7 @@ $ helm install consul -f config.yaml hashicorp/consul
Verify that the CA provider is set correctly: Verify that the CA provider is set correctly:
```shell-session ```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", "Provider": "vault",
"Config": { "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 You will similarly need to create a Vault token and a Kubernetes secret with
Vault's CA in each secondary Kubernetes cluster. Vault's CA in each secondary Kubernetes cluster.
<CodeBlockConfig highlight="7">
```json ```json
{ {
"connect": [ "connect": [
@ -168,6 +177,8 @@ Vault's CA in each secondary Kubernetes cluster.
} }
``` ```
</CodeBlockConfig>
Note that all secondary datacenters need to have access to the same Vault instance as the primary. Note that all secondary datacenters need to have access to the same Vault instance as the primary.
### Manually Rotating Vault Tokens ### 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 #### 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 #### Renewing Vault Token
To renew the Vault token, use the [`vault token renew`](https://www.vaultproject.io/docs/commands/token/renew) CLI command To renew the Vault token, use the [`vault token renew`](https://www.vaultproject.io/docs/commands/token/renew) CLI command
or API. or API.
[`ca_config`]: /docs/agent/options#connect_ca_config
[`ca_provider`]: /docs/agent/options#connect_ca_provider

View File

@ -38,9 +38,8 @@ injector:
- `global.tls.serverAdditionalIPSans` is not currently configurable and must be manually added to the server certificate in Vault. - `global.tls.serverAdditionalIPSans` is not currently configurable and must be manually added to the server certificate in Vault.
- Mesh gateway is not currently supported. - Mesh gateway is not currently supported.
- Multi-DC Federation 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 - 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.
expire it will be necessary to issue a `consul reload` on each server. - CA rotation is not currently supported through the Helm chart and must be manually rotated.
- CA rotation is not currently supported.
## Next Steps ## Next Steps

View File

@ -170,7 +170,7 @@ $ consul-k8s status
metrics: metrics:
defaultEnableMerging: true defaultEnableMerging: true
defaultEnabled: true defaultEnabled: true
enableGatewayMetrics: trueU enableGatewayMetrics: true
controller: controller:
enabled: true enabled: true
global: global:

View File

@ -69,16 +69,53 @@ If the ACL system becomes inoperable, you can follow the
### ACL Policies ### 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. ACL policies can have the following attributes:
- **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** - <EnterpriseAlert inline /> - The namespace this policy resides within. (Added in Consul Enterprise 1.7.0)
-> **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` | <EnterpriseAlert inline /> Namespace in which the policy is valid. Added in Consul Enterprise 1.7.0. | Optional | `default` |
| `partition` | <EnterpriseAlert inline /> 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 <token_id>
[
{
"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 #### Builtin Policies
@ -130,8 +167,7 @@ node_prefix "" {
The [API documentation for roles](/api/acl/roles#sample-payload) has some The [API documentation for roles](/api/acl/roles#sample-payload) has some
examples of using a service identity. examples of using a service identity.
-> **Consul Enterprise Namespacing** - Service Identity rules will be scoped to the single namespace that -> **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.
the corresponding ACL Token or Role resides within.
### ACL Node Identities ### 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. - **Service Identity Set** - The list of service identities that are applicable for the role.
- **Namespace** <EnterpriseAlert inline /> - The namespace this policy resides within. (Added in Consul Enterprise 1.7.0) - **Namespace** <EnterpriseAlert inline /> - 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
ACL tokens are used to determine if the caller is authorized to perform an action. An ACL token is composed of the following 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.:
elements:
- **Accessor ID** - The token's public identifier. - **Accessor ID** - The token's public identifier.
- **Secret ID** -The bearer token used when making requests to Consul. - **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. - **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) - **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) - **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 - **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.
the primary datacenter and globally replicated. - **CreateTime** - Timestamp indicating when the token was created.
- **Expiration Time** - The time at which this token is revoked. (Optional; Added in Consul 1.5.0) - **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** <EnterpriseAlert inline /> - The namespace this policy resides within. (Added in Consul Enterprise 1.7.0) - **Namespace** <EnterpriseAlert inline /> - The namespace in which the token resides. Added in Consul Enterprise 1.7.0.
- **Partition** <EnterpriseAlert inline /> - 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 -> **Linking Tokens to Policies in Consul Enterprise** - Tokens can only be linked to policies that are defined in the same namespace and admin partition.
the token itself.
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 <token_id>
[
{
"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 #### Builtin Tokens

View File

@ -277,24 +277,28 @@
"path": "connect/gateways" "path": "connect/gateways"
}, },
{ {
"title": "Connect Datacenters - Mesh Gateways", "title": "Mesh Gateways",
"routes": [ "routes": [
{
"title": "Overview",
"path": "connect/gateways/mesh-gateway"
},
{ {
"title": "WAN Federation", "title": "WAN Federation",
"path": "connect/gateways/mesh-gateway/wan-federation-via-mesh-gateways" "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" "path": "connect/gateways/ingress-gateway"
}, },
{ {
"title": "Internal <> External Services - Terminating Gateways", "title": "Terminating Gateways",
"path": "connect/gateways/terminating-gateway" "path": "connect/gateways/terminating-gateway"
} }
] ]

View File

@ -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',
},
]

View File

@ -1 +1 @@
export default '1.10.4' export default '1.11.1'

View File

@ -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 (
<>
<ProductSubnav
menuItems={[
{ text: 'Overview', url: '/' },
{
text: 'Use Cases',
submenu: [
{ text: 'Consul on Kubernetes', url: '/consul-on-kubernetes' },
...useCaseNavItems.map((item) => {
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}
<Footer openConsentManager={open} />
</>
)
}
StandardLayout.rivetParams = {
query,
dependencies: [],
}
interface Props {
children: React.ReactChildren
data: {
useCaseNavItems: Array<{ url: string; text: string }>
}
}

View File

@ -0,0 +1,6 @@
query UseCasesQuery {
useCaseNavItems: allConsulUseCases {
url: slug
text: heroHeading
}
}

11
website/lib/utils.ts Normal file
View File

@ -0,0 +1,11 @@
export const isInternalLink = (link: string): boolean => {
if (
link.startsWith('/') ||
link.startsWith('#') ||
link.startsWith('https://consul.io') ||
link.startsWith('https://www.consul.io')
) {
return true
}
return false
}

View File

@ -2,7 +2,12 @@ const withHashicorp = require('@hashicorp/platform-nextjs-plugin')
const redirects = require('./redirects.next') const redirects = require('./redirects.next')
module.exports = withHashicorp({ module.exports = withHashicorp({
dato: {
// This token is safe to be in this public repository, it only has access to content that is publicly viewable on the website
token: '88b4984480dad56295a8aadae6caad',
},
nextOptimizedImages: true, nextOptimizedImages: true,
transpileModules: ['@hashicorp/flight-icons'],
})({ })({
svgo: { plugins: [{ removeViewBox: false }] }, svgo: { plugins: [{ removeViewBox: false }] },
rewrites: () => [ rewrites: () => [
@ -19,4 +24,8 @@ module.exports = withHashicorp({
BUGSNAG_CLIENT_KEY: '01625078d856ef022c88f0c78d2364f1', BUGSNAG_CLIENT_KEY: '01625078d856ef022c88f0c78d2364f1',
BUGSNAG_SERVER_KEY: 'be8ed0d0fc887d547284cce9e98e60e5', BUGSNAG_SERVER_KEY: 'be8ed0d0fc887d547284cce9e98e60e5',
}, },
images: {
domains: ['www.datocms-assets.com'],
disableStaticImages: true,
},
}) })

7296
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,10 @@
"version": "0.0.1", "version": "0.0.1",
"author": "HashiCorp", "author": "HashiCorp",
"dependencies": { "dependencies": {
"@hashicorp/flight-icons": "^1.3.0",
"@hashicorp/mktg-global-styles": "^4.0.0", "@hashicorp/mktg-global-styles": "^4.0.0",
"@hashicorp/mktg-logos": "^1.2.0", "@hashicorp/mktg-logos": "^1.2.0",
"@hashicorp/nextjs-scripts": "^19.0.3",
"@hashicorp/platform-analytics": "^0.2.0", "@hashicorp/platform-analytics": "^0.2.0",
"@hashicorp/platform-code-highlighting": "^0.1.2", "@hashicorp/platform-code-highlighting": "^0.1.2",
"@hashicorp/platform-runtime-error-monitoring": "^0.1.0", "@hashicorp/platform-runtime-error-monitoring": "^0.1.0",
@ -28,24 +30,27 @@
"@hashicorp/react-inline-svg": "^6.0.3", "@hashicorp/react-inline-svg": "^6.0.3",
"@hashicorp/react-learn-callout": "^2.0.1", "@hashicorp/react-learn-callout": "^2.0.1",
"@hashicorp/react-markdown-page": "^1.4.3", "@hashicorp/react-markdown-page": "^1.4.3",
"@hashicorp/react-product-downloads-page": "^2.5.2", "@hashicorp/react-product-downloads-page": "^2.5.3",
"@hashicorp/react-product-features-list": "^5.0.0", "@hashicorp/react-product-features-list": "^5.0.0",
"@hashicorp/react-search": "^6.1.1", "@hashicorp/react-search": "^6.1.1",
"@hashicorp/react-section-header": "^5.0.4", "@hashicorp/react-section-header": "^5.0.4",
"@hashicorp/react-stepped-feature-list": "^4.0.3", "@hashicorp/react-stepped-feature-list": "^4.0.3",
"@hashicorp/react-subnav": "^9.2.2", "@hashicorp/react-subnav": "^9.3.2",
"@hashicorp/react-tabs": "^7.0.1", "@hashicorp/react-tabs": "^7.0.1",
"@hashicorp/react-text-split": "^4.0.0", "@hashicorp/react-text-split": "^4.0.0",
"@hashicorp/react-text-split-with-code": "^3.3.8", "@hashicorp/react-text-split-with-code": "^3.3.8",
"@hashicorp/react-text-split-with-image": "^4.2.5", "@hashicorp/react-text-split-with-image": "^4.2.5",
"@hashicorp/react-use-cases": "^5.0.0", "@hashicorp/react-use-cases": "^5.0.0",
"@hashicorp/react-vertical-text-block-list": "^7.0.0", "@hashicorp/react-vertical-text-block-list": "^7.0.0",
"@reach/dialog": "^0.16.2",
"ci": "^2.1.1", "ci": "^2.1.1",
"framer-motion": "^5.3.3",
"next": "^11.1.2", "next": "^11.1.2",
"next-mdx-remote": "3.0.1", "next-mdx-remote": "3.0.1",
"next-remote-watch": "1.0.0", "next-remote-watch": "1.0.0",
"nuka-carousel": "4.7.7", "nuka-carousel": "4.7.7",
"react": "^17.0.2", "react": "^17.0.2",
"react-datocms": "^1.6.6",
"react-device-detect": "1.17.0", "react-device-detect": "1.17.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-player": "^2.9.0" "react-player": "^2.9.0"

View File

@ -4,27 +4,28 @@ import '@hashicorp/platform-util/nprogress/style.css'
import useFathomAnalytics from '@hashicorp/platform-analytics' import useFathomAnalytics from '@hashicorp/platform-analytics'
import Router from 'next/router' import Router from 'next/router'
import Head from 'next/head' import Head from 'next/head'
import rivetQuery from '@hashicorp/nextjs-scripts/dato/client'
import NProgress from '@hashicorp/platform-util/nprogress' import NProgress from '@hashicorp/platform-util/nprogress'
import { ErrorBoundary } from '@hashicorp/platform-runtime-error-monitoring' import { ErrorBoundary } from '@hashicorp/platform-runtime-error-monitoring'
import createConsentManager from '@hashicorp/react-consent-manager/loader' import createConsentManager from '@hashicorp/react-consent-manager/loader'
import useAnchorLinkAnalytics from '@hashicorp/platform-util/anchor-link-analytics' import useAnchorLinkAnalytics from '@hashicorp/platform-util/anchor-link-analytics'
import HashiHead from '@hashicorp/react-head' import HashiHead from '@hashicorp/react-head'
import HashiStackMenu from '@hashicorp/react-hashi-stack-menu'
import AlertBanner from '@hashicorp/react-alert-banner' import AlertBanner from '@hashicorp/react-alert-banner'
import Footer from '../components/footer'
import ProductSubnav from '../components/subnav'
import alertBannerData, { ALERT_BANNER_ACTIVE } from '../data/alert-banner' import alertBannerData, { ALERT_BANNER_ACTIVE } from '../data/alert-banner'
import Error from './_error' import Error from './_error'
import StandardLayout from 'layouts/standard'
NProgress({ Router }) NProgress({ Router })
const { ConsentManager, openConsentManager } = createConsentManager({ const { ConsentManager } = createConsentManager({
preset: 'oss', preset: 'oss',
}) })
export default function App({ Component, pageProps }) { export default function App({ Component, pageProps, layoutData }) {
useFathomAnalytics() useFathomAnalytics()
useAnchorLinkAnalytics() useAnchorLinkAnalytics()
const Layout = Component.layout ?? StandardLayout
return ( return (
<ErrorBoundary FallbackComponent={Error}> <ErrorBoundary FallbackComponent={Error}>
<HashiHead <HashiHead
@ -44,13 +45,27 @@ export default function App({ Component, pageProps }) {
{ALERT_BANNER_ACTIVE && ( {ALERT_BANNER_ACTIVE && (
<AlertBanner {...alertBannerData} product="consul" hideOnMobile /> <AlertBanner {...alertBannerData} product="consul" hideOnMobile />
)} )}
<HashiStackMenu /> <Layout {...(layoutData && { data: layoutData })}>
<ProductSubnav /> <div className="content">
<div className="content"> <Component {...pageProps} />
<Component {...pageProps} /> </div>
</div> </Layout>
<Footer openConsentManager={openConsentManager} />
<ConsentManager /> <ConsentManager />
</ErrorBoundary> </ErrorBoundary>
) )
} }
App.getInitialProps = async ({ Component, ctx }) => {
const layoutQuery = Component.layout
? Component.layout?.rivetParams ?? null
: StandardLayout.rivetParams
const layoutData = layoutQuery ? await rivetQuery(layoutQuery) : null
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps, layoutData }
}

View File

@ -9,7 +9,7 @@ export default class MyDocument extends Document {
render() { render() {
return ( return (
<Html> <Html lang="en">
<Head> <Head>
<HashiHead /> <HashiHead />
</Head> </Head>

BIN
website/pages/home/img/cloud/hcp.jpg (Stored with Git LFS)

Binary file not shown.

BIN
website/pages/home/img/cloud/hcs.jpg (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@ -1 +0,0 @@
<svg width="50" height="49" viewBox="0 0 50 49" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M39.3 25.03l5.64-5.17-5.64-5.17M20.03 34.9l5.17 5.64 5.17-5.64M11.1 14.69l-5.64 5.17 5.64 5.17" stroke="#000" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M26.45 2a1.25 1.25 0 00-2.5 0h2.5zM6.4 21.11h18.8v-2.5H6.4v2.5zm18.8 0H44v-2.5H25.2v2.5zm-1.25-1.25V39.6h2.5V19.86h-2.5zm2.5 0V2h-2.5v17.86h2.5z" fill="#000"/></svg>

Before

Width:  |  Height:  |  Size: 456 B

View File

@ -1 +0,0 @@
<svg width="51" height="51" viewBox="0 0 51 51" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M23.46 21.044l.31-5.557a10.707 10.707 0 00-6.291 3.058l4.489 3.23.009-.004c.152.113.339.179.542.179a.926.926 0 00.919-.894l.022-.012zM32.639 18.548a10.78 10.78 0 00-6.253-3.06l.31 5.547.004.002c.008.191.074.38.2.541a.912.912 0 001.263.172l.015.007 4.46-3.21zM19.95 24.342l-4.1-3.721a11.043 11.043 0 00-1.524 6.906l5.253-1.54.005-.017a.92.92 0 00.477-.32.943.943 0 00-.116-1.285l.005-.023zM35.618 23.957a11.139 11.139 0 00-1.345-3.334l-4.076 3.703.002.012a.929.929 0 00-.292.495c-.11.49.18.978.653 1.11l.005.022 5.28 1.544a11.19 11.19 0 00-.227-3.552zM25.915 24.63h-1.68l-1.045 1.322.375 1.652 1.512.738 1.507-.736.375-1.652-1.044-1.324zM29.831 29.177a.905.905 0 00-.212-.016.909.909 0 00-.352.093.94.94 0 00-.446 1.21l-.007.01 2.11 5.172a10.898 10.898 0 004.35-5.548l-5.434-.932-.009.011zM21.375 29.91a.924.924 0 00-1.064-.71l-.009-.012-5.388.928a10.94 10.94 0 004.338 5.51l2.087-5.12-.016-.02a.938.938 0 00.052-.576zM25.474 31.52a.9.9 0 00-.43-.093.92.92 0 00-.78.493h-.004l-2.649 4.862a10.64 10.64 0 005.89.308 10.9 10.9 0 001.061-.3l-2.656-4.872h-.02a.921.921 0 00-.412-.398z" fill="#000"/><path fill-rule="evenodd" clip-rule="evenodd" d="M23.626 1.135a3.317 3.317 0 012.895 0L43.96 9.586a3.366 3.366 0 011.804 2.277l4.308 18.996a3.378 3.378 0 01-.644 2.84L37.363 48.934a3.316 3.316 0 01-2.607 1.26l-19.354.005a3.317 3.317 0 01-2.607-1.263L.726 33.705a3.37 3.37 0 01-.642-2.84l4.302-18.997A3.338 3.338 0 016.19 9.591l17.436-8.456zm.407 7.313c0-.65.467-1.177 1.044-1.177.576 0 1.043.527 1.043 1.177l.002.107c.001.068.003.139 0 .194-.009.247-.047.456-.086.67-.02.109-.04.22-.056.337l-.007.056c-.085.715-.156 1.31-.111 1.866.035.246.162.363.284.476l.057.053c.003.077.015.332.024.474 3.319.299 6.4 1.84 8.664 4.248l.397-.287c.016 0 .034.002.054.004.15.011.4.03.58-.074.455-.312.871-.741 1.37-1.255l.036-.038c.079-.085.15-.168.222-.25.142-.165.28-.327.467-.49.047-.04.11-.09.168-.137l.064-.05c.501-.406 1.197-.363 1.557.094.36.457.244 1.157-.257 1.562l-.075.063c-.054.044-.11.091-.154.125-.195.146-.38.245-.567.346a7.559 7.559 0 00-.3.168h-.002c-.623.391-1.14.715-1.55 1.107-.169.182-.18.356-.191.523a3.148 3.148 0 01-.006.076l-.158.145c-.074.067-.156.14-.212.193a13.819 13.819 0 011.955 4.588c.379 1.67.438 3.34.219 4.945l.421.125.03.044c.084.124.228.34.421.415.532.17 1.13.234 1.85.31l.021.003c.118.01.23.014.342.019.212.008.421.017.66.062.053.01.123.028.19.045l.113.028c.615.15 1.01.724.883 1.29-.127.566-.728.91-1.347.774l-.008-.001a.08.08 0 01-.008-.001l-.02-.007-.1-.02a2.895 2.895 0 01-.17-.039 4.26 4.26 0 01-.622-.234c-.102-.045-.205-.09-.314-.133l-.032-.011c-.676-.246-1.24-.451-1.788-.532-.244-.02-.385.08-.52.175l-.064.045a15.51 15.51 0 00-.446-.08c-1 3.19-3.13 5.953-6.017 7.683.016.039.036.092.056.148.043.119.091.248.118.28l-.029.074c-.06.154-.124.313-.051.55.202.534.53 1.055.925 1.682.066.1.131.19.197.281.126.174.25.345.363.563a5.468 5.468 0 01.136.286c.268.582.071 1.253-.444 1.505-.52.254-1.165-.014-1.443-.6a8.499 8.499 0 00-.039-.08c-.032-.066-.066-.137-.09-.192a4.523 4.523 0 01-.21-.644c-.03-.107-.058-.214-.093-.327l-.01-.029c-.233-.691-.426-1.266-.706-1.752-.138-.207-.303-.257-.461-.306a3.562 3.562 0 01-.072-.022l-.104-.19-.12-.218a13.298 13.298 0 01-9.631-.025l-.236.435c-.176.048-.346.096-.45.222-.267.324-.421.785-.585 1.275-.07.207-.14.42-.223.629-.035.114-.065.223-.094.332-.055.208-.11.413-.207.639a4.149 4.149 0 01-.086.18 8.18 8.18 0 00-.043.089v.002l-.002.002c-.279.584-.923.851-1.441.598-.515-.252-.712-.923-.444-1.505.015-.03.03-.066.047-.103.03-.064.06-.132.087-.182a4.46 4.46 0 01.365-.568c.065-.09.13-.18.195-.279.395-.627.742-1.19.944-1.723.051-.178-.024-.42-.092-.6l.19-.461a13.749 13.749 0 01-6.02-7.628l-.455.08-.046-.028c-.131-.077-.347-.204-.553-.188-.55.08-1.113.286-1.79.532l-.03.011c-.108.042-.21.086-.31.13-.197.086-.39.17-.627.235a3.955 3.955 0 01-.27.06.066.066 0 00-.01.004.088.088 0 01-.01.004c-.004 0-.01 0-.015.002-.62.135-1.22-.209-1.347-.774-.128-.566.268-1.14.883-1.29l.015-.005.005-.001a.68.68 0 01.004-.001l.084-.02c.068-.017.14-.035.195-.046.238-.046.447-.054.66-.062.111-.005.223-.01.341-.02l.024-.002c.72-.076 1.316-.14 1.847-.31.15-.062.296-.255.407-.404.014-.02.028-.038.042-.055l.437-.13a13.871 13.871 0 012.13-9.556l-.335-.303-.008-.05c-.021-.148-.059-.407-.202-.561-.41-.391-.928-.716-1.552-1.107-.102-.06-.2-.114-.298-.167a4.27 4.27 0 01-.567-.347c-.045-.034-.101-.08-.155-.125l-.061-.051a.17.17 0 00-.008-.006l-.008-.006c-.5-.405-.616-1.104-.256-1.561a1 1 0 01.831-.374c.25.009.508.101.727.278l.063.05c.059.047.123.098.17.138.185.162.322.323.462.486.072.084.144.169.224.255l.018.018c.507.523.928.956 1.39 1.272.212.125.382.098.545.072l.074-.011c.06.045.262.192.377.271a13.38 13.38 0 018.706-4.248l.025-.448c.139-.137.294-.333.339-.548.045-.566-.028-1.174-.115-1.907l-.001-.015c-.017-.117-.037-.228-.057-.338a4.482 4.482 0 01-.086-.67 3.557 3.557 0 01.002-.27l-.001-.015-.001-.015z" fill="#000"/></svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -1 +0,0 @@
<svg id="LOGOS" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 162.34 162.34"><path d="M23.48,24.42l57.39,115.2,57.81-115.2ZM87.7,47.54h6.68v6.68H87.7ZM74.4,74.25H67.72V67.57H74.4Zm0-10H67.72V57.55H74.4Zm0-10H67.72V47.54H74.4Zm10,30.05H77.74V77.59h6.67Zm0-10H77.74V67.57h6.67Zm0-10H77.74V57.55h6.67Zm0-10H77.74V47.54h6.67Zm3.29,3.33h6.68v6.68H87.7Zm0,16.7V67.57h6.68v6.68Z"/></svg>

Before

Width:  |  Height:  |  Size: 382 B

Some files were not shown because too many files have changed in this diff Show More