github.com/hernad/nomad@v1.6.112/command/agent/consul/catalog_testing.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package consul
     5  
     6  import (
     7  	"fmt"
     8  	"sort"
     9  	"sync"
    10  
    11  	"github.com/hashicorp/consul/api"
    12  	"github.com/hashicorp/go-hclog"
    13  	"golang.org/x/exp/maps"
    14  	"golang.org/x/exp/slices"
    15  )
    16  
    17  // MockNamespaces is a mock implementation of NamespaceAPI.
    18  type MockNamespaces struct {
    19  	namespaces []*api.Namespace
    20  }
    21  
    22  var _ NamespaceAPI = (*MockNamespaces)(nil)
    23  
    24  // NewMockNamespaces creates a MockNamespaces with the given namespaces, and
    25  // will automatically add the "default" namespace if not included.
    26  func NewMockNamespaces(namespaces []string) *MockNamespaces {
    27  	list := slices.Clone(namespaces)
    28  	if !slices.Contains(list, "default") {
    29  		list = append(list, "default")
    30  	}
    31  	sort.Strings(list)
    32  
    33  	data := make([]*api.Namespace, 0, len(list))
    34  	for _, namespace := range list {
    35  		data = append(data, &api.Namespace{
    36  			Name: namespace,
    37  		})
    38  	}
    39  
    40  	return &MockNamespaces{
    41  		namespaces: data,
    42  	}
    43  }
    44  
    45  // List implements NamespaceAPI
    46  func (m *MockNamespaces) List(*api.QueryOptions) ([]*api.Namespace, *api.QueryMeta, error) {
    47  	result := make([]*api.Namespace, len(m.namespaces))
    48  	copy(result, m.namespaces)
    49  	return result, new(api.QueryMeta), nil
    50  }
    51  
    52  // MockCatalog can be used for testing where the CatalogAPI is needed.
    53  type MockCatalog struct {
    54  	logger hclog.Logger
    55  }
    56  
    57  var _ CatalogAPI = (*MockCatalog)(nil)
    58  
    59  func NewMockCatalog(l hclog.Logger) *MockCatalog {
    60  	return &MockCatalog{logger: l.Named("mock_consul")}
    61  }
    62  
    63  func (m *MockCatalog) Datacenters() ([]string, error) {
    64  	dcs := []string{"dc1"}
    65  	m.logger.Trace("Datacenters()", "dcs", dcs, "error", "nil")
    66  	return dcs, nil
    67  }
    68  
    69  func (m *MockCatalog) Service(service, tag string, q *api.QueryOptions) ([]*api.CatalogService, *api.QueryMeta, error) {
    70  	m.logger.Trace("Services()", "service", service, "tag", tag, "query_options", q)
    71  	return nil, nil, nil
    72  }
    73  
    74  // MockAgent is a fake in-memory Consul backend for ServiceClient.
    75  type MockAgent struct {
    76  	// services tracks what services have been registered, per namespace
    77  	services map[string]map[string]*api.AgentServiceRegistration
    78  
    79  	// checks tracks what checks have been registered, per namespace
    80  	checks map[string]map[string]*api.AgentCheckRegistration
    81  
    82  	// hits is the total number of times agent methods have been called
    83  	hits int
    84  
    85  	// ent indicates whether the agent is mocking an enterprise consul
    86  	ent bool
    87  
    88  	// namespaces indicates whether the agent is mocking consul with namespaces
    89  	// feature enabled
    90  	namespaces bool
    91  
    92  	// mu guards above fields
    93  	mu sync.Mutex
    94  
    95  	// checkTTLS counts calls to UpdateTTL for each check, per namespace
    96  	checkTTLs map[string]map[string]int
    97  
    98  	// What check status to return from Checks()
    99  	checkStatus string
   100  }
   101  
   102  var _ AgentAPI = (*MockAgent)(nil)
   103  
   104  type Features struct {
   105  	Enterprise bool
   106  	Namespaces bool
   107  }
   108  
   109  // NewMockAgent that returns all checks as passing.
   110  func NewMockAgent(f Features) *MockAgent {
   111  	return &MockAgent{
   112  		services:    make(map[string]map[string]*api.AgentServiceRegistration),
   113  		checks:      make(map[string]map[string]*api.AgentCheckRegistration),
   114  		checkTTLs:   make(map[string]map[string]int),
   115  		checkStatus: api.HealthPassing,
   116  
   117  		ent:        f.Enterprise,
   118  		namespaces: f.Namespaces,
   119  	}
   120  }
   121  
   122  // getHits returns how many Consul Agent API calls have been made.
   123  func (c *MockAgent) getHits() int {
   124  	c.mu.Lock()
   125  	defer c.mu.Unlock()
   126  	return c.hits
   127  }
   128  
   129  // SetStatus that Checks() should return. Returns old status value.
   130  func (c *MockAgent) SetStatus(s string) string {
   131  	c.mu.Lock()
   132  	old := c.checkStatus
   133  	c.checkStatus = s
   134  	c.mu.Unlock()
   135  	return old
   136  }
   137  
   138  func (c *MockAgent) Self() (map[string]map[string]interface{}, error) {
   139  	c.mu.Lock()
   140  	defer c.mu.Unlock()
   141  	c.hits++
   142  
   143  	version := "1.9.5"
   144  	build := "1.9.5:22ce6c6a"
   145  	if c.ent {
   146  		version = "1.9.5+ent"
   147  		build = "1.9.5+ent:22ce6c6a"
   148  	}
   149  
   150  	stats := make(map[string]interface{})
   151  	if c.ent {
   152  		if c.namespaces {
   153  			stats = map[string]interface{}{
   154  				"license": map[string]interface{}{
   155  					"features": "Namespaces,",
   156  				},
   157  			}
   158  		}
   159  	}
   160  
   161  	return map[string]map[string]interface{}{
   162  		"Config": {
   163  			"Datacenter": "dc1",
   164  			"NodeName":   "x52",
   165  			"NodeID":     "9e7bf42e-a0b4-61b7-24f9-66dead411f0f",
   166  			"Revision":   "22ce6c6ad",
   167  			"Server":     true,
   168  			"Version":    version,
   169  		},
   170  		"Stats": stats,
   171  		"Member": {
   172  			"Addr":        "127.0.0.1",
   173  			"DelegateCur": 4,
   174  			"DelegateMax": 5,
   175  			"DelegateMin": 2,
   176  			"Name":        "rusty",
   177  			"Port":        8301,
   178  			"ProtocolCur": 2,
   179  			"ProtocolMax": 5,
   180  			"ProtocolMin": 1,
   181  			"Status":      1,
   182  			"Tags": map[string]interface{}{
   183  				"build": build,
   184  			},
   185  		},
   186  		"xDS": {
   187  			"SupportedProxies": map[string]interface{}{
   188  				"envoy": []interface{}{
   189  					"1.14.2",
   190  					"1.13.2",
   191  					"1.12.4",
   192  					"1.11.2",
   193  				},
   194  			},
   195  		},
   196  	}, nil
   197  }
   198  
   199  func getNamespace(q *api.QueryOptions) string {
   200  	if q == nil || q.Namespace == "" {
   201  		return "default"
   202  	}
   203  	return q.Namespace
   204  }
   205  
   206  // ServicesWithFilterOpts implements AgentAPI
   207  func (c *MockAgent) ServicesWithFilterOpts(_ string, q *api.QueryOptions) (map[string]*api.AgentService, error) {
   208  	c.mu.Lock()
   209  	defer c.mu.Unlock()
   210  
   211  	c.hits++
   212  	namespace := getNamespace(q)
   213  
   214  	r := make(map[string]*api.AgentService, len(c.services))
   215  	for k, v := range c.services[namespace] {
   216  		r[k] = &api.AgentService{
   217  			ID:                v.ID,
   218  			Service:           v.Name,
   219  			Tags:              make([]string, len(v.Tags)),
   220  			Meta:              maps.Clone(v.Meta),
   221  			Port:              v.Port,
   222  			Address:           v.Address,
   223  			EnableTagOverride: v.EnableTagOverride,
   224  		}
   225  		copy(r[k].Tags, v.Tags)
   226  	}
   227  	return r, nil
   228  }
   229  
   230  // ChecksWithFilterOpts implements AgentAPI
   231  func (c *MockAgent) ChecksWithFilterOpts(_ string, q *api.QueryOptions) (map[string]*api.AgentCheck, error) {
   232  	c.mu.Lock()
   233  	defer c.mu.Unlock()
   234  
   235  	c.hits++
   236  	namespace := getNamespace(q)
   237  
   238  	r := make(map[string]*api.AgentCheck, len(c.checks))
   239  	for k, v := range c.checks[namespace] {
   240  		r[k] = &api.AgentCheck{
   241  			CheckID:     v.ID,
   242  			Name:        v.Name,
   243  			Status:      c.checkStatus,
   244  			Notes:       v.Notes,
   245  			ServiceID:   v.ServiceID,
   246  			ServiceName: c.services[namespace][v.ServiceID].Name,
   247  		}
   248  	}
   249  	return r, nil
   250  }
   251  
   252  // CheckRegs returns the raw AgentCheckRegistrations registered with this mock
   253  // agent, across all namespaces.
   254  func (c *MockAgent) CheckRegs() []*api.AgentCheckRegistration {
   255  	c.mu.Lock()
   256  	defer c.mu.Unlock()
   257  
   258  	regs := make([]*api.AgentCheckRegistration, 0, len(c.checks))
   259  	for namespace := range c.checks {
   260  		for _, check := range c.checks[namespace] {
   261  			regs = append(regs, check)
   262  		}
   263  	}
   264  	return regs
   265  }
   266  
   267  // CheckRegister implements AgentAPI
   268  func (c *MockAgent) CheckRegister(check *api.AgentCheckRegistration) error {
   269  	c.mu.Lock()
   270  	defer c.mu.Unlock()
   271  	return c.checkRegister(check)
   272  }
   273  
   274  // checkRegister registers a check; c.mu must be held.
   275  func (c *MockAgent) checkRegister(check *api.AgentCheckRegistration) error {
   276  	c.hits++
   277  
   278  	// Consul will set empty Namespace to default, do the same here
   279  	if check.Namespace == "" {
   280  		check.Namespace = "default"
   281  	}
   282  
   283  	if c.checks[check.Namespace] == nil {
   284  		c.checks[check.Namespace] = make(map[string]*api.AgentCheckRegistration)
   285  	}
   286  
   287  	c.checks[check.Namespace][check.ID] = check
   288  
   289  	// Be nice and make checks reachable-by-service
   290  	serviceCheck := check.AgentServiceCheck
   291  
   292  	if c.services[check.Namespace] == nil {
   293  		c.services[check.Namespace] = make(map[string]*api.AgentServiceRegistration)
   294  	}
   295  
   296  	// replace existing check if one with same id already exists
   297  	replace := false
   298  	for i := 0; i < len(c.services[check.Namespace][check.ServiceID].Checks); i++ {
   299  		if c.services[check.Namespace][check.ServiceID].Checks[i].CheckID == check.CheckID {
   300  			c.services[check.Namespace][check.ServiceID].Checks[i] = &check.AgentServiceCheck
   301  			replace = true
   302  			break
   303  		}
   304  	}
   305  
   306  	if !replace {
   307  		c.services[check.Namespace][check.ServiceID].Checks = append(c.services[check.Namespace][check.ServiceID].Checks, &serviceCheck)
   308  	}
   309  	return nil
   310  }
   311  
   312  // CheckDeregisterOpts implements AgentAPI
   313  func (c *MockAgent) CheckDeregisterOpts(checkID string, q *api.QueryOptions) error {
   314  	c.mu.Lock()
   315  	defer c.mu.Unlock()
   316  
   317  	c.hits++
   318  	namespace := getNamespace(q)
   319  
   320  	delete(c.checks[namespace], checkID)
   321  	delete(c.checkTTLs[namespace], checkID)
   322  	return nil
   323  }
   324  
   325  // ServiceRegister implements AgentAPI
   326  func (c *MockAgent) ServiceRegister(service *api.AgentServiceRegistration) error {
   327  	c.mu.Lock()
   328  	defer c.mu.Unlock()
   329  
   330  	c.hits++
   331  
   332  	// Consul will set empty Namespace to default, do the same here
   333  	if service.Namespace == "" {
   334  		service.Namespace = "default"
   335  	}
   336  
   337  	if c.services[service.Namespace] == nil {
   338  		c.services[service.Namespace] = make(map[string]*api.AgentServiceRegistration)
   339  	}
   340  	c.services[service.Namespace][service.ID] = service
   341  
   342  	// as of Nomad v1.4.x registering service now also registers its checks
   343  	for _, check := range service.Checks {
   344  		if err := c.checkRegister(&api.AgentCheckRegistration{
   345  			ID:                check.CheckID,
   346  			Name:              check.Name,
   347  			ServiceID:         service.ID,
   348  			AgentServiceCheck: *check,
   349  			Namespace:         service.Namespace,
   350  		}); err != nil {
   351  			return err
   352  		}
   353  	}
   354  
   355  	return nil
   356  }
   357  
   358  // ServiceDeregisterOpts implements AgentAPI
   359  func (c *MockAgent) ServiceDeregisterOpts(serviceID string, q *api.QueryOptions) error {
   360  	c.mu.Lock()
   361  	defer c.mu.Unlock()
   362  
   363  	c.hits++
   364  	namespace := getNamespace(q)
   365  
   366  	delete(c.services[namespace], serviceID)
   367  
   368  	for k, v := range c.checks[namespace] {
   369  		if v.ServiceID == serviceID {
   370  			delete(c.checks[namespace], k)
   371  			delete(c.checkTTLs[namespace], k)
   372  		}
   373  	}
   374  	return nil
   375  }
   376  
   377  // UpdateTTLOpts implements AgentAPI
   378  func (c *MockAgent) UpdateTTLOpts(id string, output string, status string, q *api.QueryOptions) error {
   379  	c.mu.Lock()
   380  	defer c.mu.Unlock()
   381  
   382  	c.hits++
   383  	namespace := getNamespace(q)
   384  
   385  	checks, nsExists := c.checks[namespace]
   386  	if !nsExists {
   387  		return fmt.Errorf("unknown checks namespace: %q", namespace)
   388  	}
   389  
   390  	check, checkExists := checks[id]
   391  	if !checkExists {
   392  		return fmt.Errorf("unknown check: %s/%s", namespace, id)
   393  	}
   394  
   395  	// Flip initial status to passing
   396  	// todo(shoenig) why not just set to the given status?
   397  	check.Status = "passing"
   398  	c.checkTTLs[namespace][id]++
   399  
   400  	return nil
   401  }
   402  
   403  // a convenience method for looking up a registered service by name
   404  func (c *MockAgent) lookupService(namespace, name string) []*api.AgentServiceRegistration {
   405  	c.mu.Lock()
   406  	defer c.mu.Unlock()
   407  
   408  	var services []*api.AgentServiceRegistration
   409  	for _, service := range c.services[namespace] {
   410  		if service.Name == name {
   411  			services = append(services, service)
   412  		}
   413  	}
   414  	return services
   415  }