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  }