2023-08-11 13:12:13 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
|
2023-06-16 16:58:53 +00:00
|
|
|
package catalogtest
|
|
|
|
|
|
|
|
import (
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
"github.com/hashicorp/consul/internal/catalog"
|
|
|
|
rtest "github.com/hashicorp/consul/internal/resource/resourcetest"
|
2023-09-22 16:51:15 +00:00
|
|
|
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
|
2023-06-16 16:58:53 +00:00
|
|
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
|
|
|
"github.com/hashicorp/consul/sdk/testutil"
|
|
|
|
)
|
|
|
|
|
2023-09-22 16:51:15 +00:00
|
|
|
// RunCatalogV2Beta1LifecycleIntegrationTest intends to excercise functionality of
|
2023-06-16 16:58:53 +00:00
|
|
|
// managing catalog resources over their normal lifecycle where they will be modified
|
|
|
|
// several times, change state etc.
|
2023-09-22 16:51:15 +00:00
|
|
|
func RunCatalogV2Beta1LifecycleIntegrationTest(t *testing.T, client pbresource.ResourceServiceClient) {
|
2023-06-16 16:58:53 +00:00
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
testutil.RunStep(t, "node-lifecycle", func(t *testing.T) {
|
2023-09-22 16:51:15 +00:00
|
|
|
RunCatalogV2Beta1NodeLifecycleIntegrationTest(t, client)
|
2023-06-16 16:58:53 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
testutil.RunStep(t, "workload-lifecycle", func(t *testing.T) {
|
2023-09-22 16:51:15 +00:00
|
|
|
RunCatalogV2Beta1WorkloadLifecycleIntegrationTest(t, client)
|
2023-06-16 16:58:53 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
testutil.RunStep(t, "endpoints-lifecycle", func(t *testing.T) {
|
2023-09-22 16:51:15 +00:00
|
|
|
RunCatalogV2Beta1EndpointsLifecycleIntegrationTest(t, client)
|
2023-06-16 16:58:53 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-09-22 16:51:15 +00:00
|
|
|
// RunCatalogV2Beta1NodeLifecycleIntegrationTest verifies correct functionality of
|
2023-06-16 16:58:53 +00:00
|
|
|
// the node-health controller. This test will exercise the following behaviors:
|
|
|
|
//
|
|
|
|
// * Creating a Node without associated HealthStatuses will mark the node as passing
|
|
|
|
// * Associating a HealthStatus with a Node will cause recomputation of the Health
|
|
|
|
// * Changing HealthStatus to a worse health will cause recomputation of the Health
|
|
|
|
// * Changing HealthStatus to a better health will cause recomputation of the Health
|
|
|
|
// * Deletion of associated HealthStatuses will recompute the Health (back to passing)
|
|
|
|
// * Deletion of the node will cause deletion of associated health statuses
|
2023-09-22 16:51:15 +00:00
|
|
|
func RunCatalogV2Beta1NodeLifecycleIntegrationTest(t *testing.T, client pbresource.ResourceServiceClient) {
|
2023-06-16 16:58:53 +00:00
|
|
|
c := rtest.NewClient(client)
|
|
|
|
|
|
|
|
nodeName := "test-lifecycle"
|
|
|
|
nodeHealthName := "test-lifecycle-node-status"
|
|
|
|
|
|
|
|
// initial node creation
|
2023-09-22 21:50:56 +00:00
|
|
|
node := rtest.Resource(pbcatalog.NodeType, nodeName).
|
2023-06-16 16:58:53 +00:00
|
|
|
WithData(t, &pbcatalog.Node{
|
|
|
|
Addresses: []*pbcatalog.NodeAddress{
|
|
|
|
{Host: "172.16.2.3"},
|
|
|
|
{Host: "198.18.2.3", External: true},
|
|
|
|
},
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// wait for the node health controller to mark the node as healthy
|
|
|
|
c.WaitForStatusCondition(t, node.Id,
|
|
|
|
catalog.NodeHealthStatusKey,
|
|
|
|
catalog.NodeHealthConditions[pbcatalog.Health_HEALTH_PASSING])
|
|
|
|
|
|
|
|
// Its easy enough to simply repeatedly set the health status and it proves
|
|
|
|
// that going both from better to worse health and worse to better all
|
|
|
|
// happen as expected. We leave the health in a warning state to allow for
|
|
|
|
// the subsequent health status deletion to cause the health to go back
|
|
|
|
// to passing.
|
|
|
|
healthChanges := []pbcatalog.Health{
|
|
|
|
pbcatalog.Health_HEALTH_PASSING,
|
|
|
|
pbcatalog.Health_HEALTH_WARNING,
|
|
|
|
pbcatalog.Health_HEALTH_CRITICAL,
|
|
|
|
pbcatalog.Health_HEALTH_MAINTENANCE,
|
|
|
|
pbcatalog.Health_HEALTH_CRITICAL,
|
|
|
|
pbcatalog.Health_HEALTH_WARNING,
|
|
|
|
pbcatalog.Health_HEALTH_PASSING,
|
|
|
|
pbcatalog.Health_HEALTH_WARNING,
|
|
|
|
}
|
|
|
|
|
|
|
|
// This will be set within the loop and used afterwards to delete the health status
|
|
|
|
var nodeHealth *pbresource.Resource
|
|
|
|
|
|
|
|
// Iterate through the various desired health statuses, updating
|
|
|
|
// a HealthStatus resource owned by the node and waiting for
|
|
|
|
// reconciliation at each point
|
|
|
|
for _, health := range healthChanges {
|
|
|
|
// update the health check
|
|
|
|
nodeHealth = setHealthStatus(t, c, node.Id, nodeHealthName, health)
|
|
|
|
|
|
|
|
// wait for reconciliation to kick in and put the node into the right
|
|
|
|
// health status.
|
|
|
|
c.WaitForStatusCondition(t, node.Id,
|
|
|
|
catalog.NodeHealthStatusKey,
|
|
|
|
catalog.NodeHealthConditions[health])
|
|
|
|
}
|
|
|
|
|
|
|
|
// now delete the health status and ensure things go back to passing
|
|
|
|
c.MustDelete(t, nodeHealth.Id)
|
|
|
|
|
|
|
|
// wait for the node health controller to mark the node as healthy
|
|
|
|
c.WaitForStatusCondition(t, node.Id,
|
|
|
|
catalog.NodeHealthStatusKey,
|
|
|
|
catalog.NodeHealthConditions[pbcatalog.Health_HEALTH_PASSING])
|
|
|
|
|
|
|
|
// Add the health status back once more, the actual status doesn't matter.
|
|
|
|
// It just must be owned by the node so that we can show cascading
|
|
|
|
// deletions of owned health statuses working.
|
|
|
|
healthStatus := setHealthStatus(t, c, node.Id, nodeHealthName, pbcatalog.Health_HEALTH_CRITICAL)
|
|
|
|
|
|
|
|
// Delete the node and wait for the health status to be deleted.
|
|
|
|
c.MustDelete(t, node.Id)
|
|
|
|
c.WaitForDeletion(t, healthStatus.Id)
|
|
|
|
}
|
|
|
|
|
2023-09-22 16:51:15 +00:00
|
|
|
// RunCatalogV2Beta1WorkloadLifecycleIntegrationTest verifies correct functionality of
|
2023-06-16 16:58:53 +00:00
|
|
|
// the workload-health controller. This test will exercise the following behaviors:
|
|
|
|
//
|
|
|
|
// - Associating a workload with a node causes recomputation of the health and takes
|
|
|
|
// into account the nodes health
|
|
|
|
// - Modifying the workloads associated node causes health recomputation and takes into
|
|
|
|
// account the new nodes health
|
|
|
|
// - Removal of the node association causes recomputation of health and for no node health
|
|
|
|
// to be taken into account.
|
|
|
|
// - Creating a workload without associated health statuses or node association will
|
|
|
|
// be marked passing
|
|
|
|
// - Creating a workload without associated health statuses but with a node will
|
|
|
|
// inherit its health from the node.
|
|
|
|
// - Changing HealthStatus to a worse health will cause recompuation of the Health
|
|
|
|
// - Changing HealthStatus to a better health will cause recompuation of the Health
|
|
|
|
// - Overall health is computed as the worst health amongst the nodes health and all
|
|
|
|
// of the workloads associated HealthStatuses
|
|
|
|
// - Deletion of the workload will cause deletion of all associated health statuses.
|
2023-09-22 16:51:15 +00:00
|
|
|
func RunCatalogV2Beta1WorkloadLifecycleIntegrationTest(t *testing.T, client pbresource.ResourceServiceClient) {
|
2023-06-16 16:58:53 +00:00
|
|
|
c := rtest.NewClient(client)
|
|
|
|
testutil.RunStep(t, "nodeless-workload", func(t *testing.T) {
|
2023-09-22 16:51:15 +00:00
|
|
|
runV2Beta1NodelessWorkloadLifecycleIntegrationTest(t, c)
|
2023-06-16 16:58:53 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
testutil.RunStep(t, "node-associated-workload", func(t *testing.T) {
|
2023-09-22 16:51:15 +00:00
|
|
|
runV2Beta1NodeAssociatedWorkloadLifecycleIntegrationTest(t, c)
|
2023-06-16 16:58:53 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-09-22 16:51:15 +00:00
|
|
|
// runV2Beta1NodelessWorkloadLifecycleIntegrationTest verifies correct functionality of
|
2023-06-16 16:58:53 +00:00
|
|
|
// the workload-health controller for workloads without node associations. In particular
|
|
|
|
// the following behaviors are being tested
|
|
|
|
//
|
|
|
|
// - Creating a workload without associated health statuses or node association will
|
|
|
|
// be marked passing
|
|
|
|
// - Changing HealthStatus to a worse health will cause recompuation of the Health
|
|
|
|
// - Changing HealthStatus to a better health will cause recompuation of the Health
|
|
|
|
// - Deletion of associated HealthStatus for a nodeless workload will be set back to passing
|
|
|
|
// - Deletion of the workload will cause deletion of all associated health statuses.
|
2023-09-22 16:51:15 +00:00
|
|
|
func runV2Beta1NodelessWorkloadLifecycleIntegrationTest(t *testing.T, c *rtest.Client) {
|
2023-06-16 16:58:53 +00:00
|
|
|
workloadName := "test-lifecycle-workload"
|
|
|
|
workloadHealthName := "test-lifecycle-workload-status"
|
|
|
|
|
|
|
|
// create a workload without a node association or health statuses yet
|
2023-09-22 21:50:56 +00:00
|
|
|
workload := rtest.Resource(pbcatalog.WorkloadType, workloadName).
|
2023-06-16 16:58:53 +00:00
|
|
|
WithData(t, &pbcatalog.Workload{
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "198.18.9.8"},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"http": {Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
|
|
|
|
},
|
|
|
|
Identity: "test-lifecycle",
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// wait for the workload health controller to mark the workload as healthy
|
|
|
|
c.WaitForStatusCondition(t, workload.Id,
|
|
|
|
catalog.WorkloadHealthStatusKey,
|
|
|
|
catalog.WorkloadHealthConditions[pbcatalog.Health_HEALTH_PASSING])
|
|
|
|
|
|
|
|
// We may not need to iterate through all of these states but its easy
|
|
|
|
// enough and quick enough to do so. The general rationale is that we
|
|
|
|
// should move through changing the workloads associated health status
|
|
|
|
// in this progression. We can prove that moving from better to worse
|
|
|
|
// health or worse to better both function correctly.
|
|
|
|
healthChanges := []pbcatalog.Health{
|
|
|
|
pbcatalog.Health_HEALTH_PASSING,
|
|
|
|
pbcatalog.Health_HEALTH_WARNING,
|
|
|
|
pbcatalog.Health_HEALTH_CRITICAL,
|
|
|
|
pbcatalog.Health_HEALTH_MAINTENANCE,
|
|
|
|
pbcatalog.Health_HEALTH_CRITICAL,
|
|
|
|
pbcatalog.Health_HEALTH_WARNING,
|
|
|
|
pbcatalog.Health_HEALTH_PASSING,
|
|
|
|
pbcatalog.Health_HEALTH_WARNING,
|
|
|
|
}
|
|
|
|
|
|
|
|
var workloadHealth *pbresource.Resource
|
|
|
|
// Iterate through the various desired health statuses, updating
|
|
|
|
// a HealthStatus resource owned by the workload and waiting for
|
|
|
|
// reconciliation at each point
|
|
|
|
for _, health := range healthChanges {
|
|
|
|
// update the health status
|
|
|
|
workloadHealth = setHealthStatus(t, c, workload.Id, workloadHealthName, health)
|
|
|
|
|
|
|
|
// wait for reconciliation to kick in and put the workload into
|
|
|
|
// the right health status.
|
|
|
|
c.WaitForStatusCondition(t, workload.Id,
|
|
|
|
catalog.WorkloadHealthStatusKey,
|
|
|
|
catalog.WorkloadHealthConditions[health])
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now delete the health status, things should go back to passing status
|
|
|
|
c.MustDelete(t, workloadHealth.Id)
|
|
|
|
|
|
|
|
// ensure the workloads health went back to passing
|
|
|
|
c.WaitForStatusCondition(t, workload.Id,
|
|
|
|
catalog.WorkloadHealthStatusKey,
|
|
|
|
catalog.WorkloadHealthConditions[pbcatalog.Health_HEALTH_PASSING])
|
|
|
|
|
|
|
|
// Reset the workload health. The actual health is irrelevant, we just want it
|
|
|
|
// to exist to provde that Health Statuses get deleted along with the workload
|
|
|
|
// when its deleted.
|
|
|
|
workloadHealth = setHealthStatus(t, c, workload.Id, workloadHealthName, pbcatalog.Health_HEALTH_WARNING)
|
|
|
|
|
|
|
|
// Delete the workload and wait for the HealthStatus to also be deleted
|
|
|
|
c.MustDelete(t, workload.Id)
|
|
|
|
c.WaitForDeletion(t, workloadHealth.Id)
|
|
|
|
}
|
|
|
|
|
2023-09-22 16:51:15 +00:00
|
|
|
// runV2Beta1NodeAssociatedWorkloadLifecycleIntegrationTest verifies correct functionality of
|
2023-06-16 16:58:53 +00:00
|
|
|
// the workload-health controller. This test will exercise the following behaviors:
|
|
|
|
//
|
|
|
|
// - Associating a workload with a node causes recomputation of the health and takes
|
|
|
|
// into account the nodes health
|
|
|
|
// - Modifying the workloads associated node causes health recomputation and takes into
|
|
|
|
// account the new nodes health
|
|
|
|
// - Removal of the node association causes recomputation of health and for no node health
|
|
|
|
// to be taken into account.
|
|
|
|
// - Creating a workload without associated health statuses but with a node will
|
|
|
|
// inherit its health from the node.
|
|
|
|
// - Overall health is computed as the worst health amongst the nodes health and all
|
|
|
|
// of the workloads associated HealthStatuses
|
2023-09-22 16:51:15 +00:00
|
|
|
func runV2Beta1NodeAssociatedWorkloadLifecycleIntegrationTest(t *testing.T, c *rtest.Client) {
|
2023-06-16 16:58:53 +00:00
|
|
|
workloadName := "test-lifecycle"
|
|
|
|
workloadHealthName := "test-lifecycle"
|
|
|
|
nodeName1 := "test-lifecycle-1"
|
|
|
|
nodeName2 := "test-lifecycle-2"
|
|
|
|
nodeHealthName1 := "test-lifecycle-node-1"
|
|
|
|
nodeHealthName2 := "test-lifecycle-node-2"
|
|
|
|
|
|
|
|
// Insert a some nodes to link the workloads to at various points throughout the test
|
2023-09-22 21:50:56 +00:00
|
|
|
node1 := rtest.Resource(pbcatalog.NodeType, nodeName1).
|
2023-06-16 16:58:53 +00:00
|
|
|
WithData(t, &pbcatalog.Node{
|
|
|
|
Addresses: []*pbcatalog.NodeAddress{{Host: "172.17.9.10"}},
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
2023-09-22 21:50:56 +00:00
|
|
|
node2 := rtest.Resource(pbcatalog.NodeType, nodeName2).
|
2023-06-16 16:58:53 +00:00
|
|
|
WithData(t, &pbcatalog.Node{
|
|
|
|
Addresses: []*pbcatalog.NodeAddress{{Host: "172.17.9.11"}},
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Set some non-passing health statuses for those nodes. Using non-passing will make
|
|
|
|
// it easy to see that changing a passing workloads node association appropriately
|
|
|
|
// impacts the overall workload health.
|
|
|
|
setHealthStatus(t, c, node1.Id, nodeHealthName1, pbcatalog.Health_HEALTH_CRITICAL)
|
|
|
|
setHealthStatus(t, c, node2.Id, nodeHealthName2, pbcatalog.Health_HEALTH_WARNING)
|
|
|
|
|
|
|
|
// Add the workload but don't immediately associate with any node.
|
2023-09-22 21:50:56 +00:00
|
|
|
workload := rtest.Resource(pbcatalog.WorkloadType, workloadName).
|
2023-06-16 16:58:53 +00:00
|
|
|
WithData(t, &pbcatalog.Workload{
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "198.18.9.8"},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"http": {Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
|
|
|
|
},
|
|
|
|
Identity: "test-lifecycle",
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// wait for the workload health controller to mark the workload as healthy
|
|
|
|
c.WaitForStatusCondition(t, workload.Id,
|
|
|
|
catalog.WorkloadHealthStatusKey,
|
|
|
|
catalog.WorkloadHealthConditions[pbcatalog.Health_HEALTH_PASSING])
|
|
|
|
|
|
|
|
// now modify the workload to associate it with node 1 (currently with CRITICAL health)
|
|
|
|
workload = rtest.ResourceID(workload.Id).
|
|
|
|
WithData(t, &pbcatalog.Workload{
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{{Host: "198.18.9.8"}},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{"http": {Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP}},
|
|
|
|
Identity: "test-lifecycle",
|
|
|
|
// this is the only difference from the previous write
|
|
|
|
NodeName: node1.Id.Name,
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// wait for the workload health controller to mark the workload as critical (due to node 1 having critical health)
|
|
|
|
c.WaitForStatusCondition(t, workload.Id,
|
|
|
|
catalog.WorkloadHealthStatusKey,
|
|
|
|
catalog.WorkloadAndNodeHealthConditions[pbcatalog.Health_HEALTH_PASSING][pbcatalog.Health_HEALTH_CRITICAL])
|
|
|
|
|
|
|
|
// Now reassociate the workload with node 2. This should cause recalculation of its health into the warning state
|
|
|
|
workload = rtest.ResourceID(workload.Id).
|
|
|
|
WithData(t, &pbcatalog.Workload{
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{{Host: "198.18.9.8"}},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{"http": {Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP}},
|
|
|
|
Identity: "test-lifecycle",
|
|
|
|
// this is the only difference from the previous write
|
|
|
|
NodeName: node2.Id.Name,
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Wait for the workload health controller to mark the workload as warning (due to node 2 having warning health)
|
|
|
|
c.WaitForStatusCondition(t, workload.Id,
|
|
|
|
catalog.WorkloadHealthStatusKey,
|
|
|
|
catalog.WorkloadAndNodeHealthConditions[pbcatalog.Health_HEALTH_PASSING][pbcatalog.Health_HEALTH_WARNING])
|
|
|
|
|
|
|
|
// Delete the node, this should cause the health to be recalculated as critical because the node association
|
|
|
|
// is broken.
|
|
|
|
c.MustDelete(t, node2.Id)
|
|
|
|
|
|
|
|
// Wait for the workload health controller to mark the workload as critical due to the missing node
|
|
|
|
c.WaitForStatusCondition(t, workload.Id,
|
|
|
|
catalog.WorkloadHealthStatusKey,
|
|
|
|
catalog.WorkloadAndNodeHealthConditions[pbcatalog.Health_HEALTH_PASSING][pbcatalog.Health_HEALTH_CRITICAL])
|
|
|
|
|
|
|
|
// Now fixup the node association to point at node 1
|
|
|
|
workload = rtest.ResourceID(workload.Id).
|
|
|
|
WithData(t, &pbcatalog.Workload{
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{{Host: "198.18.9.8"}},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{"http": {Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP}},
|
|
|
|
Identity: "test-lifecycle",
|
|
|
|
// this is the only difference from the previous write
|
|
|
|
NodeName: node1.Id.Name,
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Also set node 1 health down to WARNING
|
|
|
|
setHealthStatus(t, c, node1.Id, nodeHealthName1, pbcatalog.Health_HEALTH_WARNING)
|
|
|
|
|
|
|
|
// Wait for the workload health controller to mark the workload as warning (due to node 1 having warning health now)
|
|
|
|
c.WaitForStatusCondition(t, workload.Id,
|
|
|
|
catalog.WorkloadHealthStatusKey,
|
|
|
|
catalog.WorkloadAndNodeHealthConditions[pbcatalog.Health_HEALTH_PASSING][pbcatalog.Health_HEALTH_WARNING])
|
|
|
|
|
|
|
|
// Now add a critical workload health check to ensure that both node and workload health are accounted for.
|
|
|
|
setHealthStatus(t, c, workload.Id, workloadHealthName, pbcatalog.Health_HEALTH_CRITICAL)
|
|
|
|
|
|
|
|
// Wait for the workload health to be recomputed and put into the critical status.
|
|
|
|
c.WaitForStatusCondition(t, workload.Id,
|
|
|
|
catalog.WorkloadHealthStatusKey,
|
|
|
|
catalog.WorkloadAndNodeHealthConditions[pbcatalog.Health_HEALTH_CRITICAL][pbcatalog.Health_HEALTH_WARNING])
|
|
|
|
|
|
|
|
// Reset the workloads health to passing. We expect the overall health to go back to warning
|
|
|
|
setHealthStatus(t, c, workload.Id, workloadHealthName, pbcatalog.Health_HEALTH_PASSING)
|
|
|
|
c.WaitForStatusCondition(t, workload.Id,
|
|
|
|
catalog.WorkloadHealthStatusKey,
|
|
|
|
catalog.WorkloadAndNodeHealthConditions[pbcatalog.Health_HEALTH_PASSING][pbcatalog.Health_HEALTH_WARNING])
|
|
|
|
|
|
|
|
// Remove the node association and wait for the health to go back to passing
|
|
|
|
workload = rtest.ResourceID(workload.Id).
|
|
|
|
WithData(t, &pbcatalog.Workload{
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{{Host: "198.18.9.8"}},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{"http": {Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP}},
|
|
|
|
Identity: "test-lifecycle",
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
c.WaitForStatusCondition(t, workload.Id,
|
|
|
|
catalog.WorkloadHealthStatusKey,
|
|
|
|
catalog.WorkloadHealthConditions[pbcatalog.Health_HEALTH_PASSING])
|
|
|
|
}
|
|
|
|
|
2023-09-22 16:51:15 +00:00
|
|
|
// RunCatalogV2Beta1EndpointsLifecycleIntegrationTest verifies the correct functionality of
|
2023-06-16 16:58:53 +00:00
|
|
|
// the endpoints controller. This test will exercise the following behaviors:
|
|
|
|
//
|
|
|
|
// * Services without a selector get marked with status indicating their endpoints are unmanaged
|
|
|
|
// * Services with a selector get marked with status indicating their endpoints are managed
|
|
|
|
// * Deleting a service will delete the associated endpoints (regardless of them being managed or not)
|
|
|
|
// * Moving from managed to unmanaged endpoints will delete the managed endpoints
|
|
|
|
// * Moving from unmanaged to managed endpoints will overwrite any previous endpoints.
|
|
|
|
// * A service with a selector that matches no workloads will still have the endpoints object written.
|
|
|
|
// * Adding ports to a service will recalculate the endpoints
|
|
|
|
// * Removing ports from a service will recalculate the endpoints
|
|
|
|
// * Changing the workload will recalculate the endpoints (ports, addresses, or health)
|
2023-09-22 16:51:15 +00:00
|
|
|
func RunCatalogV2Beta1EndpointsLifecycleIntegrationTest(t *testing.T, client pbresource.ResourceServiceClient) {
|
2023-06-16 16:58:53 +00:00
|
|
|
c := rtest.NewClient(client)
|
|
|
|
serviceName := "test-lifecycle"
|
|
|
|
|
|
|
|
// Create the service without a selector. We should not see endpoints generated but we should see the
|
|
|
|
// status updated to note endpoints are not being managed.
|
2023-09-22 21:50:56 +00:00
|
|
|
service := rtest.Resource(pbcatalog.ServiceType, serviceName).
|
2023-06-16 16:58:53 +00:00
|
|
|
WithData(t, &pbcatalog.Service{
|
|
|
|
Ports: []*pbcatalog.ServicePort{{TargetPort: "http", Protocol: pbcatalog.Protocol_PROTOCOL_HTTP}},
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Wait to ensure the status is updated accordingly
|
|
|
|
c.WaitForStatusCondition(t, service.Id, catalog.EndpointsStatusKey, catalog.EndpointsStatusConditionUnmanaged)
|
|
|
|
|
|
|
|
// Verify that no endpoints were created.
|
2023-09-22 21:50:56 +00:00
|
|
|
endpointsID := rtest.Resource(pbcatalog.ServiceEndpointsType, serviceName).ID()
|
2023-06-16 16:58:53 +00:00
|
|
|
c.RequireResourceNotFound(t, endpointsID)
|
|
|
|
|
|
|
|
// Add some empty endpoints (type validations enforce that they are owned by the service)
|
|
|
|
rtest.ResourceID(endpointsID).
|
|
|
|
WithData(t, &pbcatalog.ServiceEndpoints{}).
|
|
|
|
WithOwner(service.Id).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Now delete the service and ensure that they are cleaned up.
|
|
|
|
c.MustDelete(t, service.Id)
|
|
|
|
c.WaitForDeletion(t, endpointsID)
|
|
|
|
|
|
|
|
// Add some workloads to eventually select by the service
|
|
|
|
|
|
|
|
// api-1 has all ports (http, grpc and mesh). It also has a mixture of Addresses
|
|
|
|
// that select individual ports and one that selects all ports implicitly
|
2023-09-22 21:50:56 +00:00
|
|
|
api1 := rtest.Resource(pbcatalog.WorkloadType, "api-1").
|
2023-06-16 16:58:53 +00:00
|
|
|
WithData(t, &pbcatalog.Workload{
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "127.0.0.1"},
|
|
|
|
{Host: "::1", Ports: []string{"grpc"}},
|
|
|
|
{Host: "127.0.0.2", Ports: []string{"http"}},
|
|
|
|
{Host: "172.17.1.1", Ports: []string{"mesh"}},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"mesh": {Port: 10000, Protocol: pbcatalog.Protocol_PROTOCOL_MESH},
|
|
|
|
"http": {Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
|
|
|
|
"grpc": {Port: 9090, Protocol: pbcatalog.Protocol_PROTOCOL_GRPC},
|
|
|
|
},
|
|
|
|
Identity: "api",
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// api-2 has only grpc and mesh ports. It also has a mixture of Addresses that
|
|
|
|
// select individual ports and one that selects all ports implicitly
|
2023-09-22 21:50:56 +00:00
|
|
|
api2 := rtest.Resource(pbcatalog.WorkloadType, "api-2").
|
2023-06-16 16:58:53 +00:00
|
|
|
WithData(t, &pbcatalog.Workload{
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "127.0.0.1"},
|
|
|
|
{Host: "::1", Ports: []string{"grpc"}},
|
|
|
|
{Host: "172.17.1.2", Ports: []string{"mesh"}},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"mesh": {Port: 10000, Protocol: pbcatalog.Protocol_PROTOCOL_MESH},
|
|
|
|
"grpc": {Port: 9090, Protocol: pbcatalog.Protocol_PROTOCOL_GRPC},
|
|
|
|
},
|
|
|
|
Identity: "api",
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// api-3 has the mesh and HTTP ports. It also has a mixture of Addresses that
|
|
|
|
// select individual ports and one that selects all ports.
|
2023-09-22 21:50:56 +00:00
|
|
|
api3 := rtest.Resource(pbcatalog.WorkloadType, "api-3").
|
2023-06-16 16:58:53 +00:00
|
|
|
WithData(t, &pbcatalog.Workload{
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "127.0.0.1"},
|
|
|
|
{Host: "172.17.1.3", Ports: []string{"mesh"}},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"mesh": {Port: 10000, Protocol: pbcatalog.Protocol_PROTOCOL_MESH},
|
|
|
|
"http": {Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
|
|
|
|
},
|
|
|
|
Identity: "api",
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Now create a service with unmanaged endpoints again
|
2023-09-22 21:50:56 +00:00
|
|
|
service = rtest.Resource(pbcatalog.ServiceType, serviceName).
|
2023-06-16 16:58:53 +00:00
|
|
|
WithData(t, &pbcatalog.Service{
|
|
|
|
Ports: []*pbcatalog.ServicePort{{TargetPort: "http", Protocol: pbcatalog.Protocol_PROTOCOL_HTTP}},
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Inject the endpoints resource. We want to prove that transition from unmanaged to
|
|
|
|
// managed endpoints results in overwriting of the old endpoints
|
|
|
|
rtest.ResourceID(endpointsID).
|
|
|
|
WithData(t, &pbcatalog.ServiceEndpoints{
|
|
|
|
Endpoints: []*pbcatalog.Endpoint{
|
|
|
|
{
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "198.18.1.1", External: true},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"http": {Port: 443, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
|
|
|
|
},
|
|
|
|
HealthStatus: pbcatalog.Health_HEALTH_PASSING,
|
2023-09-07 15:37:15 +00:00
|
|
|
Identity: "api",
|
2023-06-16 16:58:53 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}).
|
|
|
|
WithOwner(service.Id).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Wait to ensure the status is updated accordingly
|
|
|
|
c.WaitForStatusCondition(t, service.Id, catalog.EndpointsStatusKey, catalog.EndpointsStatusConditionUnmanaged)
|
|
|
|
|
|
|
|
// Now move the service to having managed endpoints
|
|
|
|
service = rtest.ResourceID(service.Id).
|
|
|
|
WithData(t, &pbcatalog.Service{
|
|
|
|
Workloads: &pbcatalog.WorkloadSelector{Names: []string{"bar"}},
|
|
|
|
Ports: []*pbcatalog.ServicePort{{TargetPort: "http", Protocol: pbcatalog.Protocol_PROTOCOL_HTTP}},
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Verify that this status is updated to show this service as having managed endpoints
|
|
|
|
c.WaitForStatusCondition(t, service.Id, catalog.EndpointsStatusKey, catalog.EndpointsStatusConditionManaged)
|
|
|
|
|
|
|
|
// Verify that the service endpoints are created. In this case they will be empty
|
|
|
|
verifyServiceEndpoints(t, c, endpointsID, &pbcatalog.ServiceEndpoints{})
|
|
|
|
|
|
|
|
// Rewrite the service to select the API workloads - just select the singular port for now
|
|
|
|
service = rtest.ResourceID(service.Id).
|
|
|
|
WithData(t, &pbcatalog.Service{
|
|
|
|
Workloads: &pbcatalog.WorkloadSelector{Prefixes: []string{"api-"}},
|
|
|
|
Ports: []*pbcatalog.ServicePort{{TargetPort: "http", Protocol: pbcatalog.Protocol_PROTOCOL_HTTP}},
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Wait for the status to be updated. The condition itself will remain unchanged but we are waiting for
|
|
|
|
// the generations to match to know that the endpoints would have been regenerated
|
|
|
|
c.WaitForStatusCondition(t, service.Id, catalog.EndpointsStatusKey, catalog.EndpointsStatusConditionManaged)
|
|
|
|
|
|
|
|
// ensure that api-1 and api-3 are selected but api-2 is excluded due to not having the desired port
|
|
|
|
verifyServiceEndpoints(t, c, endpointsID, &pbcatalog.ServiceEndpoints{
|
|
|
|
Endpoints: []*pbcatalog.Endpoint{
|
|
|
|
{
|
|
|
|
TargetRef: api1.Id,
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "127.0.0.1", Ports: []string{"http"}},
|
|
|
|
{Host: "127.0.0.2", Ports: []string{"http"}},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"http": {Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
|
|
|
|
},
|
|
|
|
HealthStatus: pbcatalog.Health_HEALTH_PASSING,
|
2023-09-07 15:37:15 +00:00
|
|
|
Identity: "api",
|
2023-06-16 16:58:53 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
TargetRef: api3.Id,
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "127.0.0.1", Ports: []string{"http"}},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"http": {Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
|
|
|
|
},
|
|
|
|
HealthStatus: pbcatalog.Health_HEALTH_PASSING,
|
2023-09-07 15:37:15 +00:00
|
|
|
Identity: "api",
|
2023-06-16 16:58:53 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
// Rewrite the service to select the API workloads - changing from selecting the HTTP port to the gRPC port
|
|
|
|
service = rtest.ResourceID(service.Id).
|
|
|
|
WithData(t, &pbcatalog.Service{
|
|
|
|
Workloads: &pbcatalog.WorkloadSelector{Prefixes: []string{"api-"}},
|
|
|
|
Ports: []*pbcatalog.ServicePort{{TargetPort: "grpc", Protocol: pbcatalog.Protocol_PROTOCOL_GRPC}},
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Wait for the status to be updated. The condition itself will remain unchanged but we are waiting for
|
|
|
|
// the generations to match to know that the endpoints would have been regenerated
|
|
|
|
c.WaitForStatusCondition(t, service.Id, catalog.EndpointsStatusKey, catalog.EndpointsStatusConditionManaged)
|
|
|
|
|
|
|
|
// Check that the endpoints were generated as expected
|
|
|
|
verifyServiceEndpoints(t, c, endpointsID, &pbcatalog.ServiceEndpoints{
|
|
|
|
Endpoints: []*pbcatalog.Endpoint{
|
|
|
|
{
|
|
|
|
TargetRef: api1.Id,
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "127.0.0.1", Ports: []string{"grpc"}},
|
|
|
|
{Host: "::1", Ports: []string{"grpc"}},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"grpc": {Port: 9090, Protocol: pbcatalog.Protocol_PROTOCOL_GRPC},
|
|
|
|
},
|
|
|
|
HealthStatus: pbcatalog.Health_HEALTH_PASSING,
|
2023-09-07 15:37:15 +00:00
|
|
|
Identity: "api",
|
2023-06-16 16:58:53 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
TargetRef: api2.Id,
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "127.0.0.1", Ports: []string{"grpc"}},
|
|
|
|
{Host: "::1", Ports: []string{"grpc"}},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"grpc": {Port: 9090, Protocol: pbcatalog.Protocol_PROTOCOL_GRPC},
|
|
|
|
},
|
|
|
|
HealthStatus: pbcatalog.Health_HEALTH_PASSING,
|
2023-09-07 15:37:15 +00:00
|
|
|
Identity: "api",
|
2023-06-16 16:58:53 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
// Update the service to change the ports used. This should result in the workload being removed
|
|
|
|
// from the endpoints
|
|
|
|
rtest.ResourceID(api2.Id).
|
|
|
|
WithData(t, &pbcatalog.Workload{
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "127.0.0.1"},
|
|
|
|
{Host: "::1", Ports: []string{"http"}},
|
|
|
|
{Host: "172.17.1.2", Ports: []string{"mesh"}},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"mesh": {Port: 10000, Protocol: pbcatalog.Protocol_PROTOCOL_MESH},
|
|
|
|
"http": {Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
|
|
|
|
},
|
|
|
|
Identity: "api",
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Verify that api-2 was removed from the service endpoints as it no longer has a grpc port
|
|
|
|
verifyServiceEndpoints(t, c, endpointsID, &pbcatalog.ServiceEndpoints{
|
|
|
|
Endpoints: []*pbcatalog.Endpoint{
|
|
|
|
{
|
|
|
|
TargetRef: api1.Id,
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "127.0.0.1", Ports: []string{"grpc"}},
|
|
|
|
{Host: "::1", Ports: []string{"grpc"}},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"grpc": {Port: 9090, Protocol: pbcatalog.Protocol_PROTOCOL_GRPC},
|
|
|
|
},
|
|
|
|
HealthStatus: pbcatalog.Health_HEALTH_PASSING,
|
2023-09-07 15:37:15 +00:00
|
|
|
Identity: "api",
|
2023-06-16 16:58:53 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
// Remove the ::1 address from workload api1 which should result in recomputing endpoints
|
|
|
|
rtest.ResourceID(api1.Id).
|
|
|
|
WithData(t, &pbcatalog.Workload{
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "127.0.0.1"},
|
|
|
|
{Host: "172.17.1.1", Ports: []string{"mesh"}},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"mesh": {Port: 10000, Protocol: pbcatalog.Protocol_PROTOCOL_MESH},
|
|
|
|
"grpc": {Port: 9090, Protocol: pbcatalog.Protocol_PROTOCOL_GRPC},
|
|
|
|
},
|
|
|
|
Identity: "api",
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Verify that api-1 had its addresses modified appropriately
|
|
|
|
verifyServiceEndpoints(t, c, endpointsID, &pbcatalog.ServiceEndpoints{
|
|
|
|
Endpoints: []*pbcatalog.Endpoint{
|
|
|
|
{
|
|
|
|
TargetRef: api1.Id,
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "127.0.0.1", Ports: []string{"grpc"}},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"grpc": {Port: 9090, Protocol: pbcatalog.Protocol_PROTOCOL_GRPC},
|
|
|
|
},
|
|
|
|
HealthStatus: pbcatalog.Health_HEALTH_PASSING,
|
2023-09-07 15:37:15 +00:00
|
|
|
Identity: "api",
|
2023-06-16 16:58:53 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
// Add a failing health status to the api1 workload to force recomputation of endpoints
|
|
|
|
setHealthStatus(t, c, api1.Id, "api-failed", pbcatalog.Health_HEALTH_CRITICAL)
|
|
|
|
|
|
|
|
// Verify that api-1 within the endpoints has the expected health
|
|
|
|
verifyServiceEndpoints(t, c, endpointsID, &pbcatalog.ServiceEndpoints{
|
|
|
|
Endpoints: []*pbcatalog.Endpoint{
|
|
|
|
{
|
|
|
|
TargetRef: api1.Id,
|
|
|
|
Addresses: []*pbcatalog.WorkloadAddress{
|
|
|
|
{Host: "127.0.0.1", Ports: []string{"grpc"}},
|
|
|
|
},
|
|
|
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
|
|
|
"grpc": {Port: 9090, Protocol: pbcatalog.Protocol_PROTOCOL_GRPC},
|
|
|
|
},
|
|
|
|
HealthStatus: pbcatalog.Health_HEALTH_CRITICAL,
|
2023-09-07 15:37:15 +00:00
|
|
|
Identity: "api",
|
2023-06-16 16:58:53 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
// Move the service to being unmanaged. We should see the ServiceEndpoints being removed.
|
|
|
|
service = rtest.ResourceID(service.Id).
|
|
|
|
WithData(t, &pbcatalog.Service{
|
|
|
|
Ports: []*pbcatalog.ServicePort{{TargetPort: "grpc", Protocol: pbcatalog.Protocol_PROTOCOL_GRPC}},
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Wait for the endpoints controller to inform us that the endpoints are not being managed
|
|
|
|
c.WaitForStatusCondition(t, service.Id, catalog.EndpointsStatusKey, catalog.EndpointsStatusConditionUnmanaged)
|
|
|
|
// Ensure that the managed endpoints were deleted
|
|
|
|
c.WaitForDeletion(t, endpointsID)
|
|
|
|
|
|
|
|
// Put the service back into managed mode.
|
|
|
|
service = rtest.ResourceID(service.Id).
|
|
|
|
WithData(t, &pbcatalog.Service{
|
|
|
|
Workloads: &pbcatalog.WorkloadSelector{Prefixes: []string{"api-"}},
|
|
|
|
Ports: []*pbcatalog.ServicePort{{TargetPort: "grpc", Protocol: pbcatalog.Protocol_PROTOCOL_GRPC}},
|
|
|
|
}).
|
|
|
|
Write(t, c)
|
|
|
|
|
|
|
|
// Wait for the service endpoints to be regenerated
|
|
|
|
c.WaitForStatusCondition(t, service.Id, catalog.EndpointsStatusKey, catalog.EndpointsStatusConditionManaged)
|
|
|
|
c.RequireResourceExists(t, endpointsID)
|
|
|
|
|
|
|
|
// Now delete the service and ensure that the endpoints eventually are deleted as well
|
|
|
|
c.MustDelete(t, service.Id)
|
|
|
|
c.WaitForDeletion(t, endpointsID)
|
|
|
|
}
|
|
|
|
|
|
|
|
func setHealthStatus(t *testing.T, client *rtest.Client, owner *pbresource.ID, name string, health pbcatalog.Health) *pbresource.Resource {
|
2023-09-22 21:50:56 +00:00
|
|
|
return rtest.Resource(pbcatalog.HealthStatusType, name).
|
2023-06-16 16:58:53 +00:00
|
|
|
WithData(t, &pbcatalog.HealthStatus{
|
|
|
|
Type: "synthetic",
|
|
|
|
Status: health,
|
|
|
|
}).
|
|
|
|
WithOwner(owner).
|
|
|
|
Write(t, client)
|
|
|
|
}
|