github.com/caos/orbos@v1.5.14-0.20221103111702-e6cd0cea7ad4/internal/operator/orbiter/takeoff.go (about)

     1  package orbiter
     2  
     3  import (
     4  	"errors"
     5  	"net/http"
     6  	"time"
     7  
     8  	"github.com/prometheus/client_golang/prometheus"
     9  	"github.com/prometheus/client_golang/prometheus/promhttp"
    10  	"gopkg.in/yaml.v3"
    11  
    12  	"github.com/caos/orbos/internal/operator/common"
    13  	"github.com/caos/orbos/mntr"
    14  	"github.com/caos/orbos/pkg/git"
    15  	"github.com/caos/orbos/pkg/orb"
    16  	"github.com/caos/orbos/pkg/secret"
    17  	"github.com/caos/orbos/pkg/tree"
    18  )
    19  
    20  func ToEnsureResult(done bool, err error) *EnsureResult {
    21  	return &EnsureResult{
    22  		Err:  err,
    23  		Done: done,
    24  	}
    25  }
    26  
    27  type EnsureResult struct {
    28  	Err  error
    29  	Done bool
    30  }
    31  
    32  type ConfigureFunc func(orb orb.Orb) error
    33  
    34  func NoopConfigure(orb orb.Orb) error {
    35  	return nil
    36  }
    37  
    38  type QueryFunc func(nodeAgentsCurrent *common.CurrentNodeAgents, nodeAgentsDesired *common.DesiredNodeAgents, queried map[string]interface{}) (EnsureFunc, error)
    39  
    40  type EnsureFunc func(pdf func(monitor mntr.Monitor) error) *EnsureResult
    41  
    42  func NoopEnsure(_ func(monitor mntr.Monitor) error) *EnsureResult {
    43  	return &EnsureResult{Done: true}
    44  }
    45  
    46  type event struct {
    47  	commit string
    48  	files  []git.File
    49  }
    50  
    51  func Instrument(monitor mntr.Monitor, healthyChan chan bool) {
    52  	defer func() { monitor.RecoverPanic(recover()) }()
    53  
    54  	healthy := true
    55  
    56  	prometheus.MustRegister(prometheus.NewBuildInfoCollector())
    57  	http.Handle("/metrics", promhttp.Handler())
    58  	http.HandleFunc("/health", func(writer http.ResponseWriter, request *http.Request) {
    59  		msg := "OK"
    60  		status := 200
    61  		if !healthy {
    62  			msg = "ORBITER is not healthy. See the logs."
    63  			status = 404
    64  		}
    65  		writer.WriteHeader(status)
    66  		writer.Write([]byte(msg))
    67  	})
    68  
    69  	go func() {
    70  		timeout := 10 * time.Minute
    71  		ticker := time.NewTimer(timeout)
    72  		for {
    73  			select {
    74  			case newHealthiness := <-healthyChan:
    75  				ticker.Reset(timeout)
    76  				if newHealthiness == healthy {
    77  					continue
    78  				}
    79  				healthy = newHealthiness
    80  				if !newHealthiness {
    81  					monitor.Error(errors.New("ORBITER is unhealthy now"))
    82  					continue
    83  				}
    84  				monitor.Info("ORBITER is healthy now")
    85  			case <-ticker.C:
    86  				monitor.Error(errors.New("ORBITER is unhealthy now as it did not report healthiness for 10 minutes"))
    87  				healthy = false
    88  			}
    89  		}
    90  	}()
    91  
    92  	if err := http.ListenAndServe(":9000", nil); err != nil {
    93  		panic(err)
    94  	}
    95  }
    96  
    97  func Adapt(gitClient *git.Client, monitor mntr.Monitor, finished chan struct{}, adapt AdaptFunc) (QueryFunc, DestroyFunc, ConfigureFunc, bool, *tree.Tree, *tree.Tree, map[string]*secret.Secret, error) {
    98  
    99  	treeDesired, err := gitClient.ReadTree(git.OrbiterFile)
   100  	if err != nil {
   101  		return nil, nil, nil, false, nil, nil, nil, err
   102  	}
   103  	treeCurrent := &tree.Tree{}
   104  
   105  	query, destroy, configure, migrate, secrets, err := adapt(monitor, finished, treeDesired, treeCurrent)
   106  	return query, destroy, configure, migrate, treeDesired, treeCurrent, secrets, err
   107  }
   108  
   109  func Takeoff(monitor mntr.Monitor, conf *Config, healthyChan chan bool) func() {
   110  
   111  	return func() {
   112  
   113  		var err error
   114  		defer func() {
   115  			go func() {
   116  				if err != nil {
   117  					healthyChan <- false
   118  					return
   119  				}
   120  				healthyChan <- true
   121  			}()
   122  		}()
   123  
   124  		query, _, _, migrate, treeDesired, treeCurrent, _, err := Adapt(conf.GitClient, monitor, conf.FinishedChan, conf.Adapt)
   125  		if err != nil {
   126  			monitor.Error(err)
   127  			return
   128  		}
   129  
   130  		desiredNodeAgents := common.NodeAgentsDesiredKind{
   131  			Kind:    "nodeagent.caos.ch/NodeAgents",
   132  			Version: "v0",
   133  			Spec: common.NodeAgentsSpec{
   134  				Commit: conf.OrbiterCommit,
   135  			},
   136  		}
   137  
   138  		marshalCurrentFiles := func() []git.File {
   139  			return []git.File{{
   140  				Path:    "caos-internal/orbiter/current.yml",
   141  				Content: common.MarshalYAML(treeCurrent),
   142  			}, {
   143  				Path:    "caos-internal/orbiter/node-agents-desired.yml",
   144  				Content: common.MarshalYAML(desiredNodeAgents),
   145  			}}
   146  		}
   147  
   148  		if migrate {
   149  			if err = conf.GitClient.PushGitDesiredStates(monitor, "Desired state migrated", []git.GitDesiredState{{
   150  				Desired: treeDesired,
   151  				Path:    git.OrbiterFile,
   152  			}}); err != nil {
   153  				monitor.Error(err)
   154  				return
   155  			}
   156  		}
   157  
   158  		currentNodeAgents := common.NodeAgentsCurrentKind{}
   159  		if err := yaml.Unmarshal(conf.GitClient.Read("caos-internal/orbiter/node-agents-current.yml"), &currentNodeAgents); err != nil {
   160  			monitor.Error(err)
   161  			return
   162  		}
   163  
   164  		handleAdapterError := func(err error) {
   165  			monitor.Error(err)
   166  			if commitErr := conf.GitClient.Commit(mntr.CommitRecord([]*mntr.Field{{Pos: 0, Key: "err", Value: err.Error()}})); commitErr != nil {
   167  				monitor.Error(err)
   168  				return
   169  			}
   170  			monitor.Error(conf.GitClient.Push())
   171  		}
   172  
   173  		ensure, err := query(&currentNodeAgents.Current, &desiredNodeAgents.Spec.NodeAgents, nil)
   174  		if err != nil {
   175  			handleAdapterError(err)
   176  			return
   177  		}
   178  
   179  		if err := conf.GitClient.Clone(); err != nil {
   180  			monitor.Error(err)
   181  			return
   182  		}
   183  
   184  		reconciledCurrentStateMsg := "Current state reconciled"
   185  
   186  		if err := conf.GitClient.UpdateRemote(reconciledCurrentStateMsg, func() []git.File {
   187  			return []git.File{marshalCurrentFiles()[0]}
   188  		}); err != nil {
   189  			monitor.Error(err)
   190  			return
   191  		}
   192  
   193  		result := ensure(conf.GitClient.PushDesiredFunc(git.OrbiterFile, treeDesired))
   194  		if result.Err != nil {
   195  			handleAdapterError(result.Err)
   196  			return
   197  		}
   198  
   199  		if result.Done {
   200  			monitor.Info("Desired state is ensured")
   201  		} else {
   202  			monitor.Info("Desired state is not yet ensured")
   203  		}
   204  
   205  		conf.GitClient.UpdateRemote("Current state changed", func() []git.File {
   206  			pushFiles := marshalCurrentFiles()
   207  			if result.Done {
   208  				var deletedACurrentNodeAgent bool
   209  				for currentNA := range currentNodeAgents.Current.NA {
   210  					if _, ok := desiredNodeAgents.Spec.NodeAgents.NA[currentNA]; !ok {
   211  						currentNodeAgents.Current.NA[currentNA] = nil
   212  						delete(currentNodeAgents.Current.NA, currentNA)
   213  						deletedACurrentNodeAgent = true
   214  					}
   215  				}
   216  				if deletedACurrentNodeAgent {
   217  					monitor.Info("Clearing node agents current states")
   218  					pushFiles = append(pushFiles, git.File{
   219  						Path:    "caos-internal/orbiter/node-agents-current.yml",
   220  						Content: common.MarshalYAML(currentNodeAgents),
   221  					})
   222  				}
   223  			}
   224  			return pushFiles
   225  		})
   226  	}
   227  }