// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package e2eutil import ( "fmt" "testing" "time" capi "github.com/hashicorp/consul/api" "github.com/hashicorp/nomad/testutil" "github.com/kr/pretty" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // RequireConsulStatus asserts the aggregate health of the service converges to the expected status. func RequireConsulStatus(require *require.Assertions, client *capi.Client, namespace, service, expectedStatus string) { testutil.WaitForResultRetries(30, func() (bool, error) { defer time.Sleep(time.Second) // needs a long time for killing tasks/clients _, status := serviceStatus(require, client, namespace, service) return status == expectedStatus, fmt.Errorf("service %s/%s: expected %s but found %s", namespace, service, expectedStatus, status) }, func(err error) { require.NoError(err, "timedout waiting for consul status") }) } // serviceStatus gets the aggregate health of the service and returns the []ServiceEntry for further checking. func serviceStatus(require *require.Assertions, client *capi.Client, namespace, service string) ([]*capi.ServiceEntry, string) { services, _, err := client.Health().Service(service, "", false, &capi.QueryOptions{Namespace: namespace}) require.NoError(err, "expected no error for %s/%s, got %s", namespace, service, err) if len(services) > 0 { return services, services[0].Checks.AggregatedStatus() } return nil, "(unknown status)" } // RequireConsulDeregistered asserts that the service eventually is de-registered from Consul. func RequireConsulDeregistered(require *require.Assertions, client *capi.Client, namespace, service string) { testutil.WaitForResultRetries(5, func() (bool, error) { defer time.Sleep(time.Second) services, _, err := client.Health().Service(service, "", false, &capi.QueryOptions{Namespace: namespace}) require.NoError(err) if len(services) != 0 { return false, fmt.Errorf("service %v: expected empty services but found %v %v", service, len(services), pretty.Sprint(services)) } return true, nil }, func(err error) { require.NoError(err) }) } // RequireConsulRegistered assert that the service is registered in Consul. func RequireConsulRegistered(require *require.Assertions, client *capi.Client, namespace, service string, count int) { testutil.WaitForResultRetries(10, func() (bool, error) { defer time.Sleep(2 * time.Second) services, _, err := client.Catalog().Service(service, "", &capi.QueryOptions{Namespace: namespace}) require.NoError(err) if len(services) != count { return false, fmt.Errorf("service %v: expected %v services but found %v %v", service, count, len(services), pretty.Sprint(services)) } return true, nil }, func(err error) { require.NoError(err) }) } // CreateConsulNamespaces will create each namespace in Consul, with a description // containing the namespace name. // // Requires Consul Enterprise. func CreateConsulNamespaces(t *testing.T, client *capi.Client, namespaces []string) { nsClient := client.Namespaces() for _, namespace := range namespaces { _, _, err := nsClient.Create(&capi.Namespace{ Name: namespace, Description: fmt.Sprintf("An e2e namespace called %q", namespace), }, nil) require.NoError(t, err) } } // DeleteConsulNamespaces will delete each namespace from Consul. // // Requires Consul Enterprise. func DeleteConsulNamespaces(t *testing.T, client *capi.Client, namespaces []string) { nsClient := client.Namespaces() for _, namespace := range namespaces { _, err := nsClient.Delete(namespace, nil) assert.NoError(t, err) // be lenient; used in cleanup } } // ListConsulNamespaces will list the namespaces in Consul. // // Requires Consul Enterprise. func ListConsulNamespaces(t *testing.T, client *capi.Client) []string { nsClient := client.Namespaces() namespaces, _, err := nsClient.List(nil) require.NoError(t, err) result := make([]string, 0, len(namespaces)) for _, namespace := range namespaces { result = append(result, namespace.Name) } return result } // PutConsulKey sets key:value in the Consul KV store under given namespace. // // Requires Consul Enterprise. func PutConsulKey(t *testing.T, client *capi.Client, namespace, key, value string) { kvClient := client.KV() opts := &capi.WriteOptions{Namespace: namespace} _, err := kvClient.Put(&capi.KVPair{Key: key, Value: []byte(value)}, opts) require.NoError(t, err) } // DeleteConsulKey deletes the key from the Consul KV store from given namespace. // // Requires Consul Enterprise. func DeleteConsulKey(t *testing.T, client *capi.Client, namespace, key string) { kvClient := client.KV() opts := &capi.WriteOptions{Namespace: namespace} _, err := kvClient.Delete(key, opts) require.NoError(t, err) } // ReadConsulConfigEntry retrieves the ConfigEntry of the given namespace, kind, // and name. // // Requires Consul Enterprise. func ReadConsulConfigEntry(t *testing.T, client *capi.Client, namespace, kind, name string) capi.ConfigEntry { ceClient := client.ConfigEntries() opts := &capi.QueryOptions{Namespace: namespace} ce, _, err := ceClient.Get(kind, name, opts) require.NoError(t, err) return ce } // DeleteConsulConfigEntry deletes the ConfigEntry of the given namespace, kind, // and name. // // Requires Consul Enterprise. func DeleteConsulConfigEntry(t *testing.T, client *capi.Client, namespace, kind, name string) { ceClient := client.ConfigEntries() opts := &capi.WriteOptions{Namespace: namespace} _, err := ceClient.Delete(kind, name, opts) require.NoError(t, err) } // ConsulPolicy is used for create Consul ACL policies that Consul ACL tokens // can make use of. type ConsulPolicy struct { Name string // e.g. nomad-operator Rules string // e.g. service "" { policy="write" } } // CreateConsulPolicy is used to create a Consul ACL policy backed by the given // ConsulPolicy in the specified namespace. // // Requires Consul Enterprise. func CreateConsulPolicy(t *testing.T, client *capi.Client, namespace string, policy ConsulPolicy) string { aclClient := client.ACL() opts := &capi.WriteOptions{Namespace: namespace} result, _, err := aclClient.PolicyCreate(&capi.ACLPolicy{ Name: policy.Name, Rules: policy.Rules, Description: fmt.Sprintf("An e2e test policy %q", policy.Name), }, opts) require.NoError(t, err, "failed to create consul acl policy") return result.ID } // DeleteConsulPolicies is used to delete a set Consul ACL policies from Consul. // // Requires Consul Enterprise. func DeleteConsulPolicies(t *testing.T, client *capi.Client, policies map[string][]string) { aclClient := client.ACL() for namespace, policyIDs := range policies { opts := &capi.WriteOptions{Namespace: namespace} for _, policyID := range policyIDs { _, err := aclClient.PolicyDelete(policyID, opts) assert.NoError(t, err) } } } // CreateConsulToken is used to create a Consul ACL token backed by the policy of // the given policyID in the specified namespace. // // Requires Consul Enterprise. func CreateConsulToken(t *testing.T, client *capi.Client, namespace, policyID string) string { aclClient := client.ACL() opts := &capi.WriteOptions{Namespace: namespace} token, _, err := aclClient.TokenCreate(&capi.ACLToken{ Policies: []*capi.ACLTokenPolicyLink{{ID: policyID}}, Description: "An e2e test token", }, opts) require.NoError(t, err, "failed to create consul acl token") return token.SecretID } // DeleteConsulTokens is used to delete a set of tokens from Consul. // // Requires Consul Enterprise. func DeleteConsulTokens(t *testing.T, client *capi.Client, tokens map[string][]string) { aclClient := client.ACL() for namespace, tokenIDs := range tokens { opts := &capi.WriteOptions{Namespace: namespace} for _, tokenID := range tokenIDs { _, err := aclClient.TokenDelete(tokenID, opts) assert.NoError(t, err) } } }