From 235747d47300be8ffe71e8eeb83736c24b4754e9 Mon Sep 17 00:00:00 2001 From: "R.B. Boyer" <4903+rboyer@users.noreply.github.com> Date: Thu, 22 Feb 2024 13:31:50 -0600 Subject: [PATCH] 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. --- .github/workflows/go-tests.yml | 23 +- .../deployer/sprawl/sprawltest/test_test.go | 6 +- testing/deployer/topology/compile.go | 104 +- testing/deployer/topology/compile_test.go | 2282 +++++++++++++++++ testing/deployer/topology/topology.go | 16 +- 5 files changed, 2403 insertions(+), 28 deletions(-) create mode 100644 testing/deployer/topology/compile_test.go diff --git a/.github/workflows/go-tests.yml b/.github/workflows/go-tests.yml index c298623144..920a2ed5c5 100644 --- a/.github/workflows/go-tests.yml +++ b/.github/workflows/go-tests.yml @@ -490,6 +490,26 @@ jobs: consul-license: ${{secrets.CONSUL_LICENSE}} 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: runs-on: ubuntu-latest steps: @@ -532,6 +552,7 @@ jobs: - go-test-sdk-backwards-compatibility - go-test-sdk - go-test-32bit + - go-test-testing-deployer # - go-test-s390x runs-on: ${{ fromJSON(needs.setup.outputs.compute-small) }} if: always() && needs.conditional-skip.outputs.skip-ci != 'true' @@ -585,4 +606,4 @@ jobs: "message": ${{ toJSON(env.SLACK_MESSAGE_RAW) }} } env: - SLACK_WEBHOOK_URL: ${{ secrets.CONSUL_PROTECTED_BRANCH_TEST_SLACK_WEBHOOK }} \ No newline at end of file + SLACK_WEBHOOK_URL: ${{ secrets.CONSUL_PROTECTED_BRANCH_TEST_SLACK_WEBHOOK }} diff --git a/testing/deployer/sprawl/sprawltest/test_test.go b/testing/deployer/sprawl/sprawltest/test_test.go index 23032cadd1..30ebfc181f 100644 --- a/testing/deployer/sprawl/sprawltest/test_test.go +++ b/testing/deployer/sprawl/sprawltest/test_test.go @@ -1,19 +1,21 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 +//go:build integration + package sprawltest_test import ( "strconv" "testing" + "github.com/stretchr/testify/require" + "github.com/hashicorp/consul/api" pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1" pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1" pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" "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/topology" ) diff --git a/testing/deployer/topology/compile.go b/testing/deployer/topology/compile.go index 54bd2aa403..130284f37d 100644 --- a/testing/deployer/topology/compile.go +++ b/testing/deployer/topology/compile.go @@ -13,32 +13,42 @@ import ( "sort" "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" pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1" pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" "github.com/hashicorp/consul/proto-public/pbresource" - "github.com/hashicorp/go-hclog" - "golang.org/x/exp/maps" - "github.com/hashicorp/consul/testing/deployer/util" ) const DockerPrefix = "cslc" // ConSuLCluster 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) { if prev == nil { 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 - if prev == nil { + if testingID != "" { + id = testingID + } else if prev == nil { var err error id, err = newTopologyID() 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 ) - foundPeerNames := make(map[string]map[string]struct{}) - for _, c := range raw.Clusters { + compileCluster := func(c *Cluster) (map[string]struct{}, error) { if c.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) { 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 == "" { c.Datacenter = c.Name } else { @@ -114,7 +119,6 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error } } - clusters[c.Name] = c if c.NetworkName == "" { c.NetworkName = c.Name } @@ -158,6 +162,7 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error m = make(map[string]struct{}) tenancies[partition] = m } + m["default"] = 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) } - seenNodes := make(map[NodeID]struct{}) - for _, n := range c.Nodes { + compileClusterNode := func(n *Node) (map[string]struct{}, error) { if n.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") - 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()) + if n.IsServer() && n.Partition != "default" { + return nil, fmt.Errorf("server agents must always be in the default partition") } - seenNodes[n.ID()] = struct{}{} if len(n.usedPorts) != 0 { 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) } - seenServices := make(map[ID]struct{}) + var ( + foundPeerNames = make(map[string]struct{}) + seenServices = make(map[ID]struct{}) + ) for _, wrk := range n.Workloads { if n.IsAgent() { // 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.Namespace = NamespaceOrDefault(dest.ID.Namespace) - foundPeerNames[c.Name][dest.Peer] = struct{}{} + foundPeerNames[dest.Peer] = struct{}{} } 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() { // 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, }) } + 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{ 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 { @@ -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 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 diff --git a/testing/deployer/topology/compile_test.go b/testing/deployer/topology/compile_test.go new file mode 100644 index 0000000000..d7da18599f --- /dev/null +++ b/testing/deployer/topology/compile_test.go @@ -0,0 +1,2282 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package topology + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + + "github.com/hashicorp/go-hclog" + + pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1" + "github.com/hashicorp/consul/sdk/testutil" +) + +func TestCompile_CE(t *testing.T) { + type testcase struct { + in *Config + expect *Topology + expectErr string + } + + logger := hclog.NewNullLogger() + + const clusterID = "87c82bd03dc89d4d" + + run := func(t *testing.T, tc testcase) { + got, err := compile(logger, tc.in, nil, clusterID) + if tc.expectErr == "" { + require.NotNil(t, tc.expect, "field must be set") + require.NoError(t, err) + + // Set recursive pointers on expectations. + for _, c := range tc.expect.Clusters { + for _, n := range c.Nodes { + for _, w := range n.Workloads { + w.Node = n + } + } + } + + assertDeepEqual(t, tc.expect, got) + } else { + require.Nil(t, tc.expect, "field cannot be set") + require.Nil(t, got) + testutil.RequireErrorContains(t, err, tc.expectErr) + } + } + + cases := map[string]testcase{ + "nil": { + in: nil, + expectErr: `config is required`, + }, + "consul image cannot be set at the top level": { + in: &Config{ + Images: DefaultImages().ChooseConsul(true), + }, + expectErr: `topology.images.consul cannot be set at this level`, + }, + "no networks": { + in: &Config{}, + expectErr: `topology.networks is empty`, + }, + "bad network/no name": { + in: &Config{ + Networks: []*Network{{ + // + }}, + }, + expectErr: `network name is not valid`, + }, + "bad network/invalid name": { + in: &Config{ + Networks: []*Network{{ + Name: "-123", + }}, + }, + expectErr: `network name is not valid`, + }, + "bad network/should not use DockerName": { + in: &Config{ + Networks: []*Network{{ + Name: "foo", + DockerName: "blah", + }}, + }, + expectErr: `network "foo" should not specify DockerName`, + }, + "bad network/duplicate names": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + {Name: "foo"}, + }, + }, + expectErr: `cannot have two networks with the same name "foo"`, + }, + "bad network/unknown type": { + in: &Config{ + Networks: []*Network{ + {Name: "foo", Type: "ext"}, + }, + }, + expectErr: `network "foo" has unknown type "ext"`, + }, + "good networks one server node": { + in: &Config{ + Networks: []*Network{ + {Name: "foo1"}, + {Name: "foo2", Type: "lan"}, + {Name: "foo3", Type: "wan"}, + }, + Clusters: []*Cluster{{ + Name: "foo2", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "node1", + Addresses: []*Address{ + {Network: "foo2"}, + {Network: "foo3"}, + }, + }}, + }}, + }, + expect: &Topology{ + ID: clusterID, + Images: DefaultImages(), + Networks: map[string]*Network{ + "foo1": {Name: "foo1", Type: "lan", DockerName: "cslc-foo1-" + clusterID}, + "foo2": {Name: "foo2", Type: "lan", DockerName: "cslc-foo2-" + clusterID}, + "foo3": {Name: "foo3", Type: "wan", DockerName: "cslc-foo3-" + clusterID}, + }, + Clusters: map[string]*Cluster{ + "foo2": { + Name: "foo2", + NetworkName: "foo2", + Datacenter: "foo2", + Images: DefaultImages().ChooseConsul(false), + Nodes: []*Node{{ + Kind: NodeKindServer, + Version: NodeVersionV1, + Partition: "default", + Name: "node1", + Images: DefaultImages().ChooseConsul(false).ChooseNode(NodeKindServer), + Addresses: []*Address{ + {Network: "foo2", Type: "lan", DockerNetworkName: "cslc-foo2-" + clusterID}, + {Network: "foo3", Type: "wan", DockerNetworkName: "cslc-foo3-" + clusterID}, + }, + Cluster: "foo2", + Datacenter: "foo2", + }}, + Services: map[ID]*pbcatalog.Service{}, + Partitions: []*Partition{{ + Name: "default", + Namespaces: []string{"default"}, + }}, + }, + }, + }, + }, + "no clusters": { + in: &Config{ + Networks: []*Network{ + {Name: "foo1"}, + }, + }, + expectErr: `topology.clusters is empty`, + }, + "bad cluster/no name": { + in: &Config{ + Networks: []*Network{ + {Name: "foo1"}, + }, + Clusters: []*Cluster{{}}, + }, + expectErr: `error building cluster "": cluster has no name`, + }, + "bad cluster/invalid name": { + in: &Config{ + Networks: []*Network{ + {Name: "foo1"}, + }, + Clusters: []*Cluster{{ + Name: "-123", + }}, + }, + expectErr: `error building cluster "-123": cluster name is not valid: -123`, + }, + "bad cluster/invalid dc": { + in: &Config{ + Networks: []*Network{ + {Name: "foo1"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Datacenter: "-123", + }}, + }, + expectErr: `error building cluster "foo": datacenter name is not valid: -123`, + }, + "bad cluster/missing network": { + in: &Config{ + Networks: []*Network{ + {Name: "foo1"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + NetworkName: "bar", + }}, + }, + expectErr: `error building cluster "foo": cluster "foo" uses network name "bar" that does not exist`, + }, + "bad cluster/no nodes": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + }}, + }, + expectErr: `error building cluster "foo": cluster "foo" has no nodes`, + }, + "colliding clusters": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{ + { + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "node1", + }}, + }, + { + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "node1", + }}, + }, + }, + }, + expectErr: `cannot have two clusters with the same name "foo"; use unique names and override the Datacenter field if that's what you want`, + }, + // TODO: ERR: partitions in CE + // TODO: ERR: namespaces in CE + // TODO: ERR: servers in a non-default partition + "tenancy collection": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Enterprise: true, + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + }, + { + Kind: NodeKindClient, + Name: "mesh1", + Partition: "ap1", + Addresses: []*Address{ + {Network: "foo"}, + }, + Workloads: []*Workload{ + { + ID: NewID("zim", "ns1", ""), + Image: "busybox", + Port: 8888, + EnvoyAdminPort: 19000, + }, + { + ID: NewID("gir", "ns2", ""), + Image: "busybox", + Port: 8877, + EnvoyAdminPort: 19001, + Destinations: []*Destination{{ + ID: NewID("gaz", "ns3", "ap3"), + LocalAddress: "127.0.0.1", + LocalPort: 5000, + }}, + }, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV2, + Name: "mesh2", + Partition: "ap2", + Addresses: []*Address{ + {Network: "foo"}, + }, + Workloads: []*Workload{ + { + ID: NewID("gir", "ns4", "ap2"), + Image: "busybox", + Port: 8877, + EnvoyAdminPort: 19001, + ImpliedDestinations: []*Destination{{ + ID: NewID("gaz", "", "ap4"), + PortName: "www", + }}, + }, + }, + }, + }, + }}, + }, + expect: &Topology{ + ID: clusterID, + Images: DefaultImages(), + Networks: map[string]*Network{ + "foo": {Name: "foo", Type: "lan", DockerName: "cslc-foo-" + clusterID}, + }, + Clusters: map[string]*Cluster{ + "foo": { + Name: "foo", + NetworkName: "foo", + Datacenter: "foo", + Enterprise: true, + EnableV2: true, + Images: DefaultImages().ChooseConsul(true), + Services: map[ID]*pbcatalog.Service{ + NewID("gir", "ns4", "ap2"): { + Workloads: &pbcatalog.WorkloadSelector{ + Names: []string{"gir-mesh2"}, + }, + Ports: []*pbcatalog.ServicePort{ + {TargetPort: "legacy", VirtualPort: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_TCP}, + {TargetPort: "mesh", Protocol: pbcatalog.Protocol_PROTOCOL_MESH}, + }, + VirtualIps: []string{"10.244.0.2"}, + }, + }, + Nodes: []*Node{ + { + Kind: NodeKindServer, + Version: NodeVersionV1, + Name: "server1", + Partition: "default", + Images: DefaultImages().ChooseConsul(true).ChooseNode(NodeKindServer), + Addresses: []*Address{ + {Network: "foo", Type: "lan", DockerNetworkName: "cslc-foo-" + clusterID}, + }, + Cluster: "foo", + Datacenter: "foo", + }, + { + Kind: NodeKindClient, + Version: NodeVersionV1, + Name: "mesh1", + Partition: "ap1", + Images: DefaultImages().ChooseConsul(true).ChooseNode(NodeKindClient), + Addresses: []*Address{ + {Network: "foo", Type: "lan", DockerNetworkName: "cslc-foo-" + clusterID}, + }, + Cluster: "foo", + Datacenter: "foo", + Index: 1, + Workloads: []*Workload{ + { + ID: NewID("zim", "ns1", "ap1"), + Image: "busybox", + Port: 8888, + EnvoyAdminPort: 19000, + NodeVersion: NodeVersionV1, + }, + { + ID: NewID("gir", "ns2", "ap1"), + Image: "busybox", + Port: 8877, + EnvoyAdminPort: 19001, + NodeVersion: NodeVersionV1, + Destinations: []*Destination{{ + ID: NewID("gaz", "ns3", "ap3"), + Cluster: "foo", + LocalAddress: "127.0.0.1", + LocalPort: 5000, + }}, + }, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV2, + Name: "mesh2", + Partition: "ap2", + Images: DefaultImages().ChooseConsul(true).ChooseNode(NodeKindDataplane), + Addresses: []*Address{ + {Network: "foo", Type: "lan", DockerNetworkName: "cslc-foo-" + clusterID}, + }, + Cluster: "foo", + Datacenter: "foo", + Index: 2, + Workloads: []*Workload{ + { + ID: NewID("gir", "ns4", "ap2"), + Image: "busybox", + Ports: map[string]*Port{ + "legacy": {Number: 8877, Protocol: "tcp", ActualProtocol: pbcatalog.Protocol_PROTOCOL_TCP}, + "mesh": {Number: 20000, Protocol: "mesh", ActualProtocol: pbcatalog.Protocol_PROTOCOL_MESH}, + }, + EnvoyAdminPort: 19001, + EnvoyPublicListenerPort: 20000, + NodeVersion: NodeVersionV2, + V2Services: []string{"gir"}, + WorkloadIdentity: "gir", + Workload: "gir-mesh2", + ImpliedDestinations: []*Destination{{ + ID: NewID("gaz", "default", "ap4"), + Cluster: "foo", // TODO: why is this only sometimes populated? + PortName: "www", + Implied: true, + }}, + }, + }, + }, + }, + Partitions: []*Partition{ + { + Name: "ap1", + Namespaces: []string{"default", "ns1", "ns2"}, + }, + { + Name: "ap2", + Namespaces: []string{"default", "ns4"}, + }, + { + Name: "ap3", + Namespaces: []string{"default", "ns3"}, + }, + { + Name: "ap4", + Namespaces: []string{"default"}, + }, + { + Name: "default", + Namespaces: []string{"default"}, + }, + }, + }, + }, + }, + }, + "explicit v2 services": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "node1", + }}, + Services: map[ID]*pbcatalog.Service{ + NewID("zim", "default", "default"): { + Ports: []*pbcatalog.ServicePort{ + {TargetPort: "http"}, + {TargetPort: "mesh", Protocol: pbcatalog.Protocol_PROTOCOL_MESH}, + }, + }, + }, + }}, + }, + expect: &Topology{ + ID: clusterID, + Images: DefaultImages(), + Networks: map[string]*Network{ + "foo": {Name: "foo", Type: "lan", DockerName: "cslc-foo-" + clusterID}, + }, + Clusters: map[string]*Cluster{ + "foo": { + Name: "foo", + NetworkName: "foo", + Datacenter: "foo", + Images: DefaultImages().ChooseConsul(false), + Nodes: []*Node{{ + Kind: NodeKindServer, + Version: NodeVersionV1, + Partition: "default", + Name: "node1", + Images: DefaultImages().ChooseConsul(false).ChooseNode(NodeKindServer), + Addresses: []*Address{ + {Network: "foo", Type: "lan", DockerNetworkName: "cslc-foo-" + clusterID}, + }, + Cluster: "foo", + Datacenter: "foo", + }}, + EnableV2: true, + Services: map[ID]*pbcatalog.Service{ + NewID("zim", "default", "default"): { + Ports: []*pbcatalog.ServicePort{ + {TargetPort: "http", VirtualPort: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_TCP}, + {TargetPort: "mesh", Protocol: pbcatalog.Protocol_PROTOCOL_MESH}, + }, + VirtualIps: []string{"10.244.0.2"}, + }, + }, + Partitions: []*Partition{{ + Name: "default", + Namespaces: []string{"default"}, + }}, + }, + }, + }, + }, + "explicit v2 services/bad workload selector": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "node1", + }}, + Services: map[ID]*pbcatalog.Service{ + NewID("zim", "default", "default"): { + Workloads: &pbcatalog.WorkloadSelector{Names: []string{"zzz"}}, + Ports: []*pbcatalog.ServicePort{ + {TargetPort: "http"}, + {TargetPort: "mesh"}, + }, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": the workloads field for v2 service "default/default/zim" is not user settable`, + }, + "tls volume errantly set": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "node1", + }}, + TLSVolumeName: "foo", + }}, + }, + expectErr: `error building cluster "foo": user cannot specify the TLSVolumeName field`, + }, + "node/no name": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + }}, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "": cluster "foo" node has no name`, + }, + "node/invalid name": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "-123", + }}, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "-123": node name is not valid: -123`, + }, + "node/bad kind": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Name: "zim", + }}, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "zim": cluster "foo" node "zim" has invalid kind`, + }, + "node/bad version": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Version: "v3", + Name: "zim", + }}, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "zim": cluster "foo" node "zim" has invalid version: v3`, + }, + "node/bad version for client": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindClient, + Version: NodeVersionV2, + Name: "zim", + }}, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "zim": v2 does not support client agents at this time`, + }, + "node/invalid partition": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "zim", + Partition: "-123", + }}, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "zim": node partition is not valid: -123`, + }, + "node/invalid usedPorts": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "zim", + usedPorts: map[int]int{5: 6}, + }}, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "zim": user cannot specify the usedPorts field`, + }, + "node/invalid index": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "zim", + Index: 99, + }}, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "zim": user cannot specify the node index`, + }, + "node/missing address network": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "zim", + Addresses: []*Address{{ + // + }}, + }}, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "zim": cluster "foo" node "zim" has invalid address`, + }, + "node/invalid address type": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "zim", + Addresses: []*Address{{ + Network: "foo", + Type: "lan", + }}, + }}, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "zim": user cannot specify the address type directly`, + }, + "node/address network does not exist": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "zim", + Addresses: []*Address{{ + Network: "bar", + }}, + }}, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "zim": cluster "foo" node "zim" uses network name "bar" that does not exist`, + }, + "node/no local addresses": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + {Name: "bar", Type: "wan"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "zim", + Addresses: []*Address{{ + Network: "bar", + }}, + }}, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "zim": cluster "foo" node "zim" has no local addresses`, + }, + "node/too many public addresses": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + {Name: "bar", Type: "wan"}, + {Name: "baz", Type: "wan"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{{ + Kind: NodeKindServer, + Name: "zim", + Addresses: []*Address{ + {Network: "foo"}, + {Network: "bar"}, + {Network: "baz"}, + }, + }}, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "zim": cluster "foo" node "zim" has more than one public address`, + }, + "node/dataplane with more than one workload": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Addresses: []*Address{ + {Network: "foo"}, + }, + Workloads: []*Workload{ + {ID: NewID("zim", "default", "default")}, + {ID: NewID("gir", "default", "default")}, + }, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" uses dataplane, but has more than one service`, + }, + "workload/invalid partition": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: ID{ + Name: "zim", + Namespace: "default", + Partition: "-123", + }, + Image: "busybox", + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": service partition is not valid: -123`, + }, + "workload/invalid namespace": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: ID{ + Name: "zim", + Namespace: "-123", + Partition: "default", + }, + Image: "busybox", + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": service namespace is not valid: -123`, + }, + "workload/invalid name": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: ID{ + Name: "-123", + Namespace: "default", + Partition: "default", + }, + Image: "busybox", + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": service name is not valid: -123`, + }, + "workload/mismatched partitions": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: ID{ + Name: "zim", + Namespace: "default", + Partition: "ap1", + }, + Image: "busybox", + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": service zim on node mesh1 has mismatched partitions: ap1 != default`, + }, + "workload/node collision": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindClient, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{ + { + ID: NewID("zim", "", ""), + Image: "busybox", + Port: 8080, + EnvoyAdminPort: 19000, + }, + { + ID: NewID("zim", "", ""), + Image: "busybox", + Port: 9090, + EnvoyAdminPort: 19001, + }, + }, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cannot have two services on the same node "default/mesh1" in the same cluster "foo" with the same name "default/default/zim"`, + }, + "workload/v1 and implied dest": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindClient, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + Port: 8080, + EnvoyAdminPort: 19000, + ImpliedDestinations: []*Destination{{ + ID: NewID("gir", "", ""), + }}, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": v1 does not support implied destinations yet`, + }, + "workload/default-destination/impl dest need port names in v2": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV2, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + Port: 8080, + EnvoyAdminPort: 19000, + ImpliedDestinations: []*Destination{{ + ID: NewID("gir", "", ""), + }}, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": implicit destinations must use port names in v2`, + }, + "workload/default-destination/expl dest port name legacy defaulting": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV2, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + Port: 8888, + EnvoyAdminPort: 19000, + Destinations: []*Destination{{ + ID: NewID("gir", "", ""), + LocalAddress: "127.0.0.1", + LocalPort: 5000, + }}, + }}, + }, + }, + }}, + }, + expect: &Topology{ + ID: clusterID, + Images: DefaultImages(), + Networks: map[string]*Network{ + "foo": {Name: "foo", Type: "lan", DockerName: "cslc-foo-" + clusterID}, + }, + Clusters: map[string]*Cluster{ + "foo": { + Name: "foo", + NetworkName: "foo", + Datacenter: "foo", + Images: DefaultImages().ChooseConsul(false), + EnableV2: true, + Services: map[ID]*pbcatalog.Service{ + NewID("zim", "default", "default"): { + Workloads: &pbcatalog.WorkloadSelector{ + Names: []string{"zim-mesh1"}, + }, + Ports: []*pbcatalog.ServicePort{ + {TargetPort: "legacy", VirtualPort: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_TCP}, + {TargetPort: "mesh", Protocol: pbcatalog.Protocol_PROTOCOL_MESH}, + }, + VirtualIps: []string{"10.244.0.2"}, + }, + }, + Nodes: []*Node{ + { + Kind: NodeKindServer, + Version: NodeVersionV1, + Partition: "default", + Name: "server1", + Images: DefaultImages().ChooseConsul(false).ChooseNode(NodeKindServer), + Addresses: []*Address{ + {Network: "foo", Type: "lan", DockerNetworkName: "cslc-foo-" + clusterID}, + }, + Cluster: "foo", + Datacenter: "foo", + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV2, + Partition: "default", + Name: "mesh1", + Images: DefaultImages().ChooseConsul(false).ChooseNode(NodeKindDataplane), + Addresses: []*Address{ + {Network: "foo", Type: "lan", DockerNetworkName: "cslc-foo-" + clusterID}, + }, + Cluster: "foo", + Datacenter: "foo", + Index: 1, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + Ports: map[string]*Port{ + "legacy": {Number: 8888, Protocol: "tcp", ActualProtocol: pbcatalog.Protocol_PROTOCOL_TCP}, + "mesh": {Number: 20000, Protocol: "mesh", ActualProtocol: pbcatalog.Protocol_PROTOCOL_MESH}, + }, + EnvoyAdminPort: 19000, + EnvoyPublicListenerPort: 20000, + NodeVersion: NodeVersionV2, + V2Services: []string{"zim"}, + WorkloadIdentity: "zim", + Workload: "zim-mesh1", + Destinations: []*Destination{{ + ID: NewID("gir", "", ""), + LocalAddress: "127.0.0.1", + LocalPort: 5000, + Cluster: "foo", + PortName: "legacy", // <--- this + }}, + }}, + }, + }, + Partitions: []*Partition{{ + Name: "default", + Namespaces: []string{"default"}, + }}, + }, + }, + }, + }, + "workload/default-destination/expl dest local address defaulting": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV1, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + Port: 8080, + EnvoyAdminPort: 19000, + Destinations: []*Destination{{ + ID: NewID("gir", "", ""), + LocalPort: 5000, + }}, + }}, + }, + }, + }}, + }, + expect: &Topology{ + ID: clusterID, + Images: DefaultImages(), + Networks: map[string]*Network{ + "foo": {Name: "foo", Type: "lan", DockerName: "cslc-foo-" + clusterID}, + }, + Clusters: map[string]*Cluster{ + "foo": { + Name: "foo", + NetworkName: "foo", + Datacenter: "foo", + Images: DefaultImages().ChooseConsul(false), + Services: map[ID]*pbcatalog.Service{ + // + }, + Nodes: []*Node{ + { + Kind: NodeKindServer, + Version: NodeVersionV1, + Partition: "default", + Name: "server1", + Images: DefaultImages().ChooseConsul(false).ChooseNode(NodeKindServer), + Addresses: []*Address{ + {Network: "foo", Type: "lan", DockerNetworkName: "cslc-foo-" + clusterID}, + }, + Cluster: "foo", + Datacenter: "foo", + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV1, + Partition: "default", + Name: "mesh1", + Images: DefaultImages().ChooseConsul(false).ChooseNode(NodeKindDataplane), + Addresses: []*Address{ + {Network: "foo", Type: "lan", DockerNetworkName: "cslc-foo-" + clusterID}, + }, + Cluster: "foo", + Datacenter: "foo", + Index: 1, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + Port: 8080, + EnvoyAdminPort: 19000, + EnvoyPublicListenerPort: 20000, + NodeVersion: NodeVersionV1, + Destinations: []*Destination{{ + ID: NewID("gir", "", ""), + LocalAddress: "127.0.0.1", // <--- this + LocalPort: 5000, + Cluster: "foo", + }}, + }}, + }, + }, + Partitions: []*Partition{{ + Name: "default", + Namespaces: []string{"default"}, + }}, + }, + }, + }, + }, + "workload/default-destination/expl dest cannot use port names in v1": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV1, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + Port: 8080, + EnvoyAdminPort: 19000, + Destinations: []*Destination{{ + ID: NewID("gir", "", ""), + PortName: "http", + }}, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": explicit destinations cannot use port names in v1`, + }, + "workload/validate/no name": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindClient, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("", "", ""), + Image: "busybox", + Port: 8080, + EnvoyAdminPort: 19000, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": service name is not valid`, + }, + "workload/validate/no image": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindClient, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Port: 8080, + EnvoyAdminPort: 19000, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: service image is required`, + }, + "workload/validate/no image mesh gateway": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindClient, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Port: 8080, + EnvoyAdminPort: 19000, + IsMeshGateway: true, + }}, + }, + }, + }}, + }, + expect: &Topology{ + ID: clusterID, + Images: DefaultImages(), + Networks: map[string]*Network{ + "foo": {Name: "foo", Type: "lan", DockerName: "cslc-foo-" + clusterID}, + }, + Clusters: map[string]*Cluster{ + "foo": { + Name: "foo", + NetworkName: "foo", + Datacenter: "foo", + Images: DefaultImages().ChooseConsul(false), + Services: map[ID]*pbcatalog.Service{ + // + }, + Nodes: []*Node{ + { + Kind: NodeKindServer, + Version: NodeVersionV1, + Partition: "default", + Name: "server1", + Images: DefaultImages().ChooseConsul(false).ChooseNode(NodeKindServer), + Addresses: []*Address{ + {Network: "foo", Type: "lan", DockerNetworkName: "cslc-foo-" + clusterID}, + }, + Cluster: "foo", + Datacenter: "foo", + }, + { + Kind: NodeKindClient, + Version: NodeVersionV1, + Partition: "default", + Name: "mesh1", + Images: DefaultImages().ChooseConsul(false).ChooseNode(NodeKindClient), + Addresses: []*Address{ + {Network: "foo", Type: "lan", DockerNetworkName: "cslc-foo-" + clusterID}, + }, + Cluster: "foo", + Datacenter: "foo", + Index: 1, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Port: 8080, + EnvoyAdminPort: 19000, + IsMeshGateway: true, + NodeVersion: NodeVersionV1, + }}, + }, + }, + Partitions: []*Partition{{ + Name: "default", + Namespaces: []string{"default"}, + }}, + }, + }, + }, + }, + "workload/validate/single and multiport v2": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Version: NodeVersionV2, + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + Port: 8080, + EnvoyAdminPort: 19000, + Ports: map[string]*Port{ + "blah": { + Number: 8181, + Protocol: "tcp", + }, + }, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: cannot specify both singleport and multiport on service in v2`, + }, + "workload/validate/multiport nil port": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Version: NodeVersionV2, + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + EnvoyAdminPort: 19000, + Ports: map[string]*Port{ + "blah": nil, + }, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: cannot be nil`, + }, + "workload/validate/multiport negative port": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Version: NodeVersionV2, + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + EnvoyAdminPort: 19000, + Ports: map[string]*Port{ + "blah": { + Number: -5, + }, + }, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: service has invalid port number`, + }, + "workload/validate/multiport set actualprotocol": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Version: NodeVersionV2, + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + EnvoyAdminPort: 19000, + Ports: map[string]*Port{ + "blah": { + Number: 8888, + ActualProtocol: pbcatalog.Protocol_PROTOCOL_GRPC, + }, + }, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: user cannot specify ActualProtocol field`, + }, + "workload/validate/multiport invalid port protocol": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Version: NodeVersionV2, + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + EnvoyAdminPort: 19000, + Ports: map[string]*Port{ + "blah": { + Number: 8888, + Protocol: "zzzz", + }, + }, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: service has invalid port protocol "zzzz"`, + }, + "workload/validate/singleport invalid port": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + EnvoyAdminPort: 19000, + Port: -999, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: service has invalid port`, + }, + "workload/validate/singleport tproxy": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + EnvoyAdminPort: 19000, + Port: 999, + EnableTransparentProxy: true, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: tproxy does not work with v1 yet`, + }, + "workload/validate/mesh with no admin port": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + Port: 999, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: envoy admin port is required`, + }, + "workload/validate/no mesh with admin port": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + EnvoyAdminPort: 19000, + DisableServiceMesh: true, + Image: "busybox", + Port: 999, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: cannot use envoy admin port without a service mesh`, + }, + "workload/validate/expl dest with no name": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + EnvoyAdminPort: 19000, + Image: "busybox", + Port: 999, + Destinations: []*Destination{{ + ID: NewID("", "", ""), + }}, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: destination service name is required`, + }, + "workload/validate/expl dest with no local port": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + EnvoyAdminPort: 19000, + Image: "busybox", + Port: 999, + Destinations: []*Destination{{ + ID: NewID("dib", "", ""), + }}, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: destination local port is required`, + }, + "workload/validate/expl dest bad local address": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + EnvoyAdminPort: 19000, + Image: "busybox", + Port: 999, + Destinations: []*Destination{{ + ID: NewID("dib", "", ""), + LocalPort: 5000, + LocalAddress: "clown@address", + }}, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: destination local address is invalid: clown@address`, + }, + "workload/validate/expl dest with implied": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV2, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + EnvoyAdminPort: 19000, + Image: "busybox", + Port: 999, + Destinations: []*Destination{{ + ID: NewID("dib", "", ""), + LocalPort: 5000, + PortName: "http", + Implied: true, + }}, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: implied field cannot be set`, + }, + "workload/validate/impl dest with no name": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV2, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + EnvoyAdminPort: 19000, + Image: "busybox", + Port: 999, + ImpliedDestinations: []*Destination{{ + ID: NewID("", "", ""), + PortName: "http", + }}, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: implied destination service name is required`, + }, + "workload/validate/impl dest with local port": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV2, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + EnvoyAdminPort: 19000, + Image: "busybox", + Port: 999, + ImpliedDestinations: []*Destination{{ + ID: NewID("dib", "", ""), + PortName: "http", + LocalPort: 5000, + }}, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: implied destination local port cannot be set`, + }, + "workload/validate/impl dest with local address": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV2, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + EnvoyAdminPort: 19000, + Image: "busybox", + Port: 999, + ImpliedDestinations: []*Destination{{ + ID: NewID("dib", "", ""), + PortName: "http", + LocalAddress: "127.0.0.1", + }}, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: implied destination local address cannot be set`, + }, + "workload/validate/v1 cannot use multiport": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + EnvoyAdminPort: 19000, + Ports: map[string]*Port{"web": {Number: 8080}}, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: cannot specify multiport on service in v1`, + }, + "workload/validate/disable-mesh/mgw": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + EnvoyAdminPort: 19000, + IsMeshGateway: true, + Port: 8443, + DisableServiceMesh: true, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: cannot disable service mesh and still run a mesh gateway`, + }, + "workload/validate/disable-mesh/expl dest": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV2, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + EnvoyAdminPort: 19000, + Port: 8443, + DisableServiceMesh: true, + Destinations: []*Destination{{ + ID: NewID("gir", "", ""), + }}, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: cannot disable service mesh and configure destinations`, + }, + "workload/validate/disable-mesh/impl dest": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV2, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + EnvoyAdminPort: 19000, + Port: 8443, + DisableServiceMesh: true, + ImpliedDestinations: []*Destination{{ + ID: NewID("gir", "", ""), + PortName: "web", + }}, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: cannot disable service mesh and configure implied destinations`, + }, + "workload/validate/disable-mesh/tproxy": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV2, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + EnvoyAdminPort: 19000, + Port: 8443, + DisableServiceMesh: true, + EnableTransparentProxy: true, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: cannot disable service mesh and activate tproxy`, + }, + "workload/validate/disable-mesh/set admin port": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV2, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + EnvoyAdminPort: 19000, + Port: 8443, + DisableServiceMesh: true, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: cannot use envoy admin port without a service mesh`, + }, + "workload/validate/enable-mesh/unset admin port": { + in: &Config{ + Networks: []*Network{ + {Name: "foo"}, + }, + Clusters: []*Cluster{{ + Name: "foo", + Nodes: []*Node{ + { + Kind: NodeKindServer, + Name: "server1", + Addresses: []*Address{ + {Network: "foo"}, + }, + }, + { + Kind: NodeKindDataplane, + Version: NodeVersionV2, + Name: "mesh1", + Addresses: []*Address{{Network: "foo"}}, + Workloads: []*Workload{{ + ID: NewID("zim", "", ""), + Image: "busybox", + Port: 8443, + }}, + }, + }, + }}, + }, + expectErr: `error building cluster "foo": error compiling node "mesh1": cluster "foo" node "mesh1" service "default/default/zim" is not valid: envoy admin port is required`, + }, + + // TODO: OK: v2 mesh gateway port defaulting + + // TODO: collect tenancies from all places (cluster, initialconfigs, initialres, nodes, services, ...) + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + run(t, tc) + }) + } +} + +type testingT interface { + Helper() + Fatalf(format string, args ...interface{}) + require.TestingT +} + +var ignoreUnexportedTypes = []any{ + Cluster{}, + Images{}, + Node{}, + Workload{}, +} + +func assertDeepEqual[V any](t testingT, exp, got V, msgAndArgs ...any) { + t.Helper() + + if diff := cmp.Diff(exp, got, protocmp.Transform(), cmpopts.IgnoreUnexported(ignoreUnexportedTypes...)); diff != "" { + format := "assertion failed: values are not equal\n--- expected\n+++ actual\n%v" + args := []any{diff} + + if len(msgAndArgs) > 0 { + suffix, ok := msgAndArgs[0].(string) + require.True(t, ok) + require.NotEmpty(t, suffix) + format += "\n\nMessage: " + suffix + args = append(args, msgAndArgs[1:]...) + } + t.Fatalf(format, args...) + } +} diff --git a/testing/deployer/topology/topology.go b/testing/deployer/topology/topology.go index b1395ae78c..a45b27678d 100644 --- a/testing/deployer/topology/topology.go +++ b/testing/deployer/topology/topology.go @@ -17,6 +17,10 @@ import ( "github.com/hashicorp/consul/proto-public/pbresource" ) +const ( + V1DefaultPortName = "legacy" +) + type Topology struct { ID string @@ -898,6 +902,9 @@ func (w *Workload) ports() []int { if len(w.Ports) > 0 { seen := make(map[int]struct{}) for _, port := range w.Ports { + if port == nil { + continue + } if _, ok := seen[port.Number]; !ok { // It's totally fine to expose the same port twice in a workload. 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 { if w.ID.Name == "" { return fmt.Errorf("service name is required") @@ -956,13 +965,16 @@ func (w *Workload) Validate() error { } if w.Port > 0 { w.Ports = map[string]*Port{ - "legacy": { + V1DefaultPortName: { Number: w.Port, Protocol: "tcp", }, } w.Port = 0 } + if w.Ports == nil { + w.Ports = make(map[string]*Port) + } if !w.DisableServiceMesh && w.EnvoyPublicListenerPort > 0 { w.Ports["mesh"] = &Port{ @@ -990,7 +1002,7 @@ func (w *Workload) Validate() error { } } else { 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 { return fmt.Errorf("service has invalid port")