github.com/hernad/nomad@v1.6.112/e2e/e2eutil/consul.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package e2eutil
     5  
     6  import (
     7  	"fmt"
     8  	"testing"
     9  	"time"
    10  
    11  	capi "github.com/hashicorp/consul/api"
    12  	"github.com/hernad/nomad/testutil"
    13  	"github.com/kr/pretty"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  // RequireConsulStatus asserts the aggregate health of the service converges to the expected status.
    19  func RequireConsulStatus(require *require.Assertions, client *capi.Client, namespace, service, expectedStatus string) {
    20  	testutil.WaitForResultRetries(30, func() (bool, error) {
    21  		defer time.Sleep(time.Second) // needs a long time for killing tasks/clients
    22  
    23  		_, status := serviceStatus(require, client, namespace, service)
    24  		return status == expectedStatus, fmt.Errorf("service %s/%s: expected %s but found %s", namespace, service, expectedStatus, status)
    25  	}, func(err error) {
    26  		require.NoError(err, "timedout waiting for consul status")
    27  	})
    28  }
    29  
    30  // serviceStatus gets the aggregate health of the service and returns the []ServiceEntry for further checking.
    31  func serviceStatus(require *require.Assertions, client *capi.Client, namespace, service string) ([]*capi.ServiceEntry, string) {
    32  	services, _, err := client.Health().Service(service, "", false, &capi.QueryOptions{Namespace: namespace})
    33  	require.NoError(err, "expected no error for %s/%s, got %s", namespace, service, err)
    34  	if len(services) > 0 {
    35  		return services, services[0].Checks.AggregatedStatus()
    36  	}
    37  	return nil, "(unknown status)"
    38  }
    39  
    40  // RequireConsulDeregistered asserts that the service eventually is de-registered from Consul.
    41  func RequireConsulDeregistered(require *require.Assertions, client *capi.Client, namespace, service string) {
    42  	testutil.WaitForResultRetries(5, func() (bool, error) {
    43  		defer time.Sleep(time.Second)
    44  
    45  		services, _, err := client.Health().Service(service, "", false, &capi.QueryOptions{Namespace: namespace})
    46  		require.NoError(err)
    47  		if len(services) != 0 {
    48  			return false, fmt.Errorf("service %v: expected empty services but found %v %v", service, len(services), pretty.Sprint(services))
    49  		}
    50  		return true, nil
    51  	}, func(err error) {
    52  		require.NoError(err)
    53  	})
    54  }
    55  
    56  // RequireConsulRegistered assert that the service is registered in Consul.
    57  func RequireConsulRegistered(require *require.Assertions, client *capi.Client, namespace, service string, count int) {
    58  	testutil.WaitForResultRetries(10, func() (bool, error) {
    59  		defer time.Sleep(2 * time.Second)
    60  
    61  		services, _, err := client.Catalog().Service(service, "", &capi.QueryOptions{Namespace: namespace})
    62  		require.NoError(err)
    63  		if len(services) != count {
    64  			return false, fmt.Errorf("service %v: expected %v services but found %v %v", service, count, len(services), pretty.Sprint(services))
    65  		}
    66  		return true, nil
    67  	}, func(err error) {
    68  		require.NoError(err)
    69  	})
    70  }
    71  
    72  // CreateConsulNamespaces will create each namespace in Consul, with a description
    73  // containing the namespace name.
    74  //
    75  // Requires Consul Enterprise.
    76  func CreateConsulNamespaces(t *testing.T, client *capi.Client, namespaces []string) {
    77  	nsClient := client.Namespaces()
    78  
    79  	for _, namespace := range namespaces {
    80  		_, _, err := nsClient.Create(&capi.Namespace{
    81  			Name:        namespace,
    82  			Description: fmt.Sprintf("An e2e namespace called %q", namespace),
    83  		}, nil)
    84  		require.NoError(t, err)
    85  	}
    86  }
    87  
    88  // DeleteConsulNamespaces will delete each namespace from Consul.
    89  //
    90  // Requires Consul Enterprise.
    91  func DeleteConsulNamespaces(t *testing.T, client *capi.Client, namespaces []string) {
    92  	nsClient := client.Namespaces()
    93  
    94  	for _, namespace := range namespaces {
    95  		_, err := nsClient.Delete(namespace, nil)
    96  		assert.NoError(t, err) // be lenient; used in cleanup
    97  	}
    98  }
    99  
   100  // ListConsulNamespaces will list the namespaces in Consul.
   101  //
   102  // Requires Consul Enterprise.
   103  func ListConsulNamespaces(t *testing.T, client *capi.Client) []string {
   104  	nsClient := client.Namespaces()
   105  
   106  	namespaces, _, err := nsClient.List(nil)
   107  	require.NoError(t, err)
   108  
   109  	result := make([]string, 0, len(namespaces))
   110  	for _, namespace := range namespaces {
   111  		result = append(result, namespace.Name)
   112  	}
   113  	return result
   114  }
   115  
   116  // PutConsulKey sets key:value in the Consul KV store under given namespace.
   117  //
   118  // Requires Consul Enterprise.
   119  func PutConsulKey(t *testing.T, client *capi.Client, namespace, key, value string) {
   120  	kvClient := client.KV()
   121  	opts := &capi.WriteOptions{Namespace: namespace}
   122  
   123  	_, err := kvClient.Put(&capi.KVPair{Key: key, Value: []byte(value)}, opts)
   124  	require.NoError(t, err)
   125  }
   126  
   127  // DeleteConsulKey deletes the key from the Consul KV store from given namespace.
   128  //
   129  // Requires Consul Enterprise.
   130  func DeleteConsulKey(t *testing.T, client *capi.Client, namespace, key string) {
   131  	kvClient := client.KV()
   132  	opts := &capi.WriteOptions{Namespace: namespace}
   133  
   134  	_, err := kvClient.Delete(key, opts)
   135  	require.NoError(t, err)
   136  }
   137  
   138  // ReadConsulConfigEntry retrieves the ConfigEntry of the given namespace, kind,
   139  // and name.
   140  //
   141  // Requires Consul Enterprise.
   142  func ReadConsulConfigEntry(t *testing.T, client *capi.Client, namespace, kind, name string) capi.ConfigEntry {
   143  	ceClient := client.ConfigEntries()
   144  	opts := &capi.QueryOptions{Namespace: namespace}
   145  
   146  	ce, _, err := ceClient.Get(kind, name, opts)
   147  	require.NoError(t, err)
   148  	return ce
   149  }
   150  
   151  // DeleteConsulConfigEntry deletes the ConfigEntry of the given namespace, kind,
   152  // and name.
   153  //
   154  // Requires Consul Enterprise.
   155  func DeleteConsulConfigEntry(t *testing.T, client *capi.Client, namespace, kind, name string) {
   156  	ceClient := client.ConfigEntries()
   157  	opts := &capi.WriteOptions{Namespace: namespace}
   158  
   159  	_, err := ceClient.Delete(kind, name, opts)
   160  	require.NoError(t, err)
   161  }
   162  
   163  // ConsulPolicy is used for create Consul ACL policies that Consul ACL tokens
   164  // can make use of.
   165  type ConsulPolicy struct {
   166  	Name  string // e.g. nomad-operator
   167  	Rules string // e.g. service "" { policy="write" }
   168  }
   169  
   170  // CreateConsulPolicy is used to create a Consul ACL policy backed by the given
   171  // ConsulPolicy in the specified namespace.
   172  //
   173  // Requires Consul Enterprise.
   174  func CreateConsulPolicy(t *testing.T, client *capi.Client, namespace string, policy ConsulPolicy) string {
   175  	aclClient := client.ACL()
   176  	opts := &capi.WriteOptions{Namespace: namespace}
   177  
   178  	result, _, err := aclClient.PolicyCreate(&capi.ACLPolicy{
   179  		Name:        policy.Name,
   180  		Rules:       policy.Rules,
   181  		Description: fmt.Sprintf("An e2e test policy %q", policy.Name),
   182  	}, opts)
   183  	require.NoError(t, err, "failed to create consul acl policy")
   184  	return result.ID
   185  }
   186  
   187  // DeleteConsulPolicies is used to delete a set Consul ACL policies from Consul.
   188  //
   189  // Requires Consul Enterprise.
   190  func DeleteConsulPolicies(t *testing.T, client *capi.Client, policies map[string][]string) {
   191  	aclClient := client.ACL()
   192  
   193  	for namespace, policyIDs := range policies {
   194  		opts := &capi.WriteOptions{Namespace: namespace}
   195  		for _, policyID := range policyIDs {
   196  			_, err := aclClient.PolicyDelete(policyID, opts)
   197  			assert.NoError(t, err)
   198  		}
   199  	}
   200  }
   201  
   202  // CreateConsulToken is used to create a Consul ACL token backed by the policy of
   203  // the given policyID in the specified namespace.
   204  //
   205  // Requires Consul Enterprise.
   206  func CreateConsulToken(t *testing.T, client *capi.Client, namespace, policyID string) string {
   207  	aclClient := client.ACL()
   208  	opts := &capi.WriteOptions{Namespace: namespace}
   209  
   210  	token, _, err := aclClient.TokenCreate(&capi.ACLToken{
   211  		Policies:    []*capi.ACLTokenPolicyLink{{ID: policyID}},
   212  		Description: "An e2e test token",
   213  	}, opts)
   214  	require.NoError(t, err, "failed to create consul acl token")
   215  	return token.SecretID
   216  }
   217  
   218  // DeleteConsulTokens is used to delete a set of tokens from Consul.
   219  //
   220  // Requires Consul Enterprise.
   221  func DeleteConsulTokens(t *testing.T, client *capi.Client, tokens map[string][]string) {
   222  	aclClient := client.ACL()
   223  
   224  	for namespace, tokenIDs := range tokens {
   225  		opts := &capi.WriteOptions{Namespace: namespace}
   226  		for _, tokenID := range tokenIDs {
   227  			_, err := aclClient.TokenDelete(tokenID, opts)
   228  			assert.NoError(t, err)
   229  		}
   230  	}
   231  }