github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/e2e/e2eutil/consul.go (about)

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