deployer: add a bunch of test coverage and fix a few panics (#20694)

This adds a bunch of coverage of the topology.Compile method. It is not complete, but it is a start.

- A few panics and miscellany were fixed.
- The testing/deployer tests are now also run in CI.
This commit is contained in:
R.B. Boyer 2024-02-22 13:31:50 -06:00 committed by GitHub
parent 317eaa9a87
commit 235747d473
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 2403 additions and 28 deletions

View File

@ -490,6 +490,26 @@ jobs:
consul-license: ${{secrets.CONSUL_LICENSE}} consul-license: ${{secrets.CONSUL_LICENSE}}
datadog-api-key: "${{ !endsWith(github.repository, '-enterprise') && secrets.DATADOG_API_KEY || '' }}" datadog-api-key: "${{ !endsWith(github.repository, '-enterprise') && secrets.DATADOG_API_KEY || '' }}"
go-test-testing-deployer:
needs:
- setup
- get-go-version
- dev-build
uses: ./.github/workflows/reusable-unit.yml
with:
directory: testing/deployer
runs-on: ${{ needs.setup.outputs.compute-large }}
repository-name: ${{ github.repository }}
go-tags: "${{ github.event.repository.name == 'consul-enterprise' && 'consulent consuldev' || '' }}"
go-version: ${{ needs.get-go-version.outputs.go-version }}
permissions:
id-token: write # NOTE: this permission is explicitly required for Vault auth.
contents: read
secrets:
elevated-github-token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
consul-license: ${{secrets.CONSUL_LICENSE}}
datadog-api-key: "${{ !endsWith(github.repository, '-enterprise') && secrets.DATADOG_API_KEY || '' }}"
noop: noop:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -532,6 +552,7 @@ jobs:
- go-test-sdk-backwards-compatibility - go-test-sdk-backwards-compatibility
- go-test-sdk - go-test-sdk
- go-test-32bit - go-test-32bit
- go-test-testing-deployer
# - go-test-s390x # - go-test-s390x
runs-on: ${{ fromJSON(needs.setup.outputs.compute-small) }} runs-on: ${{ fromJSON(needs.setup.outputs.compute-small) }}
if: always() && needs.conditional-skip.outputs.skip-ci != 'true' if: always() && needs.conditional-skip.outputs.skip-ci != 'true'
@ -585,4 +606,4 @@ jobs:
"message": ${{ toJSON(env.SLACK_MESSAGE_RAW) }} "message": ${{ toJSON(env.SLACK_MESSAGE_RAW) }}
} }
env: env:
SLACK_WEBHOOK_URL: ${{ secrets.CONSUL_PROTECTED_BRANCH_TEST_SLACK_WEBHOOK }} SLACK_WEBHOOK_URL: ${{ secrets.CONSUL_PROTECTED_BRANCH_TEST_SLACK_WEBHOOK }}

View File

@ -1,19 +1,21 @@
// Copyright (c) HashiCorp, Inc. // Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1 // SPDX-License-Identifier: BUSL-1.1
//go:build integration
package sprawltest_test package sprawltest_test
import ( import (
"strconv" "strconv"
"testing" "testing"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1" pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1" pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto-public/pbresource"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/testing/deployer/sprawl/sprawltest" "github.com/hashicorp/consul/testing/deployer/sprawl/sprawltest"
"github.com/hashicorp/consul/testing/deployer/topology" "github.com/hashicorp/consul/testing/deployer/topology"
) )

View File

@ -13,32 +13,42 @@ import (
"sort" "sort"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"golang.org/x/exp/maps"
"github.com/hashicorp/go-hclog"
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1" pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1" pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/go-hclog"
"golang.org/x/exp/maps"
"github.com/hashicorp/consul/testing/deployer/util" "github.com/hashicorp/consul/testing/deployer/util"
) )
const DockerPrefix = "cslc" // ConSuLCluster const DockerPrefix = "cslc" // ConSuLCluster
func Compile(logger hclog.Logger, raw *Config) (*Topology, error) { func Compile(logger hclog.Logger, raw *Config) (*Topology, error) {
return compile(logger, raw, nil) return compile(logger, raw, nil, "")
} }
func Recompile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error) { func Recompile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error) {
if prev == nil { if prev == nil {
return nil, errors.New("missing previous topology") return nil, errors.New("missing previous topology")
} }
return compile(logger, raw, prev) return compile(logger, raw, prev, "")
} }
func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error) { func compile(logger hclog.Logger, raw *Config, prev *Topology, testingID string) (*Topology, error) {
if logger == nil {
return nil, errors.New("logger is required")
}
if raw == nil {
return nil, errors.New("config is required")
}
var id string var id string
if prev == nil { if testingID != "" {
id = testingID
} else if prev == nil {
var err error var err error
id, err = newTopologyID() id, err = newTopologyID()
if err != nil { if err != nil {
@ -90,22 +100,17 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error
nextIndex int // use a global index so any shared networks work properly with assignments nextIndex int // use a global index so any shared networks work properly with assignments
) )
foundPeerNames := make(map[string]map[string]struct{}) compileCluster := func(c *Cluster) (map[string]struct{}, error) {
for _, c := range raw.Clusters {
if c.Name == "" { if c.Name == "" {
return nil, fmt.Errorf("cluster has no name") return nil, fmt.Errorf("cluster has no name")
} }
foundPeerNames[c.Name] = make(map[string]struct{}) foundPeerNames := make(map[string]struct{})
if !IsValidLabel(c.Name) { if !IsValidLabel(c.Name) {
return nil, fmt.Errorf("cluster name is not valid: %s", c.Name) return nil, fmt.Errorf("cluster name is not valid: %s", c.Name)
} }
if _, exists := clusters[c.Name]; exists {
return nil, fmt.Errorf("cannot have two clusters with the same name %q; use unique names and override the Datacenter field if that's what you want", c.Name)
}
if c.Datacenter == "" { if c.Datacenter == "" {
c.Datacenter = c.Name c.Datacenter = c.Name
} else { } else {
@ -114,7 +119,6 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error
} }
} }
clusters[c.Name] = c
if c.NetworkName == "" { if c.NetworkName == "" {
c.NetworkName = c.Name c.NetworkName = c.Name
} }
@ -158,6 +162,7 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error
m = make(map[string]struct{}) m = make(map[string]struct{})
tenancies[partition] = m tenancies[partition] = m
} }
m["default"] = struct{}{}
m[namespace] = struct{}{} m[namespace] = struct{}{}
} }
@ -203,8 +208,7 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error
addTenancy(res.Id.Tenancy.Partition, res.Id.Tenancy.Namespace) addTenancy(res.Id.Tenancy.Partition, res.Id.Tenancy.Namespace)
} }
seenNodes := make(map[NodeID]struct{}) compileClusterNode := func(n *Node) (map[string]struct{}, error) {
for _, n := range c.Nodes {
if n.Name == "" { if n.Name == "" {
return nil, fmt.Errorf("cluster %q node has no name", c.Name) return nil, fmt.Errorf("cluster %q node has no name", c.Name)
} }
@ -238,10 +242,9 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error
} }
addTenancy(n.Partition, "default") addTenancy(n.Partition, "default")
if _, exists := seenNodes[n.ID()]; exists { if n.IsServer() && n.Partition != "default" {
return nil, fmt.Errorf("cannot have two nodes in the same cluster %q with the same name %q", c.Name, n.ID()) return nil, fmt.Errorf("server agents must always be in the default partition")
} }
seenNodes[n.ID()] = struct{}{}
if len(n.usedPorts) != 0 { if len(n.usedPorts) != 0 {
return nil, fmt.Errorf("user cannot specify the usedPorts field") return nil, fmt.Errorf("user cannot specify the usedPorts field")
@ -328,7 +331,10 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error
return nil, fmt.Errorf("cluster %q node %q uses dataplane, but has more than one service", c.Name, n.Name) return nil, fmt.Errorf("cluster %q node %q uses dataplane, but has more than one service", c.Name, n.Name)
} }
seenServices := make(map[ID]struct{}) var (
foundPeerNames = make(map[string]struct{})
seenServices = make(map[ID]struct{})
)
for _, wrk := range n.Workloads { for _, wrk := range n.Workloads {
if n.IsAgent() { if n.IsAgent() {
// Default to that of the enclosing node. // Default to that of the enclosing node.
@ -412,7 +418,7 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error
dest.ID.Partition = "" // irrelevant here; we'll set it to the value of the OTHER side for plumbing purposes in tests dest.ID.Partition = "" // irrelevant here; we'll set it to the value of the OTHER side for plumbing purposes in tests
} }
dest.ID.Namespace = NamespaceOrDefault(dest.ID.Namespace) dest.ID.Namespace = NamespaceOrDefault(dest.ID.Namespace)
foundPeerNames[c.Name][dest.Peer] = struct{}{} foundPeerNames[dest.Peer] = struct{}{}
} }
addTenancy(dest.ID.Partition, dest.ID.Namespace) addTenancy(dest.ID.Partition, dest.ID.Namespace)
@ -431,7 +437,7 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error
} }
if dest.PortName == "" && n.IsV2() { if dest.PortName == "" && n.IsV2() {
// Assume this is a v1->v2 conversion and name it. // Assume this is a v1->v2 conversion and name it.
dest.PortName = "legacy" dest.PortName = V1DefaultPortName
} }
} }
@ -476,6 +482,15 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error
Protocol: cfg.ActualProtocol, Protocol: cfg.ActualProtocol,
}) })
} }
sort.Slice(svcPorts, func(i, j int) bool {
a, b := svcPorts[i], svcPorts[j]
if a.TargetPort < b.TargetPort {
return true
} else if a.TargetPort > b.TargetPort {
return false
}
return a.Protocol < b.Protocol
})
v2svc := &pbcatalog.Service{ v2svc := &pbcatalog.Service{
Workloads: &pbcatalog.WorkloadSelector{}, Workloads: &pbcatalog.WorkloadSelector{},
@ -520,6 +535,31 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error
} }
} }
} }
return foundPeerNames, nil
}
seenNodes := make(map[NodeID]struct{})
for _, n := range c.Nodes {
peerNames, err := compileClusterNode(n)
if err != nil {
return nil, fmt.Errorf("error compiling node %q: %w", n.Name, err)
}
if _, exists := seenNodes[n.ID()]; exists {
return nil, fmt.Errorf("cannot have two nodes in the same cluster %q with the same name %q", c.Name, n.ID())
}
seenNodes[n.ID()] = struct{}{}
maps.Copy(foundPeerNames, peerNames)
}
// Default anything in the toplevel services map.
for _, svc := range c.Services {
for _, port := range svc.Ports {
if port.Protocol == pbcatalog.Protocol_PROTOCOL_UNSPECIFIED {
port.Protocol = pbcatalog.Protocol_PROTOCOL_TCP
}
}
} }
if err := assignVirtualIPs(c); err != nil { if err := assignVirtualIPs(c); err != nil {
@ -582,6 +622,24 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error
return nil, fmt.Errorf("cluster %q references non-default partitions or namespaces but is CE", c.Name) return nil, fmt.Errorf("cluster %q references non-default partitions or namespaces but is CE", c.Name)
} }
} }
return foundPeerNames, nil
}
foundPeerNames := make(map[string]map[string]struct{})
for _, c := range raw.Clusters {
peerNames, err := compileCluster(c)
if err != nil {
return nil, fmt.Errorf("error building cluster %q: %w", c.Name, err)
}
foundPeerNames[c.Name] = peerNames
if _, exists := clusters[c.Name]; exists {
return nil, fmt.Errorf("cannot have two clusters with the same name %q; use unique names and override the Datacenter field if that's what you want", c.Name)
}
clusters[c.Name] = c
} }
clusteredPeerings := make(map[string]map[string]*PeerCluster) // local-cluster -> local-peer -> info clusteredPeerings := make(map[string]map[string]*PeerCluster) // local-cluster -> local-peer -> info

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,10 @@ import (
"github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto-public/pbresource"
) )
const (
V1DefaultPortName = "legacy"
)
type Topology struct { type Topology struct {
ID string ID string
@ -898,6 +902,9 @@ func (w *Workload) ports() []int {
if len(w.Ports) > 0 { if len(w.Ports) > 0 {
seen := make(map[int]struct{}) seen := make(map[int]struct{})
for _, port := range w.Ports { for _, port := range w.Ports {
if port == nil {
continue
}
if _, ok := seen[port.Number]; !ok { if _, ok := seen[port.Number]; !ok {
// It's totally fine to expose the same port twice in a workload. // It's totally fine to expose the same port twice in a workload.
seen[port.Number] = struct{}{} seen[port.Number] = struct{}{}
@ -933,6 +940,8 @@ func (w *Workload) DigestExposedPorts(ports map[int]int) {
} }
} }
// Validate checks a bunch of stuff intrinsic to the definition of the workload
// itself.
func (w *Workload) Validate() error { func (w *Workload) Validate() error {
if w.ID.Name == "" { if w.ID.Name == "" {
return fmt.Errorf("service name is required") return fmt.Errorf("service name is required")
@ -956,13 +965,16 @@ func (w *Workload) Validate() error {
} }
if w.Port > 0 { if w.Port > 0 {
w.Ports = map[string]*Port{ w.Ports = map[string]*Port{
"legacy": { V1DefaultPortName: {
Number: w.Port, Number: w.Port,
Protocol: "tcp", Protocol: "tcp",
}, },
} }
w.Port = 0 w.Port = 0
} }
if w.Ports == nil {
w.Ports = make(map[string]*Port)
}
if !w.DisableServiceMesh && w.EnvoyPublicListenerPort > 0 { if !w.DisableServiceMesh && w.EnvoyPublicListenerPort > 0 {
w.Ports["mesh"] = &Port{ w.Ports["mesh"] = &Port{
@ -990,7 +1002,7 @@ func (w *Workload) Validate() error {
} }
} else { } else {
if len(w.Ports) > 0 { if len(w.Ports) > 0 {
return fmt.Errorf("cannot specify mulitport on service in v1") return fmt.Errorf("cannot specify multiport on service in v1")
} }
if w.Port <= 0 { if w.Port <= 0 {
return fmt.Errorf("service has invalid port") return fmt.Errorf("service has invalid port")