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:
commit
5835d18664
|
@ -0,0 +1,4 @@
|
||||||
|
```release-note:bug
|
||||||
|
ui: Differentiate between Service Meta and Node Meta when choosing search fields
|
||||||
|
in Service Instance listings
|
||||||
|
```
|
|
@ -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.
|
||||||
|
```
|
|
@ -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
|
||||||
|
```
|
|
@ -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
|
||||||
|
```
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
ui: Ensure a login buttons appear for some error states, plus text amends
|
||||||
|
```
|
|
@ -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
|
|
@ -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
|
|
@ -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)) })
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 |
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 || '',
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)}}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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({}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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'),
|
||||||
|
},
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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 |
|
@ -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);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
.subnav {
|
||||||
|
border-top: 1px solid transparent;
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -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',
|
|
||||||
},
|
|
||||||
]
|
|
|
@ -1 +1 @@
|
||||||
export default '1.10.4'
|
export default '1.11.1'
|
||||||
|
|
|
@ -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 }>
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
query UseCasesQuery {
|
||||||
|
useCaseNavItems: allConsulUseCases {
|
||||||
|
url: slug
|
||||||
|
text: heroHeading
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||||
|
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
|
|
@ -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)
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)
BIN
website/pages/home/img/cloud/hcs.jpg (Stored with Git LFS)
Binary file not shown.
BIN
website/pages/home/img/extend-mesh-transparent.png (Stored with Git LFS)
BIN
website/pages/home/img/extend-mesh-transparent.png (Stored with Git LFS)
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 |
|
@ -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 |
|
@ -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 |
|
@ -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
Loading…
Reference in New Issue