From db5f4eaadc9f2b91302623ac040d18f4e038d426 Mon Sep 17 00:00:00 2001 From: Marco Molteni Date: Wed, 8 Dec 2021 20:16:36 +0100 Subject: [PATCH 01/23] cli: consul tls: create private keys with mode 0600 This applies to consul tls ca create consul tls cert create -client consul tls cert create -server Closes: #11741 --- command/tls/ca/create/tls_ca_create.go | 2 +- command/tls/ca/create/tls_ca_create_test.go | 9 +++++++++ command/tls/cert/create/tls_cert_create.go | 2 +- command/tls/cert/create/tls_cert_create_test.go | 9 +++++++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/command/tls/ca/create/tls_ca_create.go b/command/tls/ca/create/tls_ca_create.go index ceef70b37..810d452c4 100644 --- a/command/tls/ca/create/tls_ca_create.go +++ b/command/tls/ca/create/tls_ca_create.go @@ -83,7 +83,7 @@ func (c *cmd) Run(args []string) int { } c.UI.Output("==> Saved " + certFileName) - if err := file.WriteAtomicWithPerms(pkFileName, []byte(pk), 0755, 0666); err != nil { + if err := file.WriteAtomicWithPerms(pkFileName, []byte(pk), 0755, 0600); err != nil { c.UI.Error(err.Error()) return 1 } diff --git a/command/tls/ca/create/tls_ca_create_test.go b/command/tls/ca/create/tls_ca_create_test.go index 568958959..19c5fb965 100644 --- a/command/tls/ca/create/tls_ca_create_test.go +++ b/command/tls/ca/create/tls_ca_create_test.go @@ -3,6 +3,7 @@ package create import ( "crypto" "crypto/x509" + "io/fs" "io/ioutil" "os" "strings" @@ -120,6 +121,14 @@ func expectFiles(t *testing.T, caPath, keyPath string) (*x509.Certificate, crypt require.FileExists(t, caPath) require.FileExists(t, keyPath) + fi, err := os.Stat(keyPath) + if err != nil { + t.Fatal("should not happen", err) + } + if want, have := fs.FileMode(0600), fi.Mode().Perm(); want != have { + t.Fatalf("private key file %s: permissions: want: %o; have: %o", keyPath, want, have) + } + caData, err := ioutil.ReadFile(caPath) require.NoError(t, err) keyData, err := ioutil.ReadFile(keyPath) diff --git a/command/tls/cert/create/tls_cert_create.go b/command/tls/cert/create/tls_cert_create.go index 6281ca3ae..b1cdaa131 100644 --- a/command/tls/cert/create/tls_cert_create.go +++ b/command/tls/cert/create/tls_cert_create.go @@ -196,7 +196,7 @@ func (c *cmd) Run(args []string) int { } c.UI.Output("==> Saved " + certFileName) - if err := file.WriteAtomicWithPerms(pkFileName, []byte(priv), 0755, 0666); err != nil { + if err := file.WriteAtomicWithPerms(pkFileName, []byte(priv), 0755, 0600); err != nil { c.UI.Error(err.Error()) return 1 } diff --git a/command/tls/cert/create/tls_cert_create_test.go b/command/tls/cert/create/tls_cert_create_test.go index 306eed8df..78f75eb11 100644 --- a/command/tls/cert/create/tls_cert_create_test.go +++ b/command/tls/cert/create/tls_cert_create_test.go @@ -3,6 +3,7 @@ package create import ( "crypto" "crypto/x509" + "io/fs" "io/ioutil" "net" "os" @@ -242,6 +243,14 @@ func expectFiles(t *testing.T, certPath, keyPath string) (*x509.Certificate, cry require.FileExists(t, certPath) require.FileExists(t, keyPath) + fi, err := os.Stat(keyPath) + if err != nil { + t.Fatal("should not happen", err) + } + if want, have := fs.FileMode(0600), fi.Mode().Perm(); want != have { + t.Fatalf("private key file %s: permissions: want: %o; have: %o", keyPath, want, have) + } + certData, err := ioutil.ReadFile(certPath) require.NoError(t, err) keyData, err := ioutil.ReadFile(keyPath) From 74e92316dea8f062c37d32379d7e7a02796bb8d8 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 9 Dec 2021 18:57:22 -0500 Subject: [PATCH 02/23] testing: remove old config.Build version DefaultConfig already sets the version to version.Version, so by removing this our tests will run with the version that matches the code. --- agent/consul/server_test.go | 2 -- agent/consul/system_metadata_test.go | 23 +++++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/agent/consul/server_test.go b/agent/consul/server_test.go index d608b8fa0..da24f3220 100644 --- a/agent/consul/server_test.go +++ b/agent/consul/server_test.go @@ -164,8 +164,6 @@ func testServerConfig(t *testing.T) (string, *Config) { config.ServerHealthInterval = 50 * time.Millisecond config.AutopilotInterval = 100 * time.Millisecond - config.Build = "1.7.2" - config.CoordinateUpdatePeriod = 100 * time.Millisecond config.LeaveDrainTime = 1 * time.Millisecond diff --git a/agent/consul/system_metadata_test.go b/agent/consul/system_metadata_test.go index 30f57defd..61f62c8d1 100644 --- a/agent/consul/system_metadata_test.go +++ b/agent/consul/system_metadata_test.go @@ -4,9 +4,10 @@ import ( "os" "testing" + "github.com/stretchr/testify/require" + "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/testrpc" - "github.com/stretchr/testify/require" ) func TestLeader_SystemMetadata_CRUD(t *testing.T) { @@ -32,10 +33,10 @@ func TestLeader_SystemMetadata_CRUD(t *testing.T) { state := srv.fsm.State() - // Initially has no entries + // Initially has one entry for virtual-ips feature flag _, entries, err := state.SystemMetadataList(nil) require.NoError(t, err) - require.Len(t, entries, 0) + require.Len(t, entries, 1) // Create 3 require.NoError(t, srv.setSystemMetadataKey("key1", "val1")) @@ -52,12 +53,13 @@ func TestLeader_SystemMetadata_CRUD(t *testing.T) { _, entries, err = state.SystemMetadataList(nil) require.NoError(t, err) - require.Len(t, entries, 3) + require.Len(t, entries, 4) require.Equal(t, map[string]string{ - "key1": "val1", - "key2": "val2", - "key3": "", + structs.SystemMetadataVirtualIPsEnabled: "true", + "key1": "val1", + "key2": "val2", + "key3": "", }, mapify(entries)) // Update one and delete one. @@ -66,10 +68,11 @@ func TestLeader_SystemMetadata_CRUD(t *testing.T) { _, entries, err = state.SystemMetadataList(nil) require.NoError(t, err) - require.Len(t, entries, 2) + require.Len(t, entries, 3) require.Equal(t, map[string]string{ - "key2": "val2", - "key3": "val3", + structs.SystemMetadataVirtualIPsEnabled: "true", + "key2": "val2", + "key3": "val3", }, mapify(entries)) } From 6444d1d4b3aedb3582eb623b65b7544ed2c0975f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 9 Dec 2021 19:08:40 -0500 Subject: [PATCH 03/23] testing: Deprecate functions for creating a server. These helper functions actually end up hiding important setup details that should be visible from the test case. We already have a convenient way of setting this config when calling newTestServerWithConfig. --- agent/consul/autopilot_test.go | 9 ++-- agent/consul/intention_endpoint_test.go | 2 +- agent/consul/internal_endpoint_test.go | 2 +- agent/consul/server_test.go | 60 ++++++++++--------------- 4 files changed, 31 insertions(+), 42 deletions(-) diff --git a/agent/consul/autopilot_test.go b/agent/consul/autopilot_test.go index 8b2632411..1935fc5e8 100644 --- a/agent/consul/autopilot_test.go +++ b/agent/consul/autopilot_test.go @@ -6,12 +6,13 @@ import ( "testing" "time" - "github.com/hashicorp/consul/agent/structs" - "github.com/hashicorp/consul/sdk/testutil/retry" - "github.com/hashicorp/consul/testrpc" "github.com/hashicorp/raft" "github.com/hashicorp/serf/serf" "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/sdk/testutil/retry" + "github.com/hashicorp/consul/testrpc" ) func TestAutopilot_IdempotentShutdown(t *testing.T) { @@ -19,7 +20,7 @@ func TestAutopilot_IdempotentShutdown(t *testing.T) { t.Skip("too slow for testing.Short") } - dir1, s1 := testServerWithConfig(t, nil) + dir1, s1 := testServerWithConfig(t) defer os.RemoveAll(dir1) defer s1.Shutdown() retry.Run(t, func(r *retry.R) { r.Check(waitForLeader(s1)) }) diff --git a/agent/consul/intention_endpoint_test.go b/agent/consul/intention_endpoint_test.go index ec3941348..ec8349f82 100644 --- a/agent/consul/intention_endpoint_test.go +++ b/agent/consul/intention_endpoint_test.go @@ -1599,7 +1599,7 @@ func TestIntentionList_acl(t *testing.T) { t.Parallel() - dir1, s1 := testServerWithConfig(t, testServerACLConfig(nil)) + dir1, s1 := testServerWithConfig(t, testServerACLConfig) defer os.RemoveAll(dir1) defer s1.Shutdown() codec := rpcClient(t, s1) diff --git a/agent/consul/internal_endpoint_test.go b/agent/consul/internal_endpoint_test.go index f9105304f..7a354e7b5 100644 --- a/agent/consul/internal_endpoint_test.go +++ b/agent/consul/internal_endpoint_test.go @@ -1784,7 +1784,7 @@ func TestInternal_GatewayIntentions_aclDeny(t *testing.T) { t.Skip("too slow for testing.Short") } - dir1, s1 := testServerWithConfig(t, testServerACLConfig(nil)) + dir1, s1 := testServerWithConfig(t, testServerACLConfig) defer os.RemoveAll(dir1) defer s1.Shutdown() codec := rpcClient(t, s1) diff --git a/agent/consul/server_test.go b/agent/consul/server_test.go index da24f3220..0c7b84223 100644 --- a/agent/consul/server_test.go +++ b/agent/consul/server_test.go @@ -66,21 +66,12 @@ func testTLSCertificates(serverName string) (cert string, key string, cacert str return cert, privateKey, ca, nil } -// testServerACLConfig wraps another arbitrary Config altering callback -// to setup some common ACL configurations. A new callback func will -// be returned that has the original callback invoked after setting -// up all of the ACL configurations (so they can still be overridden) -func testServerACLConfig(cb func(*Config)) func(*Config) { - return func(c *Config) { - c.PrimaryDatacenter = "dc1" - c.ACLsEnabled = true - c.ACLInitialManagementToken = TestDefaultMasterToken - c.ACLResolverSettings.ACLDefaultPolicy = "deny" - - if cb != nil { - cb(c) - } - } +// testServerACLConfig setup some common ACL configurations. +func testServerACLConfig(c *Config) { + c.PrimaryDatacenter = "dc1" + c.ACLsEnabled = true + c.ACLInitialManagementToken = TestDefaultMasterToken + c.ACLResolverSettings.ACLDefaultPolicy = "deny" } func configureTLS(config *Config) { @@ -185,14 +176,12 @@ func testServerConfig(t *testing.T) (string, *Config) { return dir, config } +// Deprecated: use testServerWithConfig instead. It does the same thing and more. func testServer(t *testing.T) (string, *Server) { - return testServerWithConfig(t, func(c *Config) { - c.Datacenter = "dc1" - c.PrimaryDatacenter = "dc1" - c.Bootstrap = true - }) + return testServerWithConfig(t) } +// Deprecated: use testServerWithConfig func testServerDC(t *testing.T, dc string) (string, *Server) { return testServerWithConfig(t, func(c *Config) { c.Datacenter = dc @@ -200,6 +189,7 @@ func testServerDC(t *testing.T, dc string) (string, *Server) { }) } +// Deprecated: use testServerWithConfig func testServerDCBootstrap(t *testing.T, dc string, bootstrap bool) (string, *Server) { return testServerWithConfig(t, func(c *Config) { c.Datacenter = dc @@ -208,6 +198,7 @@ func testServerDCBootstrap(t *testing.T, dc string, bootstrap bool) (string, *Se }) } +// Deprecated: use testServerWithConfig func testServerDCExpect(t *testing.T, dc string, expect int) (string, *Server) { return testServerWithConfig(t, func(c *Config) { c.Datacenter = dc @@ -216,16 +207,7 @@ func testServerDCExpect(t *testing.T, dc string, expect int) (string, *Server) { }) } -func testServerDCExpectNonVoter(t *testing.T, dc string, expect int) (string, *Server) { - return testServerWithConfig(t, func(c *Config) { - c.Datacenter = dc - c.Bootstrap = false - c.BootstrapExpect = expect - c.ReadReplica = true - }) -} - -func testServerWithConfig(t *testing.T, cb func(*Config)) (string, *Server) { +func testServerWithConfig(t *testing.T, configOpts ...func(*Config)) (string, *Server) { var dir string var srv *Server @@ -233,8 +215,8 @@ func testServerWithConfig(t *testing.T, cb func(*Config)) (string, *Server) { retry.RunWith(retry.ThreeTimes(), t, func(r *retry.R) { var config *Config dir, config = testServerConfig(t) - if cb != nil { - cb(config) + for _, fn := range configOpts { + fn(config) } // Apply config to copied fields because many tests only set the old @@ -255,8 +237,11 @@ func testServerWithConfig(t *testing.T, cb func(*Config)) (string, *Server) { // cb is a function that can alter the test servers configuration prior to the server starting. func testACLServerWithConfig(t *testing.T, cb func(*Config), initReplicationToken bool) (string, *Server, rpc.ClientCodec) { - dir, srv := testServerWithConfig(t, testServerACLConfig(cb)) - t.Cleanup(func() { srv.Shutdown() }) + opts := []func(*Config){testServerACLConfig} + if cb != nil { + opts = append(opts, cb) + } + dir, srv := testServerWithConfig(t, opts...) if initReplicationToken { // setup some tokens here so we get less warnings in the logs @@ -264,7 +249,6 @@ func testACLServerWithConfig(t *testing.T, cb func(*Config), initReplicationToke } codec := rpcClient(t, srv) - t.Cleanup(func() { codec.Close() }) return dir, srv, codec } @@ -1282,7 +1266,11 @@ func TestServer_Expect_NonVoters(t *testing.T) { } t.Parallel() - dir1, s1 := testServerDCExpectNonVoter(t, "dc1", 2) + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.Bootstrap = false + c.BootstrapExpect = 2 + c.ReadReplica = true + }) defer os.RemoveAll(dir1) defer s1.Shutdown() From 478e9882060e21db02768a2affb9c823fed5d62e Mon Sep 17 00:00:00 2001 From: trujillo-adam Date: Wed, 15 Dec 2021 13:11:52 -0800 Subject: [PATCH 04/23] clarify mesh gateway docs use cases; include admin partition flow --- .../content/docs/connect/gateways/index.mdx | 26 +-- .../docs/connect/gateways/ingress-gateway.mdx | 7 +- .../connect/gateways/mesh-gateway/index.mdx | 215 ------------------ ...service-to-service-traffic-datacenters.mdx | 205 +++++++++++++++++ .../service-to-service-traffic-partitions.mdx | 173 ++++++++++++++ website/data/docs-nav-data.json | 18 +- 6 files changed, 404 insertions(+), 240 deletions(-) delete mode 100644 website/content/docs/connect/gateways/mesh-gateway/index.mdx create mode 100644 website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters.mdx create mode 100644 website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx diff --git a/website/content/docs/connect/gateways/index.mdx b/website/content/docs/connect/gateways/index.mdx index 31fdb4f10..dafdcb092 100644 --- a/website/content/docs/connect/gateways/index.mdx +++ b/website/content/docs/connect/gateways/index.mdx @@ -7,31 +7,29 @@ description: >- # Gateways -Gateways provide connectivity into, out of, and between Consul service meshes. +This topic provides an overview of the gateway features shipped with Consul. Gateways provide connectivity into, out of, and between Consul service meshes. You can configure the following types of gateways: -- Enable service-to-service traffic between Consul datacenters with [mesh gateways](#mesh-gateways). -- Accept traffic from outside the Consul service mesh to services in the mesh with [ingress gateways](#ingress-gateways). -- Route traffic from services in the Consul service mesh to external services with [terminating gateways](#terminating-gateways). +- [Mesh gateways](#mesh-gateways) enable service-to-service traffic between Consul datacenters or between Consul admin partitions. They also enable datacenters to be federated across wide area networks. +- [Ingress gateways](#ingress-gateways) enable services to accept traffic from outside the Consul service mesh. +- [Terminating gateways](#terminating-gateways) enable you to route traffic from services in the Consul service mesh to external services. ## Mesh Gateways -> **1.6.0+:** This feature is available in Consul versions 1.6.0 and newer. -Mesh gateways enable routing of service mesh traffic between different Consul datacenters. Those datacenters can reside +Mesh gateways enable service mesh traffic to be routed between different Consul datacenters and admin partitions. The datacenters or partitions can reside in different clouds or runtime environments where general interconnectivity between all services in all datacenters -isn't feasible. One scenario where this is useful is when connecting networks with overlapping IP address space. +isn't feasible. -These gateways operate by sniffing the SNI header out of the mTLS connection and then routing the connection to the -appropriate destination based on the server name requested. The data within the mTLS session is not decrypted by -the Gateway. +They operate by sniffing and extracting the server name indication (SNI) header from the service mesh session and routing the connection to the appropriate destination based on the server name requested. The gateway does not decrypt the data within the mTLS session. -As of Consul 1.8.0, mesh gateways can also forward gossip and RPC traffic between Consul servers. -This is enabled by [WAN federation via mesh gateways](/docs/connect/gateways/wan-federation-via-mesh-gateways). +Mesh gateways enable the following scenarios: -For more information about mesh gateways, review the [complete documentation](/docs/connect/gateways/mesh-gateway) -and the [mesh gateway tutorial](https://learn.hashicorp.com/tutorials/consul/service-mesh-gateways). +* **Federate multiple datacenters across a WAN**. Since Consul 1.8.0, mesh gateways can forward gossip and RPC traffic between Consul servers. See [WAN federation via mesh gateways](/docs/connect/gateways/wan-federation-via-mesh-gateways) for additional information. +* **Service-to-service communication across datacenters**. Refer to [Enabling Service-to-service Traffic Accross Datacenters](/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters) for additional information. +* **Service-to-service communication across admin partitions**. Since Consul 1.11.0, you can create administrative boundaries for single Consul deployements called "admin partitions". You can use mesh gateways to facilitate cross-partition communication. Refer to [Enabling Service-to-service Traffic Accross Admin Partitions](/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions) for additional information. -![Mesh Gateway Architecture](/img/mesh-gateways.png) +-> **Mesh gateway tutorial**: Follow the [mesh gateway tutorial](https://learn.hashicorp.com/tutorials/consul/service-mesh-gateways) to learn concepts associated with mesh gateways. ## Ingress Gateways diff --git a/website/content/docs/connect/gateways/ingress-gateway.mdx b/website/content/docs/connect/gateways/ingress-gateway.mdx index ec969d9a6..50997b563 100644 --- a/website/content/docs/connect/gateways/ingress-gateway.mdx +++ b/website/content/docs/connect/gateways/ingress-gateway.mdx @@ -1,10 +1,9 @@ --- layout: docs -page_title: External <> Internal Services - Ingress Gateways +page_title: Using Ingress Gateways to Connect External Traffic to Internal Services description: >- - An ingress gateway enables ingress traffic from services outside the Consul - service mesh to services inside the Consul service mesh. This section details - how to use Envoy and describes how you can plug in a gateway of your choice. + This topic describes how ingress gateways enable traffic from external services to reach services inside the Consul service mesh. + It provides guidance on how to use Envoy and how to plug into your preferred gateway. --- # Ingress Gateways diff --git a/website/content/docs/connect/gateways/mesh-gateway/index.mdx b/website/content/docs/connect/gateways/mesh-gateway/index.mdx deleted file mode 100644 index 533821c5d..000000000 --- a/website/content/docs/connect/gateways/mesh-gateway/index.mdx +++ /dev/null @@ -1,215 +0,0 @@ ---- -layout: docs -page_title: Connect Datacenters - Mesh Gateways -description: >- - A Mesh Gateway enables better routing of a Connect service's data to upstreams - in other datacenters. This section details how to use Envoy and describes how - you can plug in a gateway of your choice. ---- - -# Mesh Gateways - --> **1.6.0+:** This feature is available in Consul versions 1.6.0 and newer. - -Mesh gateways enable routing of Connect traffic between different Consul datacenters. Those datacenters -can reside in different clouds or runtime environments where general interconnectivity between all services -in all datacenters isn't feasible. These gateways operate by sniffing the SNI header out of the Connect session -and then route the connection to the appropriate destination based on the server name requested. The data -within the mTLS session is not decrypted by the Gateway. - -![Mesh Gateway Architecture](/img/mesh-gateways.png) - -For a complete example of how to connect services across datacenters, -review the [mesh gateway tutorial](https://learn.hashicorp.com/tutorials/consul/service-mesh-gateways). - -## Prerequisites - -Each mesh gateway needs three things: - -1. A local Consul agent to manage its configuration. -2. General network connectivity to all services within its local Consul datacenter. -3. General network connectivity to all mesh gateways within remote Consul datacenters. - -Mesh gateways also require that your Consul datacenters are configured correctly: - -- Consul version 1.6.0 or newer is required. -- Consul [Connect](/docs/agent/options#connect) must be enabled in both datacenters. -- Each of your [datacenters](/docs/agent/options#datacenter) must have a unique name. -- Your datacenters must be [WAN joined](https://learn.hashicorp.com/tutorials/consul/federarion-gossip-wan). -- The [primary datacenter](/docs/agent/options#primary_datacenter) must be set to the same value in both datacenters. This specifies which datacenter is the authority for Connect certificates and is required for services in all datacenters to establish mutual TLS with each other. -- [gRPC](/docs/agent/options#grpc_port) must be enabled. -- If you want to [enable gateways globally](/docs/connect/mesh-gateway#enabling-gateways-globally) you must enable [centralized configuration](/docs/agent/options#enable_central_service_config). - -Currently, Envoy is the only proxy with mesh gateway capabilities in Consul. - -- Mesh gateway proxies receive their configuration through Consul, which - automatically generates it based on the proxy's registration. Currently Consul - can only translate mesh gateway registration information into Envoy - configuration, therefore the proxies acting as mesh gateways must be Envoy. - -- Sidecar proxies that send traffic to an upstream service through a gateway - need to know the location of that gateway. They discover the gateway based on - their sidecar proxy registrations. Consul can only translate the gateway - registration information into Envoy configuration, so any sidecars that send - upstream traffic through a gateway must be Envoy. - -Sidecar proxies that don't send upstream traffic through a gateway aren't -affected when you deploy gateways. If you are using Consul's built-in proxy as a -Connect sidecar it will continue to work for intra-datacenter traffic and will -receive incoming traffic even if that traffic has passed through a gateway. - -## Modes of Operation - -Each upstream of a Connect proxy can be configured to be routed through a mesh gateway. Depending on -your network, the proxy's connection to the gateway can happen in one of the following modes -illustrated in the diagram above: - -- `local` - In this mode the Connect proxy makes its outbound connection to a gateway running in the - same datacenter. That gateway is then responsible for ensuring the data gets forwarded along to - gateways in the destination datacenter. This is the mode of operation depicted in the diagram at - the beginning of the page. - -- `remote` - In this mode the Connect proxy makes its outbound connection to a gateway running in the - destination datacenter. That gateway will then forward the data to the final destination service. - -- `none` - In this mode, no gateway is used and a Connect proxy makes its outbound connections directly - to the destination services. - -## Mesh Gateway Configuration - -Mesh gateways are defined similarly to other services registered with Consul, with two exceptions. -The first is that the [service kind](/api/agent/service#kind) must be "mesh-gateway". Second, -the mesh gateway service definition may contain a `Proxy.Config` entry, just like a -Connect proxy service, to define opaque configuration parameters useful for the actual proxy software. -For Envoy there are some supported [gateway options](/docs/connect/proxies/envoy#gateway-options) as well as -[escape-hatch overrides](/docs/connect/proxies/envoy#escape-hatch-overrides). - --> **Note:** If ACLs are enabled, a token granting `service:write` for the gateway's service name -and `service:read` for all services in the datacenter must be added to the gateway's service definition. -These permissions authorize the token to route communications for other Connect services but does not -allow decrypting any of their communications. - -## Connect Proxy Configuration - -Configuring a Connect Proxy to use gateways is as simple as setting its mode of operation. This can be done -in several different places allowing for global to more fine grained control. If the gateway mode is configured -in multiple locations the order of precedence is as follows - -1. Upstream Definition -2. Service Instance Definition -3. Centralized `service-defaults` configuration entry -4. Centralized `proxy-defaults` configuration entry. - -### Enabling Gateways Globally - -The following `proxy-defaults` configuration will enable gateways for all Connect services in the `local` mode. - -```hcl -Kind = "proxy-defaults" -Name = "global" -MeshGateway { - Mode = "local" -} -``` - -### Enabling Gateways Per-Service - -The following `service-defaults` configuration will enable gateways for all Connect services with the name "web". - -```hcl -Kind = "service-defaults" -Name = "web" -MeshGateway { - Mode = "local" -} -``` - -### Enabling Gateways for a Service Instance - -The following [Proxy Service Registration](/docs/connect/registration/service-registration) -definition will enable gateways for the service instance in the `remote` mode. - -```hcl -service { - name = "web-sidecar-proxy" - kind = "connect-proxy" - port = 8181 - proxy { - destination_service_name = "web" - mesh_gateway { - mode = "remote" - } - upstreams = [ - { - destination_name = "api" - datacenter = "secondary" - local_bind_port = 10000 - } - ] - } -} -``` - -Or alternatively inline with the service definition: - -```hcl -service { - name = "web" - port = 8181 - connect { - sidecar_service { - proxy { - mesh_gateway { - mode = "remote" - } - upstreams = [ - { - destination_name = "api" - datacenter = "secondary" - local_bind_port = 10000 - } - ] - } - } - } -} -``` - -### Enabling Gateways for a Proxy Upstream - -The following service definition will enable gateways in the `local` mode for one upstream, the `remote` mode -for a second upstream and will disable gateways for a third upstream. - -```hcl -service { - name = "web-sidecar-proxy" - kind = "connect-proxy" - port = 8181 - proxy { - destination_service_name = "web" - upstreams = [ - { - destination_name = "api" - local_bind_port = 10000 - mesh_gateway { - mode = "remote" - } - }, - { - destination_name = "db" - local_bind_port = 10001 - mesh_gateway { - mode = "local" - } - }, - { - destination_name = "logging" - local_bind_port = 10002 - mesh_gateway { - mode = "none" - } - }, - ] - } -} -``` diff --git a/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters.mdx b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters.mdx new file mode 100644 index 000000000..e6ce374eb --- /dev/null +++ b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters.mdx @@ -0,0 +1,205 @@ +--- +layout: docs +page_title: Service-to-service Traffic Across Datacenters +description: >- + This topic describes how to configure mesh gateways to route a service's data to upstreams + in other datacenters. It describes how to use Envoy and how you can integrate with your preferred gateway. +--- + +# Service-to-service Traffic Across Datacenters + +-> **1.6.0+:** This feature is available in Consul versions 1.6.0 and newer. + +Mesh gateways enable service mesh traffic to be routed between different Consul datacenters. +Datacenters can reside in different clouds or runtime environments where general interconnectivity between all services +in all datacenters isn't feasible. + +Mesh gateways operate by sniffing and extracting the server name indication (SNI) header from the service mesh session and routing the connection to the appropriate destination based on the server name requested. The gateway does not decrypt the data within the mTLS session. + +The following diagram describes the architecture for using mesh gateways for cross-datacenter communication: + +![Mesh Gateway Architecture](/img/mesh-gateways.png) + +-> **Mesh Gateway Tutorial**: Follow the [mesh gateway tutorial](https://learn.hashicorp.com/tutorials/consul/service-mesh-gateways) to learn important concepts associated with using mesh gateways for connecting services across datacenters. + +## Prerequisites + +Ensure that your Consul environment meets the following requirements. + +### Consul + +* Consul version 1.6.0 or newer. +* A local Consul agent is required to manage its configuration. +* Consul [Connect](/docs/agent/options#connect) must be enabled in both datacenters. +* Each [datacenter](/docs/agent/options#datacenter) must have a unique name. +* Each datacenters must be [WAN joined](https://learn.hashicorp.com/tutorials/consul/federarion-gossip-wan). +* The [primary datacenter](/docs/agent/options#primary_datacenter) must be set to the same value in both datacenters. This specifies which datacenter is the authority for Connect certificates and is required for services in all datacenters to establish mutual TLS with each other. +* [gRPC](/docs/agent/options#grpc_port) must be enabled. +* If you want to [enable gateways globally](/docs/connect/mesh-gateway#enabling-gateways-globally) you must enable [centralized configuration](/docs/agent/options#enable_central_service_config). + +### Network + +* General network connectivity to all services within its local Consul datacenter. +* General network connectivity to all mesh gateways within remote Consul datacenters. + +### Proxy + +Envoy is the only proxy with mesh gateway capabilities in Consul. + +Mesh gateway proxies receive their configuration through Consul, which automatically generates it based on the proxy's registration. +Consul can only translate mesh gateway registration information into Envoy configuration. + +Sidecar proxies that send traffic to an upstream service through a gateway need to know the location of that gateway. They discover the gateway based on their sidecar proxy registrations. Consul can only translate the gateway registration information into Envoy configuration. + +Sidecar proxies that do not send upstream traffic through a gateway are not affected when you deploy gateways. If you are using Consul's built-in proxy as a Connect sidecar it will continue to work for intra-datacenter traffic and will receive incoming traffic even if that traffic has passed through a gateway. + +## Configuration + +Configure the following settings to register the mesh gateway as a service in Consul. + +* Specify `mesh-gateway` in the `kind` field to register the gateway with Consul. +* Define the `Proxy.Config` settings using opaque parameters compatible with your proxy, i.e., Envoy. For Envoy, refer to the [Gateway Options](/docs/connect/proxies/envoy#gateway-options) and [Escape-hatch Overrides](/docs/connect/proxies/envoy#escape-hatch-overrides) documentation for additional configuration information. +* If ACLs are enabled, a token granting `service:write` for the gateway's service name and `service:read` for all services in the datacenter or partition must be added to the gateway's service definition. These permissions authorize the token to route communications for other Consul service mesh services, but does not allow decrypting any of their communications. + +### Modes + +Each upstream associated with a service mesh proxy can be configured so that it is routed through a mesh gateway. +Depending on your network, the proxy's connection to the gateway can operate in one of the following modes (refer to the [mesh-architecture-diagram](#mesh-architecture-diagram)): + +* `none` - (Default) No gateway is used and a service mesh connect proxy makes its outbound connections directly + to the destination services. + +* `local` - The service mesh connect proxy makes an outbound connection to a gateway running in the + same datacenter. That gateway is responsible for ensuring that the data is forwarded to gateways in the destination datacenter. + Refer to the flow labeled `local` in the [mesh-architecture-diagram](#mesh-architecture-diagram). + +* `remote` - The service mesh proxy makes an outbound connection to a gateway running in the destination datacenter. + The gateway forwards the data to the final destination service. + Refer to the flow labeled `remote` in the [mesh-architecture-diagram](#mesh-architecture-diagram). + +### Connect Proxy Configuration + +Set the proxy to the preferred [mode](#modes) to configure the service mesh proxy. You can specify the mode globally or within child configurations to control proxy behaviors at a lower level. Consul recognizes the following order of precedence if the gateway mode is configured in multiple locations the order of precedence: + +1. Upstream definition (highest priority) +2. Service instance definition +3. Centralized `service-defaults` configuration entry +4. Centralized `proxy-defaults` configuration entry + +## Example Configurations + +Use the following example configurations to help you understand some of the common scenarios. + +### Enabling Gateways Globally + +The following `proxy-defaults` configuration will enable gateways for all Connect services in the `local` mode. + +```hcl +Kind = "proxy-defaults" +Name = "global" +MeshGateway { + Mode = "local" +} +``` + +### Enabling Gateways Per-Service + +The following `service-defaults` configuration will enable gateways for all Connect services with the name "web". + +```hcl +Kind = "service-defaults" +Name = "web" +MeshGateway { + Mode = "local" +} +``` + +### Enabling Gateways for a Service Instance + +The following [Proxy Service Registration](/docs/connect/registration/service-registration) +definition will enable gateways for the service instance in the `remote` mode. + +```hcl +service { + name = "web-sidecar-proxy" + kind = "connect-proxy" + port = 8181 + proxy { + destination_service_name = "web" + mesh_gateway { + mode = "remote" + } + upstreams = [ + { + destination_name = "api" + datacenter = "secondary" + local_bind_port = 10000 + } + ] + } +} +``` + +Or alternatively inline with the service definition: + +```hcl +service { + name = "web" + port = 8181 + connect { + sidecar_service { + proxy { + mesh_gateway { + mode = "remote" + } + upstreams = [ + { + destination_name = "api" + datacenter = "secondary" + local_bind_port = 10000 + } + ] + } + } + } +} +``` + +### Enabling Gateways for a Proxy Upstream + +The following service definition will enable gateways in the `local` mode for one upstream, the `remote` mode +for a second upstream and will disable gateways for a third upstream. + +```hcl +service { + name = "web-sidecar-proxy" + kind = "connect-proxy" + port = 8181 + proxy { + destination_service_name = "web" + upstreams = [ + { + destination_name = "api" + local_bind_port = 10000 + mesh_gateway { + mode = "remote" + } + }, + { + destination_name = "db" + local_bind_port = 10001 + mesh_gateway { + mode = "local" + } + }, + { + destination_name = "logging" + local_bind_port = 10002 + mesh_gateway { + mode = "none" + } + }, + ] + } +} +``` diff --git a/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx new file mode 100644 index 000000000..70a2e9c4c --- /dev/null +++ b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx @@ -0,0 +1,173 @@ +--- +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. +* 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. +* 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. +* 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. +* If ACLs are enabled, a token granting `service:write` for the gateway's service name and `service:read` for all services in the datacenter or partition must be added to the gateway's service definition. These permissions authorize the token to route communications for other Consul service mesh services, but does not allow decrypting any of their communications. + + +### Modes + +Each upstream associated with a service mesh proxy can be configured so that it is routed through a mesh gateway. +Depending on your network, the proxy's connection to the gateway can operate in one of the following modes: + +* `none` - (Default) No gateway is used and a service mesh connect proxy makes its outbound connections directly + to the destination services. + +* `local` - The service mesh connect proxy makes an outbound connection to a gateway running in the same datacenter. The gateway at the outbound connection is responsible for ensuring that the data is forwarded to gateways in the destination partition. + +* `remote` - The service mesh connect proxy makes an outbound connection to a gateway running in the destination datacenter. + The gateway forwards the data to the final destination service. + +### Connect Proxy Configuration + +Set the proxy to the preferred [mode](#modes) to configure the service mesh proxy. You can specify the mode globally or within child configurations to control proxy behaviors at a lower level. Consul recognizes the following order of precedence if the gateway mode is configured in multiple locations the order of precedence: + +1. Upstream definition (highest priority) +2. Service instance definition +3. Centralized `service-defaults` configuration entry +4. Centralized `proxy-defaults` configuration entry + +## Example Configurations + +Use the following example configurations to help you understand some of the common scenarios. + +### Enabling Gateways Globally + +The following `proxy-defaults` configuration will enable gateways for all Connect services in the `local` mode. + +```hcl +Kind = "proxy-defaults" +Name = "global" +MeshGateway { + Mode = "local" +} +``` + +### 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 `web` service instances in the `finance` partition. + +```hcl +service { + name = "web-sidecar-proxy" + kind = "connect-proxy" + port = 8181 + proxy { + destination_service_name = "web" + mesh_gateway { + mode = "local" + } + upstreams = [ + { + destination_partition = "finance" + destination_namespace = "default" + destination_type = "service" + destination_name = "billing" + local_bind_port = 9090 + } + ] + } +} +``` + +### Enabling Gateways for a Proxy Upstream + +The following service definition will enable gateways in `local` mode for three different partitions. Note that each service exists in the same namepace, but are separated by admin partition. + +```hcl +service { + name = "web-sidecar-proxy" + kind = "connect-proxy" + port = 8181 + proxy { + destination_service_name = "web" + upstreams = [ + { + destination_name = "api" + destination_namespace = "dev" + destination_partition = "api" + local_bind_port = 10000 + mesh_gateway { + mode = "local" + } + }, + { + destination_name = "db" + destination_namespace = "dev" + destination_partition = "db" + local_bind_port = 10001 + mesh_gateway { + mode = "local" + } + }, + { + destination_name = "logging" + destination_namespace = "dev" + destination_partition = "logging" + local_bind_port = 10002 + mesh_gateway { + mode = "local" + } + }, + ] + } +} +``` \ No newline at end of file diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 47f87740b..fcba21bbd 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -277,24 +277,28 @@ "path": "connect/gateways" }, { - "title": "Connect Datacenters - Mesh Gateways", + "title": "Mesh Gateways", "routes": [ - { - "title": "Overview", - "path": "connect/gateways/mesh-gateway" - }, { "title": "WAN Federation", "path": "connect/gateways/mesh-gateway/wan-federation-via-mesh-gateways" + }, + { + "title": "Enabling Service-to-service Traffic Across Datacenters", + "path": "connect/gateways/mesh-gateway/service-to-service-traffic-datacenters" + }, + { + "title": "Enabling Service-to-service Traffic Across Admin Partitions", + "path": "connect/gateways/mesh-gateway/service-to-service-traffic-partitions" } ] }, { - "title": "External <> Internal Services - Ingress Gateways", + "title": "Ingress Gateways", "path": "connect/gateways/ingress-gateway" }, { - "title": "Internal <> External Services - Terminating Gateways", + "title": "Terminating Gateways", "path": "connect/gateways/terminating-gateway" } ] From 2b0bb8c62befaf8b4260212279df8681d1df266d Mon Sep 17 00:00:00 2001 From: trujillo-adam Date: Thu, 16 Dec 2021 17:24:46 -0800 Subject: [PATCH 05/23] ACL system docs updates re: admin partitions in Consul v1.11.0 --- .../content/docs/security/acl/acl-system.mdx | 120 ++++++++++++++---- 1 file changed, 98 insertions(+), 22 deletions(-) diff --git a/website/content/docs/security/acl/acl-system.mdx b/website/content/docs/security/acl/acl-system.mdx index 927e0838d..7de5783a2 100644 --- a/website/content/docs/security/acl/acl-system.mdx +++ b/website/content/docs/security/acl/acl-system.mdx @@ -69,16 +69,53 @@ If the ACL system becomes inoperable, you can follow the ### ACL Policies -An ACL policy is a named set of rules and is composed of the following elements: +An ACL policy (not to be confused with [policy dispositions](/docs/security/acl/acl-rules#policy-dispositions)) is a named set of rules and several attributes that define the policy domain. The ID is generated when policy is created, but you can specify the attributes when creating the policy. Refer to the [ACL policy command line](https://www.consul.io/commands/acl/policy) documentation or [ACL policy API](/api-docs/acl/policies) documentation for additional information on how to create policies. -- **ID** - The policy's auto-generated public identifier. -- **Name** - A unique meaningful name for the policy. -- **Description** - A human readable description of the policy. (Optional) -- **Rules** - Set of rules granting or denying permissions. See the [Rule Specification](/docs/acl/acl-rules#rule-specification) documentation for more details. -- **Datacenters** - A list of datacenters the policy is valid within. -- **Namespace** - - The namespace this policy resides within. (Added in Consul Enterprise 1.7.0) +ACL policies can have the following attributes: --> **Consul Enterprise Namespacing** - Rules defined in a policy in any namespace other than `default` will be [restricted](/docs/acl/acl-rules#namespace-rules) to being able to grant a subset of the overall privileges and only affecting that single namespace. +| Attribute | Description | Required | Default | +| --- | --- | --- | --- | +| `ID` | The policy's auto-generated public identifier. | N/A | N/A | +| `name` | Unique name for the policy. | Required | none | +| `description` | Human readable description of the policy. | Optional | none | +| `rules` | Set of rules granting or denying permissions. See the [Rule Specification](/docs/acl/acl-rules#rule-specification) documentation for more details. | Optional | none | +| `datacenter` | Datacenter in which the policy is valid. More than one datacenter can be specified. | Optional | none | +| `namespace` | Namespace in which the policy is valid. Added in Consul Enterprise 1.7.0. | Optional | `default` | +| `partition` | Admin partition in which the policy is valid. Added in Consul Enterprise 1.11.0 | Optional | `default` | + +-> **Non-default Namespaces and Partitions** - Rules defined in a policy tied to an namespace or admin partition other than `default` can only grant a subset of privileges that affect the namespace or partition. See [Namespace Rules](/docs/acl/acl-rules#namespace-rules) and [Admin Partition Rules](/docs/acl/acl-rules#admin-partition-rules) for additional information. + +You can view the current ACL policies on the command line or through the API. The following example demonstrates the command line usage: + +```shell-session +$ consul acl policy list -format json -token +[ + { + "ID": "56595ec1-52e4-d6de-e566-3b78696d5459", + "Name": "b-policy", + "Description": "", + "Datacenters": null, + "Hash": "ULwaXlI6Ecqb9YSPegXWgVL1LlwctY9TeeAOhp5HGBA=", + "CreateIndex": 126, + "ModifyIndex": 126, + "Namespace": "default", + "Partition": "default" + }, + { + "ID": "00000000-0000-0000-0000-000000000001", + "Name": "global-management", + "Description": "Builtin Policy that grants unlimited access", + "Datacenters": null, + "Hash": "W1bQuDAlAlxEb4ZWwnVHplnt3I5oPKOZJQITh79Xlog=", + "CreateIndex": 70, + "ModifyIndex": 70, + "Namespace": "default", + "Partition": "default" + } +] +``` + +Note that the `Hash`, `CreateIndex`, and `ModifyIndex` attributes are also printed. These attributes are printed for all responses and are not specific to ACL policies. #### Builtin Policies @@ -130,8 +167,7 @@ node_prefix "" { The [API documentation for roles](/api/acl/roles#sample-payload) has some examples of using a service identity. --> **Consul Enterprise Namespacing** - Service Identity rules will be scoped to the single namespace that -the corresponding ACL Token or Role resides within. +-> **Service Scope for Namespace and Admin Partition** - Service identity rules in Consul Enterprise are scoped to the namespace or admin partition within which the corresponding ACL token or role resides. ### ACL Node Identities @@ -179,26 +215,66 @@ of the following elements: - **Service Identity Set** - The list of service identities that are applicable for the role. - **Namespace** - The namespace this policy resides within. (Added in Consul Enterprise 1.7.0) --> **Consul Enterprise Namespacing** - Roles may only link to policies defined in the same namespace as the role itself. +-> **Linking Roles to Policies in Consul Enterprise** - Roles can only be linked to policies that are defined in the same namespace or admin partition. ### ACL Tokens -ACL tokens are used to determine if the caller is authorized to perform an action. An ACL token is composed of the following -elements: +Consul uses ACL tokens to determine if the caller is authorized to perform an action. An ACL token is composed of several attributes that you can specify when creating the token. Refer to the [ACL token command line](https://www.consul.io/commands/acl/token) documentation or [ACL token API](/api-docs/acl/tokens) documentation for additional information on how to create policies.: - **Accessor ID** - The token's public identifier. - **Secret ID** -The bearer token used when making requests to Consul. -- **Description** - A human readable description of the token. (Optional) - **Policy Set** - The list of policies that are applicable for the token. -- **Role Set** - The list of roles that are applicable for the token. (Added in Consul 1.5.0) -- **Service Identity Set** - The list of service identities that are applicable for the token. (Added in Consul 1.5.0) -- **Locality** - Indicates whether the token should be local to the datacenter it was created within or created in - the primary datacenter and globally replicated. -- **Expiration Time** - The time at which this token is revoked. (Optional; Added in Consul 1.5.0) -- **Namespace** - The namespace this policy resides within. (Added in Consul Enterprise 1.7.0) +- **Role Set** - The list of roles that are applicable for the token. Added in Consul 1.5.0. +- **Service Identity Set** - The list of service identities that are applicable for the token. Added in Consul 1.5.0 +- **Local** - Indicates whether the token is local to the datacenter in which it was created. The attribute also can specify if the token was created in the primary datacenter and globally replicated. +- **CreateTime** - Timestamp indicating when the token was created. +- **Expiration Time** - The time at which this token is revoked. This attribute is option when creating a token. Added in Consul 1.5.0. +- **Namespace** - The namespace in which the policy resides. Added in Consul Enterprise 1.7.0. +- **Partition** - The partition in which the policy resides. Added in Consul Enterprise 1.11.0. --> **Consul Enterprise Namespacing** - Tokens may only link to policies and roles defined in the same namespace as -the token itself. +-> **Linking Tokens to Policies in Consul Enterprise** - Tokens can only be linked to policies that are defined in the same namespace or admin partition. + +You can view the current ACL tokens on the command line or through the API. The following example demonstrates the command line usage: + +```shell-session +$ consul acl token list -format json -token +[ + { + "CreateIndex": 75, + "ModifyIndex": 75, + "AccessorID": "c3274caa-fbe4-b457-f4af-c05ba89a048d", + "SecretID": "105c016a-ae9c-2006-ce23-4ef8823ba2af", + "Description": "Bootstrap Token (Global Management)", + "Policies": [ + { + "ID": "00000000-0000-0000-0000-000000000001", + "Name": "global-management" + } + ], + "Local": false, + "CreateTime": "2021-12-16T10:22:08.906291-08:00", + "Hash": "Wda9obh/gvreyTbVhbyJ3ipX0M/apF4kpqowPQQx+u8=", + "Legacy": false, + "Namespace": "default", + "Partition": "default" + }, + { + "CreateIndex": 71, + "ModifyIndex": 71, + "AccessorID": "00000000-0000-0000-0000-000000000002", + "SecretID": "anonymous", + "Description": "Anonymous Token", + "Local": false, + "CreateTime": "2021-12-16T10:21:11.996298-08:00", + "Hash": "tgCOyeidw+oaoZXQ9mHy6+EnY7atKoGaBzg2ndTwXl0=", + "Legacy": false, + "Namespace": "default", + "Partition": "default" + } +] +``` + +Note that the `CreateIndex`, `ModifyIndex`, and `Hash` attributes are also printed. These attributes are printed for all responses and are not specific to ACL tokens. #### Builtin Tokens From 94da06f6ee372e836a4e10e8bcd50d20d89db2b4 Mon Sep 17 00:00:00 2001 From: trujillo-adam Date: Fri, 17 Dec 2021 09:46:50 -0800 Subject: [PATCH 06/23] additional clarification on upstream configurations for x-dc and x-partition traffic --- .../mesh-gateway/service-to-service-traffic-datacenters.mdx | 3 ++- .../mesh-gateway/service-to-service-traffic-partitions.mdx | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters.mdx b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters.mdx index e6ce374eb..f7c153a34 100644 --- a/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters.mdx +++ b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters.mdx @@ -58,7 +58,8 @@ Sidecar proxies that do not send upstream traffic through a gateway are not affe 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. -* 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. +* 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 diff --git a/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx index 70a2e9c4c..2a0b91751 100644 --- a/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx +++ b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx @@ -44,12 +44,11 @@ Sidecar proxies that do not send upstream traffic through a gateway are not affe 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. -* 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. -* 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. +* 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. From 659e909fe3b1e6327f6a2d6eb8b358bbc47b546e Mon Sep 17 00:00:00 2001 From: David Yu Date: Fri, 17 Dec 2021 14:38:12 -0800 Subject: [PATCH 07/23] docs: clarification on Limitations for Vault Secrets backend (#11885) * docs: clarification on Limitations for Vault Secrets backend * address typo in CLI docs * Update index.mdx * Update index.mdx --- website/content/docs/k8s/installation/vault/index.mdx | 5 ++--- website/content/docs/k8s/k8s-cli.mdx | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/website/content/docs/k8s/installation/vault/index.mdx b/website/content/docs/k8s/installation/vault/index.mdx index 0943c3c92..9008d5bd4 100644 --- a/website/content/docs/k8s/installation/vault/index.mdx +++ b/website/content/docs/k8s/installation/vault/index.mdx @@ -38,9 +38,8 @@ injector: - `global.tls.serverAdditionalIPSans` is not currently configurable and must be manually added to the server certificate in Vault. - Mesh gateway is not currently supported. - Multi-DC Federation is not currently supported. -- Certificate rotation is not currently supported, ensure the TTL for your certificates is sufficiently long. Should your certificates -expire it will be necessary to issue a `consul reload` on each server. -- CA rotation is not currently supported. +- Certificate rotation for Server TLS certs is not currently supported through the Helm chart. Ensure the TTL for your Server TLS certificates are sufficiently long. Should your certificates expire it will be necessary to issue a `consul reload` on each server after issuing new Server TLS certs from Vault. +- CA rotation is not currently supported through the Helm chart and must be manually rotated. ## Next Steps diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index 2b55f25c3..c4b07305a 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -170,7 +170,7 @@ $ consul-k8s status metrics: defaultEnableMerging: true defaultEnabled: true - enableGatewayMetrics: trueU + enableGatewayMetrics: true controller: enabled: true global: From 7495b38c56ffc9b3e5639f4f3cd8f2a2727f171d Mon Sep 17 00:00:00 2001 From: FFMMM Date: Mon, 20 Dec 2021 09:10:13 -0800 Subject: [PATCH 08/23] add a metrics test check GH action (#11536) Signed-off-by: FFMMM --- .github/scripts/metrics_checker.sh | 33 +++++++++++++++++++ .github/workflows/pr-metrics-test-checker.yml | 25 ++++++++++++++ 2 files changed, 58 insertions(+) create mode 100755 .github/scripts/metrics_checker.sh create mode 100644 .github/workflows/pr-metrics-test-checker.yml diff --git a/.github/scripts/metrics_checker.sh b/.github/scripts/metrics_checker.sh new file mode 100755 index 000000000..067fdd96a --- /dev/null +++ b/.github/scripts/metrics_checker.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -uo pipefail + +### This script checks if any metric behavior has been modified. +### The checks rely on the git diff against origin/main +### It is still up to the reviewer to make sure that any tests added are needed and meaningful. + +# search for any "new" or modified metric emissions +metrics_modified=$(git --no-pager diff HEAD origin/main | grep -i "SetGauge\|EmitKey\|IncrCounter\|AddSample\|MeasureSince\|UpdateFilter") +# search for PR body or title metric references +metrics_in_pr_body=$(echo "${PR_BODY-""}" | grep -i "metric") +metrics_in_pr_title=$(echo "${PR_TITLE-""}" | grep -i "metric") + +# if there have been code changes to any metric or mention of metrics in the pull request body +if [ "$metrics_modified" ] || [ "$metrics_in_pr_body" ] || [ "$metrics_in_pr_title" ]; then + # need to check if there are modifications to metrics_test + test_files_regex="*_test.go" + modified_metrics_test_files=$(git --no-pager diff HEAD "$(git merge-base HEAD "origin/main")" -- "$test_files_regex" | grep -i "metric") + if [ "$modified_metrics_test_files" ]; then + # 1 happy path: metrics_test has been modified bc we modified metrics behavior + echo "PR seems to modify metrics behavior. It seems it may have added tests to the metrics as well." + exit 0 + else + echo "PR seems to modify metrics behavior. It seems no tests or test behavior has been modified." + echo "Please update the PR with any relevant updated testing or add a pr/no-metrics-test label to skip this check." + exit 1 + fi + +else + # no metrics modified in code, nothing to check + echo "No metric behavior seems to be modified." + exit 0 +fi diff --git a/.github/workflows/pr-metrics-test-checker.yml b/.github/workflows/pr-metrics-test-checker.yml new file mode 100644 index 000000000..3019d357c --- /dev/null +++ b/.github/workflows/pr-metrics-test-checker.yml @@ -0,0 +1,25 @@ +name: "Check for metrics tests" +on: + pull_request: + types: [ opened, synchronize, labeled ] + # Runs on PRs to main + branches: + - main + +jobs: + metrics_test_check: + if: "!contains(github.event.pull_request.labels.*.name, 'pr/no-metrics-test')" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + name: "checkout repo" + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 # by default the checkout action doesn't checkout all branches + - name: "Check for metrics modifications" + run: ./.github/scripts/metrics_checker.sh + # as of now, cannot use github vars in "external" scripts; ref: https://github.community/t/using-github-action-environment-variables-in-shell-script/18330 + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + shell: bash From a2c55fc50f9c333aa2cfed4a81ac63381c0ddda4 Mon Sep 17 00:00:00 2001 From: "Chris S. Kim" Date: Mon, 20 Dec 2021 15:09:03 -0500 Subject: [PATCH 09/23] website: Update version.js (#11897) --- website/data/version.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/data/version.js b/website/data/version.js index 7f1d5d8c8..9797bab9f 100644 --- a/website/data/version.js +++ b/website/data/version.js @@ -1 +1 @@ -export default '1.10.4' +export default '1.11.1' From 0eb6334b2dcc5f87a4bd9d7177eb211471a5cd9e Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 20 Dec 2021 16:42:20 -0500 Subject: [PATCH 10/23] Homepage use case redesign (#11728) * init homepage * adds tutorials * update subnav * adds intro background * add offerings * adds in practice cta * include radial gradient * cleanup gradient * Fix learn more button display * include use case pages * connect subnav menu items * extract in practice section for reuse * use Products type * fix type error * add neutral option * rework cta logic * Fix links map * fix use case path * updates accent method * fix button prop usage * refactor customer case study * refactor case studies component * cleanup margin * refactor data props * fix offering cta * spacing updates and introduce intro component * adds intro interface * removes footer border * fix intro description color * add revalidate code to homepage * cleanup unused imports * bump subnav * makes stats optional * adjust border radius based on customer story * redirect /home to homepage * fix: turtorials link * fix: logo alignment * fix: section background color * feat: home reorder and tuts and docs links * fix: flush padding * formatting * feat: sort use cases in nav * fix: card overflow * fix: adjust overflow method * fix: padding on desktop * fix: card container overflow padding on mobile * fix: intro cta conditional * fix: simplify conditional * fix: customer logo sizing * cleanup old data * accept isInternalLink as arg * remove chunk * fix: isInternalLink usage * fix: isInternalLink prop usage * fix: add lang to document * init homepage * adds tutorials * add offerings * cleanup unused imports * bump subnav * fix: flush padding * formatting * fix: intro cta conditional * fix: simplify conditional * cleanup old data * add consul on kubernetes to menu items * add use case redirect * Add use case redirect --- website/components/footer/style.css | 1 - .../components/io-card-container/index.tsx | 82 + .../io-card-container/style.module.css | 114 + website/components/io-card/index.tsx | 124 + website/components/io-card/product-logos.ts | 34 + website/components/io-card/style.module.css | 148 + website/components/io-dialog/index.tsx | 47 + website/components/io-dialog/style.module.css | 62 + .../io-home-call-to-action/index.tsx | 39 + .../io-home-call-to-action/style.module.css | 12 + .../components/io-home-case-studies/index.tsx | 81 + .../io-home-case-studies/style.module.css | 170 + website/components/io-home-feature/index.tsx | 80 + .../io-home-feature/style.module.css | 79 + website/components/io-home-hero/index.tsx | 135 + .../components/io-home-hero/style.module.css | 148 + .../components/io-home-in-practice/index.tsx | 86 + .../io-home-in-practice/style.module.css | 98 + website/components/io-home-intro/index.tsx | 155 + .../components/io-home-intro/style.module.css | 169 + .../components/io-home-pre-footer/index.tsx | 79 + .../io-home-pre-footer/style.module.css | 122 + .../io-usecase-call-to-action/index.tsx | 69 + .../style.module.css | 66 + .../components/io-usecase-customer/index.tsx | 86 + .../io-usecase-customer/style.module.css | 119 + website/components/io-usecase-hero/index.tsx | 41 + .../components/io-usecase-hero/pattern.svg | 1 + .../io-usecase-hero/style.module.css | 83 + .../components/io-usecase-section/index.tsx | 81 + .../io-usecase-section/style.module.css | 106 + website/components/io-video-callout/index.tsx | 80 + .../components/io-video-callout/play-icon.tsx | 23 + .../io-video-callout/style.module.css | 128 + website/components/subnav/index.jsx | 12 +- website/components/subnav/style.module.css | 3 + website/data/subnav.js | 56 - website/layouts/standard/index.tsx | 76 + website/layouts/standard/query.graphql | 6 + website/lib/utils.ts | 11 + website/next.config.js | 9 + website/package-lock.json | 7282 +++++++++++------ website/package.json | 7 +- website/pages/_app.js | 37 +- website/pages/_document.js | 2 +- website/pages/home/img/cloud/hcp.jpg | 3 - website/pages/home/img/cloud/hcs.jpg | 3 - .../home/img/extend-mesh-transparent.png | 3 - website/pages/home/img/hcp-consul.svg | 1 - website/pages/home/img/hcp_consul.svg | 120 - .../img/kubernetes/communication-arrows.svg | 1 - website/pages/home/img/kubernetes/logo.svg | 1 - website/pages/home/img/learn/Vault.svg | 1 - .../pages/home/img/learn/getting-started.svg | 5 - website/pages/home/img/learn/kubernetes.svg | 7 - .../home/img/multi-datacenter-transparent.png | 3 - .../pages/home/img/quotes/barclays-logo.svg | 12 - .../pages/home/img/quotes/citadel-logo.svg | 15 - website/pages/home/img/quotes/criteo-logo.svg | 1 - .../pages/home/img/quotes/mercedes-logo.svg | 5 - .../pages/home/img/service-splitter@2x.png | 3 - .../img/service-to-service-transparent.png | 3 - .../pages/home/img/stack/consul-and-nomad.svg | 12 - .../home/img/stack/consul-and-terraform.svg | 15 - .../pages/home/img/stack/consul-and-vault.svg | 12 - .../use-cases/discovery_health_checking.svg | 29 - .../home/img/use-cases/network_automation.svg | 24 - .../pages/home/img/use-cases/service_mesh.svg | 24 - .../img/why-consul/consul_features_arrows.svg | 4 - .../img/why-consul/consul_features_cloud.svg | 4 - .../img/why-consul/consul_features_gear.svg | 11 - .../img/why-consul/consul_features_health.svg | 4 - .../img/why-consul/consul_features_kub.svg | 11 - .../img/why-consul/consul_features_plus.svg | 4 - .../img/why-consul/consul_features_world.svg | 5 - website/pages/home/index.jsx | 473 -- website/pages/home/index.tsx | 206 + website/pages/home/query.graphql | 135 + website/pages/home/style.css | 88 - website/pages/home/style.module.css | 27 + website/pages/{index.jsx => index.tsx} | 0 website/pages/style.css | 3 - website/pages/use-cases/[slug].tsx | 236 + .../img/discovery-health-checking/Map.png | 3 - .../centralized-service-registry.png | 3 - .../consul_screenshot@2x.png | 3 - .../extesnsible-api.png | 3 - .../health-monitoring.png | 3 - .../mercedes-case-study.png | 3 - .../service-discovery-and-health-checking.svg | 147 - website/pages/use-cases/img/mercedes-logo.svg | 1 - .../multi-platform-service-mesh/Ecosystem.png | 3 - .../extend-mesh.svg | 1 - .../kubernetes-extend-transparent.png | 3 - .../kubernetes-extend.png | 3 - .../muilti-datacenter.png | 3 - .../observability@3x.png | 3 - .../service-to-service.png | 3 - .../service-to-service.svg | 140 - .../traffic_mgmt@3x.png | 3 - .../architecture-extend.png | 3 - .../network-automation/ecosystem-extend.png | 3 - .../img/network-automation/firewalling.png | 3 - .../health-checks-visibility.png | 3 - .../img/network-automation/load-balancing.png | 3 - .../use-cases/multi-platform-service-mesh.jsx | 155 - .../network-infrastructure-automation.jsx | 132 - website/pages/use-cases/query.graphql | 92 + .../service-discovery-and-health-checking.jsx | 185 - website/pages/use-cases/style.module.css | 34 + website/public/img/home-hero-pattern.svg | 1 + website/public/img/offerings-graphic.svg | 1 + website/public/img/practice-pattern.svg | 1 + .../public/img/usecase-callout-pattern.svg | 1 + website/public/img/usecase-hero-pattern.svg | 1 + website/redirects.next.js | 10 + 116 files changed, 8838 insertions(+), 4081 deletions(-) create mode 100644 website/components/io-card-container/index.tsx create mode 100644 website/components/io-card-container/style.module.css create mode 100644 website/components/io-card/index.tsx create mode 100644 website/components/io-card/product-logos.ts create mode 100644 website/components/io-card/style.module.css create mode 100644 website/components/io-dialog/index.tsx create mode 100644 website/components/io-dialog/style.module.css create mode 100644 website/components/io-home-call-to-action/index.tsx create mode 100644 website/components/io-home-call-to-action/style.module.css create mode 100644 website/components/io-home-case-studies/index.tsx create mode 100644 website/components/io-home-case-studies/style.module.css create mode 100644 website/components/io-home-feature/index.tsx create mode 100644 website/components/io-home-feature/style.module.css create mode 100644 website/components/io-home-hero/index.tsx create mode 100644 website/components/io-home-hero/style.module.css create mode 100644 website/components/io-home-in-practice/index.tsx create mode 100644 website/components/io-home-in-practice/style.module.css create mode 100644 website/components/io-home-intro/index.tsx create mode 100644 website/components/io-home-intro/style.module.css create mode 100644 website/components/io-home-pre-footer/index.tsx create mode 100644 website/components/io-home-pre-footer/style.module.css create mode 100644 website/components/io-usecase-call-to-action/index.tsx create mode 100644 website/components/io-usecase-call-to-action/style.module.css create mode 100644 website/components/io-usecase-customer/index.tsx create mode 100644 website/components/io-usecase-customer/style.module.css create mode 100644 website/components/io-usecase-hero/index.tsx create mode 100644 website/components/io-usecase-hero/pattern.svg create mode 100644 website/components/io-usecase-hero/style.module.css create mode 100644 website/components/io-usecase-section/index.tsx create mode 100644 website/components/io-usecase-section/style.module.css create mode 100644 website/components/io-video-callout/index.tsx create mode 100644 website/components/io-video-callout/play-icon.tsx create mode 100644 website/components/io-video-callout/style.module.css create mode 100644 website/components/subnav/style.module.css delete mode 100644 website/data/subnav.js create mode 100644 website/layouts/standard/index.tsx create mode 100644 website/layouts/standard/query.graphql create mode 100644 website/lib/utils.ts delete mode 100644 website/pages/home/img/cloud/hcp.jpg delete mode 100644 website/pages/home/img/cloud/hcs.jpg delete mode 100644 website/pages/home/img/extend-mesh-transparent.png delete mode 100644 website/pages/home/img/hcp-consul.svg delete mode 100644 website/pages/home/img/hcp_consul.svg delete mode 100644 website/pages/home/img/kubernetes/communication-arrows.svg delete mode 100644 website/pages/home/img/kubernetes/logo.svg delete mode 100644 website/pages/home/img/learn/Vault.svg delete mode 100644 website/pages/home/img/learn/getting-started.svg delete mode 100644 website/pages/home/img/learn/kubernetes.svg delete mode 100644 website/pages/home/img/multi-datacenter-transparent.png delete mode 100644 website/pages/home/img/quotes/barclays-logo.svg delete mode 100644 website/pages/home/img/quotes/citadel-logo.svg delete mode 100644 website/pages/home/img/quotes/criteo-logo.svg delete mode 100644 website/pages/home/img/quotes/mercedes-logo.svg delete mode 100644 website/pages/home/img/service-splitter@2x.png delete mode 100644 website/pages/home/img/service-to-service-transparent.png delete mode 100644 website/pages/home/img/stack/consul-and-nomad.svg delete mode 100644 website/pages/home/img/stack/consul-and-terraform.svg delete mode 100644 website/pages/home/img/stack/consul-and-vault.svg delete mode 100644 website/pages/home/img/use-cases/discovery_health_checking.svg delete mode 100644 website/pages/home/img/use-cases/network_automation.svg delete mode 100644 website/pages/home/img/use-cases/service_mesh.svg delete mode 100644 website/pages/home/img/why-consul/consul_features_arrows.svg delete mode 100644 website/pages/home/img/why-consul/consul_features_cloud.svg delete mode 100644 website/pages/home/img/why-consul/consul_features_gear.svg delete mode 100644 website/pages/home/img/why-consul/consul_features_health.svg delete mode 100644 website/pages/home/img/why-consul/consul_features_kub.svg delete mode 100644 website/pages/home/img/why-consul/consul_features_plus.svg delete mode 100644 website/pages/home/img/why-consul/consul_features_world.svg delete mode 100644 website/pages/home/index.jsx create mode 100644 website/pages/home/index.tsx create mode 100644 website/pages/home/query.graphql delete mode 100644 website/pages/home/style.css create mode 100644 website/pages/home/style.module.css rename website/pages/{index.jsx => index.tsx} (100%) create mode 100644 website/pages/use-cases/[slug].tsx delete mode 100644 website/pages/use-cases/img/discovery-health-checking/Map.png delete mode 100644 website/pages/use-cases/img/discovery-health-checking/centralized-service-registry.png delete mode 100644 website/pages/use-cases/img/discovery-health-checking/consul_screenshot@2x.png delete mode 100644 website/pages/use-cases/img/discovery-health-checking/extesnsible-api.png delete mode 100644 website/pages/use-cases/img/discovery-health-checking/health-monitoring.png delete mode 100644 website/pages/use-cases/img/discovery-health-checking/mercedes-case-study.png delete mode 100644 website/pages/use-cases/img/discovery-health-checking/service-discovery-and-health-checking.svg delete mode 100644 website/pages/use-cases/img/mercedes-logo.svg delete mode 100644 website/pages/use-cases/img/multi-platform-service-mesh/Ecosystem.png delete mode 100644 website/pages/use-cases/img/multi-platform-service-mesh/extend-mesh.svg delete mode 100644 website/pages/use-cases/img/multi-platform-service-mesh/kubernetes-extend-transparent.png delete mode 100644 website/pages/use-cases/img/multi-platform-service-mesh/kubernetes-extend.png delete mode 100644 website/pages/use-cases/img/multi-platform-service-mesh/muilti-datacenter.png delete mode 100644 website/pages/use-cases/img/multi-platform-service-mesh/observability@3x.png delete mode 100644 website/pages/use-cases/img/multi-platform-service-mesh/service-to-service.png delete mode 100644 website/pages/use-cases/img/multi-platform-service-mesh/service-to-service.svg delete mode 100644 website/pages/use-cases/img/multi-platform-service-mesh/traffic_mgmt@3x.png delete mode 100644 website/pages/use-cases/img/network-automation/architecture-extend.png delete mode 100644 website/pages/use-cases/img/network-automation/ecosystem-extend.png delete mode 100644 website/pages/use-cases/img/network-automation/firewalling.png delete mode 100644 website/pages/use-cases/img/network-automation/health-checks-visibility.png delete mode 100644 website/pages/use-cases/img/network-automation/load-balancing.png delete mode 100644 website/pages/use-cases/multi-platform-service-mesh.jsx delete mode 100644 website/pages/use-cases/network-infrastructure-automation.jsx create mode 100644 website/pages/use-cases/query.graphql delete mode 100644 website/pages/use-cases/service-discovery-and-health-checking.jsx create mode 100644 website/pages/use-cases/style.module.css create mode 100644 website/public/img/home-hero-pattern.svg create mode 100644 website/public/img/offerings-graphic.svg create mode 100644 website/public/img/practice-pattern.svg create mode 100644 website/public/img/usecase-callout-pattern.svg create mode 100644 website/public/img/usecase-hero-pattern.svg diff --git a/website/components/footer/style.css b/website/components/footer/style.css index 26edf1933..5f04743af 100644 --- a/website/components/footer/style.css +++ b/website/components/footer/style.css @@ -2,7 +2,6 @@ padding: 25px 0 17px 0; flex-shrink: 0; display: flex; - border-top: 1px solid var(--gray-5); & .g-grid-container { display: flex; diff --git a/website/components/io-card-container/index.tsx b/website/components/io-card-container/index.tsx new file mode 100644 index 000000000..e71ab886e --- /dev/null +++ b/website/components/io-card-container/index.tsx @@ -0,0 +1,82 @@ +import * as React from 'react' +import classNames from 'classnames' +import Button from '@hashicorp/react-button' +import IoCard, { IoCardProps } from 'components/io-card' +import s from './style.module.css' + +interface IoCardContaianerProps { + theme?: 'light' | 'dark' + heading?: string + description?: string + label?: string + cta?: { + url: string + text: string + } + cardsPerRow: 3 | 4 + cards: Array +} + +export default function IoCardContaianer({ + theme = 'light', + heading, + description, + label, + cta, + cardsPerRow = 3, + cards, +}: IoCardContaianerProps): React.ReactElement { + return ( +
+ {heading || description ? ( +
+ {heading ?

{heading}

: null} + {description ?

{description}

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

{label}

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

{children}

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

{children}

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

{heading}

+

{description}

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

{heading}

+

{description}

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

{heading}

+

{description}

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

{heading}

+

{description}

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

{cta.heading}

+ {cta.description ? ( +

{cta.description}

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

{heading}

+

{description}

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

    + {offering.heading} +

    +

    + {offering.description} +

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

{heading}

+

{description}

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

{heading}

+

{description}

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

{heading}

+
+

{description}

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

{heading}

+

{description}

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

    {value}

    +

    {key}

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

{eyebrow}

+

{heading}

+

{description}

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

{eyebrow}

+
+
+

{heading}

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

{heading}

+

{description}

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

{person.name}

+

{person.description}

+
+
+ )} +
+
+ +

{heading}

+
+ +
+
+ + ) +} diff --git a/website/components/io-video-callout/play-icon.tsx b/website/components/io-video-callout/play-icon.tsx new file mode 100644 index 000000000..37395ba2b --- /dev/null +++ b/website/components/io-video-callout/play-icon.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' + +export default function PlayIcon(): React.ReactElement { + return ( + + + + + ) +} diff --git a/website/components/io-video-callout/style.module.css b/website/components/io-video-callout/style.module.css new file mode 100644 index 000000000..815601ff0 --- /dev/null +++ b/website/components/io-video-callout/style.module.css @@ -0,0 +1,128 @@ +.videoCallout { + --columns: 1; + + margin: 0; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + background-color: var(--black); + border-radius: 6px; + overflow: hidden; + + @media (--medium-up) { + --columns: 12; + } +} + +.thumbnail { + position: relative; + display: grid; + place-items: center; + grid-column: 1 / -1; + background-color: transparent; + border: 0; + cursor: pointer; + padding: 96px 32px; + min-height: 300px; + + @media (--medium-up) { + grid-column: 1 / 7; + } + + @media (--large) { + grid-column: 1 / 9; + } + + & > svg { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1; + + @media (--small) { + width: 52px; + height: 52px; + } + } + + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #000; + opacity: 0.45; + transition: opacity ease-in-out 0.2s; + } + + &:hover::after { + opacity: 0.2; + } +} + +.content { + padding: 32px; + grid-column: 1 / -1; + + @media (--medium-up) { + padding: 80px 32px; + grid-column: 7 / -1; + } + + @media (--large) { + grid-column: 9 / -1; + } +} + +.heading { + margin: 0; + composes: g-type-display-4 from global; + color: var(--white); +} + +.description { + margin: 8px 0 0; + composes: g-type-body-small from global; + color: var(--white); +} + +.person { + margin-top: 64px; + display: flex; + align-items: center; + gap: 16px; +} + +.personThumbnail { + display: flex; + border-radius: 9999px; + overflow: hidden; +} + +.personName { + margin: 0; + composes: g-type-body-strong from global; + color: var(--white); +} + +.personDescription { + margin: 4px 0 0; + composes: g-type-label-strong from global; + color: var(--gray-3); +} + +.videoHeading { + margin-top: 0; + margin-bottom: 32px; + padding-right: 100px; + composes: g-type-display-4 from global; +} + +.video { + position: relative; + background-color: var(--gray-2); + aspect-ratio: 16 / 9; +} diff --git a/website/components/subnav/index.jsx b/website/components/subnav/index.jsx index 219545e33..4a70b89c1 100644 --- a/website/components/subnav/index.jsx +++ b/website/components/subnav/index.jsx @@ -1,14 +1,15 @@ import Subnav from '@hashicorp/react-subnav' -import subnavItems from '../../data/subnav' import { useRouter } from 'next/router' +import s from './style.module.css' -export default function ConsulSubnav() { +export default function ConsulSubnav({ menuItems }) { const router = useRouter() return ( diff --git a/website/components/subnav/style.module.css b/website/components/subnav/style.module.css new file mode 100644 index 000000000..5cb3cbccd --- /dev/null +++ b/website/components/subnav/style.module.css @@ -0,0 +1,3 @@ +.subnav { + border-top: 1px solid transparent; +} diff --git a/website/data/subnav.js b/website/data/subnav.js deleted file mode 100644 index cc14febd2..000000000 --- a/website/data/subnav.js +++ /dev/null @@ -1,56 +0,0 @@ -export default [ - { text: 'Overview', url: '/' }, - { - text: 'Use Cases', - submenu: [ - { - text: 'Service Discovery and Health Checking', - url: '/use-cases/service-discovery-and-health-checking', - }, - { - text: 'Network Infrastructure Automation', - url: '/use-cases/network-infrastructure-automation', - }, - { - text: 'Multi-Platform Service Mesh', - url: '/use-cases/multi-platform-service-mesh', - }, - { - text: 'Consul on Kubernetes', - url: '/consul-on-kubernetes', - }, - ], - }, - { - text: 'Enterprise', - url: - 'https://www.hashicorp.com/products/consul/?utm_source=oss&utm_medium=header-nav&utm_campaign=consul', - type: 'outbound', - }, - 'divider', - { - text: 'Tutorials', - url: 'https://learn.hashicorp.com/consul', - type: 'outbound', - }, - { - text: 'Docs', - url: '/docs', - type: 'inbound', - }, - { - text: 'API', - url: '/api-docs', - type: 'inbound', - }, - { - text: 'CLI', - url: '/commands', - type: 'inbound,', - }, - { - text: 'Community', - url: '/community', - type: 'inbound', - }, -] diff --git a/website/layouts/standard/index.tsx b/website/layouts/standard/index.tsx new file mode 100644 index 000000000..38bb3f8c5 --- /dev/null +++ b/website/layouts/standard/index.tsx @@ -0,0 +1,76 @@ +import query from './query.graphql' +import ProductSubnav from 'components/subnav' +import Footer from 'components/footer' +import { open } from '@hashicorp/react-consent-manager' + +export default function StandardLayout(props: Props): React.ReactElement { + const { useCaseNavItems } = props.data + + return ( + <> + { + return { + text: item.text, + url: `/use-cases/${item.url}`, + } + }), + ].sort((a, b) => a.text.localeCompare(b.text)), + }, + { + text: 'Enterprise', + url: + 'https://www.hashicorp.com/products/consul/?utm_source=oss&utm_medium=header-nav&utm_campaign=consul', + type: 'outbound', + }, + 'divider', + { + text: 'Tutorials', + url: 'https://learn.hashicorp.com/consul', + type: 'outbound', + }, + { + text: 'Docs', + url: '/docs', + type: 'inbound', + }, + { + text: 'API', + url: '/api-docs', + type: 'inbound', + }, + { + text: 'CLI', + url: '/commands', + type: 'inbound,', + }, + { + text: 'Community', + url: '/community', + type: 'inbound', + }, + ]} + /> + {props.children} +