github.com/OpsMx/go-app-base@v0.0.24/birger/controller.go (about) 1 // Copyright 2022 OpsMx, Inc 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package birger 16 17 import ( 18 "bytes" 19 "crypto/tls" 20 "encoding/json" 21 "fmt" 22 "io" 23 "log" 24 "net/http" 25 "net/url" 26 "sync" 27 "time" 28 29 "github.com/OpsMx/go-app-base/httputil" 30 "github.com/OpsMx/go-app-base/util" 31 ) 32 33 // ControllerManager checks the services available on the controller, 34 // and fetches new tokens for newly discovered services. It will 35 // update the ArgoManager with new endpoints, and remove old ones. 36 type ControllerManager struct { 37 UpdateChan chan ServiceUpdate 38 conf Config 39 serviceTypes []string 40 shutdownWorker chan bool 41 shutdownCount sync.WaitGroup 42 updateRate time.Duration 43 healthcheckStatus error 44 services map[string]controllerService 45 } 46 47 type controllerService struct { 48 URL string 49 Name string 50 Type string 51 Annotations map[string]string 52 AgentName string 53 Token string 54 } 55 56 // MakeControllerManager returns a new ControllerManager which will periodically poll 57 // the controller for services, and send 58 func MakeControllerManager(conf Config, serviceTypes []string) *ControllerManager { 59 conf.applyDefaults() 60 m := ControllerManager{ 61 conf: conf, 62 serviceTypes: serviceTypes, 63 shutdownWorker: make(chan bool), 64 updateRate: time.Duration(conf.UpdateFrequencySeconds) * time.Second, 65 services: map[string]controllerService{}, 66 healthcheckStatus: fmt.Errorf("controller is not yet synced"), 67 UpdateChan: make(chan ServiceUpdate, 10), 68 } 69 70 m.shutdownCount.Add(1) 71 go m.worker() 72 73 return &m 74 } 75 76 // Shutdown tells the manager to stop doing updates and causes all 77 // goprocs started to exit as cleanly as possible. 78 func (m *ControllerManager) Shutdown() { 79 m.shutdownWorker <- true 80 close(m.UpdateChan) 81 m.shutdownCount.Wait() 82 } 83 84 func (m *ControllerManager) worker() { 85 // Initialize but stop the timer before it triggers. 86 t := time.NewTimer(1 * time.Hour) 87 t.Stop() 88 89 m.reloadFromController() 90 t.Reset(m.updateRate) 91 92 for { 93 select { 94 case <-m.shutdownWorker: 95 t.Stop() 96 m.shutdownCount.Done() 97 return 98 case <-t.C: 99 m.reloadFromController() 100 t.Reset(m.updateRate) 101 } 102 } 103 } 104 105 func (m *ControllerManager) reloadFromController() { 106 services, err := m.getArgoServices() 107 if err != nil { 108 m.healthcheckStatus = err 109 log.Printf("unable to get argo services from controller: %v", err) 110 return 111 } 112 m.healthcheckStatus = nil 113 114 // compare existing services to the new list. We can assume that if we have an entry, 115 // we do not need to refresh tokens and the URL cannot change when talking to the 116 // controller. If these change, we will want a restart. 117 for key, fetchedService := range services { 118 if svc, found := m.services[key]; found { 119 if annotationsDifferent(svc, fetchedService) { 120 fetchedService.URL = svc.URL 121 fetchedService.Token = svc.Token 122 m.services[key] = fetchedService 123 m.sendUpdate(fetchedService) 124 } 125 continue 126 } 127 url, token, err := m.getTokenAndURL(fetchedService) 128 if err != nil { 129 m.healthcheckStatus = err 130 log.Printf("unable to fetch service credentials from controller: %v", err) 131 return 132 } 133 fetchedService.URL = url 134 fetchedService.Token = token 135 m.services[key] = fetchedService 136 m.sendUpdate(fetchedService) 137 } 138 139 // now, remove any we don't currently see. 140 for key, service := range m.services { 141 if _, found := services[key]; found { 142 continue 143 } 144 m.sendDelete(service) 145 delete(m.services, key) 146 } 147 } 148 149 func annotationsDifferent(a controllerService, b controllerService) bool { 150 if len(a.Annotations) != len(b.Annotations) { 151 return true 152 } 153 for k, v := range a.Annotations { 154 if v != b.Annotations[k] { 155 return true 156 } 157 } 158 return false 159 } 160 161 func (m *ControllerManager) sendUpdate(s controllerService) { 162 m.UpdateChan <- ServiceUpdate{ 163 Operation: "update", 164 Name: s.Name, 165 Type: s.Type, 166 AgentName: s.AgentName, 167 Annotations: s.Annotations, 168 URL: s.URL, 169 Token: s.Token, 170 } 171 } 172 173 func (m *ControllerManager) sendDelete(s controllerService) { 174 m.UpdateChan <- ServiceUpdate{ 175 Operation: "delete", 176 Name: s.Name, 177 Type: s.Type, 178 AgentName: s.AgentName, 179 } 180 } 181 182 // Check returns the last error received during a sync, if any. 183 // Used for a healthcheck status. 184 func (m *ControllerManager) Check() error { 185 return m.healthcheckStatus 186 } 187 188 type connectedAgentsResponse struct { 189 ConnectedAgents []connectedAgent `json:"connectedAgents,omitempty"` 190 } 191 192 type connectedAgent struct { 193 Name string `json:"name,omitempty"` 194 Annnotations map[string]string `json:"annotations,omitempty"` 195 Endpoints []agentEndpoint `json:"endpoints,omitempty"` 196 ConnectedAt int64 `json:"connectedAt,omitempty"` 197 } 198 199 type agentEndpoint struct { 200 Name string `json:"name,omitempty"` 201 Type string `json:"type,omitempty"` 202 Annnotations map[string]string `json:"annotations,omitempty"` 203 Configured bool `json:"configured,omitempty"` 204 } 205 206 type controllerServiceCredentialsRequest struct { 207 AgentName string `json:"agentName,omitempty"` 208 Type string `json:"type,omitempty"` 209 Name string `json:"name,omitempty"` 210 } 211 212 type controllerServiceCredentialResponse struct { 213 AgentName string `json:"agentName,omitempty"` 214 Name string `json:"name,omitempty"` 215 Type string `json:"type,omitempty"` 216 CredentialType string `json:"credentialType,omitempty"` 217 Credential struct { 218 Password string `json:"password,omitempty"` 219 } `json:"credential,omitempty"` 220 URL string `json:"url,omitempty"` 221 } 222 223 func (m *ControllerManager) makeRequest(method string, url string, body io.Reader) (*http.Request, error) { 224 req, err := http.NewRequest(method, url, body) 225 if err != nil { 226 return nil, err 227 } 228 req.Header.Set("content-type", "application/json") 229 req.Header.Set("authorization", "Bearer "+m.conf.Token) 230 return req, nil 231 } 232 233 func (m *ControllerManager) getTokenAndURL(s controllerService) (serviceUrl string, serviceToken string, err error) { 234 url, err := url.JoinPath(m.conf.URL, "/api/v1/generateServiceCredentials") 235 if err != nil { 236 return 237 } 238 239 client, err := m.getTLSClient() 240 if err != nil { 241 return "", "", fmt.Errorf("making TLS client: %v", err) 242 } 243 244 credentialsRequest := controllerServiceCredentialsRequest{ 245 AgentName: s.AgentName, 246 Name: s.Name, 247 Type: s.Type, 248 } 249 250 d, err := json.Marshal(credentialsRequest) 251 if err != nil { 252 return 253 } 254 r := bytes.NewReader(d) 255 req, err := m.makeRequest(http.MethodPost, url, r) 256 if err != nil { 257 return 258 } 259 resp, err := client.Do(req) 260 if err != nil { 261 return "", "", fmt.Errorf("fetching service credentials: %v", err) 262 } 263 defer resp.Body.Close() 264 265 if resp.StatusCode != http.StatusOK { 266 return "", "", fmt.Errorf("fetching service credentials: http status %d", resp.StatusCode) 267 } 268 data, err := io.ReadAll(resp.Body) 269 if err != nil { 270 return "", "", fmt.Errorf("reading body: %v", err) 271 } 272 273 var creds controllerServiceCredentialResponse 274 err = json.Unmarshal(data, &creds) 275 if err != nil { 276 return "", "", fmt.Errorf("cannot decode service credentials JSON: %v", err) 277 } 278 279 return creds.URL, creds.Credential.Password, nil 280 } 281 282 func (m *ControllerManager) getArgoServices() (map[string]controllerService, error) { 283 url, err := url.JoinPath(m.conf.URL, "/api/v1/getAgentStatistics") 284 if err != nil { 285 return map[string]controllerService{}, fmt.Errorf("joining url: %v", err) 286 } 287 288 client, err := m.getTLSClient() 289 if err != nil { 290 return map[string]controllerService{}, fmt.Errorf("making TLS client: %v", err) 291 } 292 293 req, err := m.makeRequest(http.MethodGet, url, nil) 294 if err != nil { 295 return map[string]controllerService{}, fmt.Errorf("making connected agents request: %v", err) 296 } 297 298 resp, err := client.Do(req) 299 if err != nil { 300 return map[string]controllerService{}, fmt.Errorf("fetching connected agents: %v", err) 301 } 302 defer resp.Body.Close() 303 304 if resp.StatusCode != http.StatusOK { 305 return map[string]controllerService{}, fmt.Errorf("fetching connnected agents: http status %d", resp.StatusCode) 306 } 307 data, err := io.ReadAll(resp.Body) 308 if err != nil { 309 return map[string]controllerService{}, fmt.Errorf("reading body: %v", err) 310 } 311 312 return m.parseAgentStatistics(data) 313 } 314 315 type serviceList struct { 316 connectedAt int64 317 endpoints []agentEndpoint 318 } 319 320 func (m *ControllerManager) parseAgentStatistics(data []byte) (map[string]controllerService, error) { 321 var ca connectedAgentsResponse 322 err := json.Unmarshal(data, &ca) 323 if err != nil { 324 return map[string]controllerService{}, fmt.Errorf("cannot decode connected agent JSON: %v", err) 325 } 326 327 newestAgents := map[string]serviceList{} 328 // Find the newest versions of each agent, based on connect time. 329 for _, a := range ca.ConnectedAgents { 330 f, found := newestAgents[a.Name] 331 if !found || f.connectedAt < a.ConnectedAt { 332 newestAgents[a.Name] = serviceList{ 333 connectedAt: a.ConnectedAt, 334 endpoints: a.Endpoints, 335 } 336 } 337 } 338 339 endpoints := map[string]controllerService{} 340 341 for agentName, agent := range newestAgents { 342 for _, ep := range agent.endpoints { 343 if !ep.Configured || !util.Contains(m.serviceTypes, ep.Type) { 344 continue 345 } 346 key := agentName + ":" + ep.Name + ":" + ep.Type 347 endpoints[key] = controllerService{AgentName: agentName, Name: ep.Name, Type: ep.Type, Annotations: ep.Annnotations} 348 } 349 } 350 351 return endpoints, nil 352 } 353 354 func (m *ControllerManager) getTLSClient() (*http.Client, error) { 355 tlsConfig := tls.Config{ 356 MinVersion: tls.VersionTLS13, 357 } 358 return httputil.NewHTTPClient(&tlsConfig), nil 359 }