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 }