Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 11 additions & 43 deletions cmd/agent/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"context"
"errors"
"flag"
"fmt"
"log"
Expand All @@ -15,7 +14,6 @@ import (
"github.com/bigstack-oss/cube-advisor-agent/internal/agent"
"github.com/bigstack-oss/cube-advisor-agent/internal/console"
"github.com/bigstack-oss/cube-advisor-agent/internal/identity"
"github.com/bigstack-oss/cube-advisor-agent/internal/openstack"
"github.com/bigstack-oss/cube-advisor-agent/internal/toolplane"
"github.com/bigstack-oss/cube-advisor-agent/pkg/tunnel"
"github.com/bigstack-oss/cube-advisor-agent/pkg/tunnelproto"
Expand Down Expand Up @@ -91,54 +89,24 @@ func runCmd(args []string) int {
}
opts = append(opts, toolplane.WithProbes(probeRunner))
}
// The cluster's own action level (ADR 0011), read once here so a malformed
// value is one loud line at startup rather than a mystery repeated per
// call. A missing or empty file is not an error and means observe; a word
// that is not a level is an error, and the agent still starts — at observe,
// serving reads — because refusing to run would take diagnosis away from
// the operator at exactly the moment they need it.
level, err := toolplane.ReadLevel(*dir)
if err != nil {
fmt.Fprintf(os.Stderr, "run: %v; serving %s until it is corrected\n", err, level)
}
opts = append(opts, toolplane.WithLevel(level))

reg, err := toolplane.New(toolplane.Allowlist, auditor, opts...)
// Every per-cluster setting the operator configured, through the one path
// that reads them (ADR 0016). A setting absent from that list does not
// exist, which is what stops the next one shipping unwired.
reg, states, err := configure(*dir, toolplane.Allowlist, auditor, opts...)
if err != nil {
fmt.Fprintf(os.Stderr, "run: %v\n", err)
return exitFailed
}

// The OpenStack credential, if the operator has written one. Absent is the
// ordinary state and not an error: an agent with no credential serves
// every read it always did and refuses a create with a message naming the
// missing configuration — which is a different refusal from the action
// level's, and says so.
//
// Wired only when present, so opting in is writing a file, and nothing
// about an existing deployment changes until someone does.
switch cred, err := openstack.ReadCredential(*dir); {
case errors.Is(err, openstack.ErrNoCredential):
// Said once, at startup, because "why did it refuse" is a question
// better answered before it is asked.
fmt.Fprintf(os.Stderr, "run: no OpenStack credential; creates will refuse until one is configured\n")
case err != nil:
// Loud, and still starts: a malformed credential must not take
// diagnosis away from the operator at the moment they need it, which
// is the same argument the action level makes.
fmt.Fprintf(os.Stderr, "run: %v; creates will refuse until it is corrected\n", err)
default:
compute, cerr := openstack.NewCompute(cred)
if cerr != nil {
fmt.Fprintf(os.Stderr, "run: %v; creates will refuse until it is corrected\n", cerr)
break
}
reg.ConfigureWriter(toolplane.BackendOpenStackCompute, compute)
fmt.Fprintf(os.Stderr, "run: OpenStack credential loaded for project %s\n", cred.Project)
// One line per setting, whatever happened to it. A broken setting disables
// what it enables and never more: the agent starts anyway, because
// refusing to run would take diagnosis away from the operator at exactly
// the moment they need it.
for _, st := range states {
fmt.Fprintf(os.Stderr, "run: %s\n", st.line)
}
// Stated at startup, because "what may this assistant do here" is the
// question an operator asks of a log and should not have to infer.
fmt.Fprintf(os.Stderr, "run: action level %s; serving %d tool(s)\n", level, len(reg.Names()))
fmt.Fprintf(os.Stderr, "run: action level %s; serving %d tool(s)\n", reg.Level(), len(reg.Names()))

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
Expand Down
136 changes: 136 additions & 0 deletions cmd/agent/settings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package main

import (
"errors"
"fmt"

"github.com/bigstack-oss/cube-advisor-agent/internal/openstack"
"github.com/bigstack-oss/cube-advisor-agent/internal/toolplane"
)

// A setting is one thing the cluster's operator configures, as a file in the
// agent's own directory (ADR 0016).
//
// The list below is the whole set. configure walks it and nothing else reads
// operator configuration, so a setting absent from the list does not exist.
// That is the point: startup used to be a sequence of hand-written blocks
// where adding one was optional, and two settings that never got their block
// shipped documented, tested and unreachable.
type setting struct {
// name is what an operator sees in the startup line.
name string
// file is the setting's file in the agent's directory, named in messages
// so that "not configured" says which configuration.
file string
// load reads the setting from dir. It always returns a line, and returns
// an opt or an apply when the setting is usable.
load func(dir string) settingState
}

// settingState is what one setting resolved to.
type settingState struct {
// line is the one startup line this setting owes, whatever happened: its
// value, that it is not configured, or why it was rejected.
line string
// broken marks a setting an operator wrote that this agent could not
// honour, as distinct from one they never wrote. Only the first is worth
// shouting about.
broken bool
// opt applies the setting when the registry is built; apply applies it
// afterwards. A setting uses whichever its target needs.
opt toolplane.Option
apply func(*toolplane.Registry)
}

// settings is every per-cluster setting this agent reads. Adding one is adding
// an entry here, and there is no second place to forget.
var settings = []setting{actionLevel, openStackCredential}

// actionLevel is the cluster's own action level (ADR 0011).
//
// Read once at startup so a malformed value is one loud line rather than a
// mystery repeated per call. Absent, empty and whitespace all mean observe and
// are not errors; a word that is not a level is an error, and the level is
// still applied, because ReadLevel answers observe alongside it and serving
// reads beats refusing to start.
var actionLevel = setting{
name: "action level",
file: toolplane.LevelFileName,
load: func(dir string) settingState {
level, err := toolplane.ReadLevel(dir)
st := settingState{opt: toolplane.WithLevel(level)}
if err != nil {
st.broken = true
st.line = fmt.Sprintf("action level: %v; serving %s until it is corrected", err, level)
return st
}
st.line = fmt.Sprintf("action level: %s", level)
return st
},
}

// openStackCredential is the application credential creates are made with.
//
// Absent is the ordinary state of every cluster that has not opted in: it
// refuses creates with a message naming the missing configuration, which is a
// different refusal from the action level's and says so. Wired only when
// present, so opting in is writing a file.
var openStackCredential = setting{
name: "OpenStack credential",
file: openstack.CredentialFileName,
load: func(dir string) settingState {
cred, err := openstack.ReadCredential(dir)
switch {
case errors.Is(err, openstack.ErrNoCredential):
return settingState{line: "OpenStack credential: not configured; creates will refuse until one is"}
case err != nil:
return settingState{
broken: true,
line: fmt.Sprintf("OpenStack credential: %v; creates will refuse until it is corrected", err),
}
}
compute, err := openstack.NewCompute(cred)
if err != nil {
return settingState{
broken: true,
line: fmt.Sprintf("OpenStack credential: %v; creates will refuse until it is corrected", err),
}
}
return settingState{
line: fmt.Sprintf("OpenStack credential: loaded for project %s", cred.Project),
apply: func(r *toolplane.Registry) {
r.ConfigureWriter(toolplane.BackendOpenStackCompute, compute)
},
}
},
}

// configure builds the tool plane from the operator's configuration in dir.
//
// The one path every setting takes. run calls it and reads no operator
// configuration itself; `config check` will call the same function, because a
// second validator would be this design's own defect one level up.
//
// extra carries options that are not operator configuration — the probe plane,
// which a flag enables.
func configure(dir string, tools []toolplane.Tool, audit toolplane.Auditor, extra ...toolplane.Option) (*toolplane.Registry, []settingState, error) {
states := make([]settingState, len(settings))
opts := make([]toolplane.Option, 0, len(extra)+len(settings))
opts = append(opts, extra...)
for i, s := range settings {
states[i] = s.load(dir)
if states[i].opt != nil {
opts = append(opts, states[i].opt)
}
}
reg, err := toolplane.New(tools, audit, opts...)
if err != nil {
return nil, states, err
}
for _, st := range states {
if st.apply != nil {
st.apply(reg)
}
}
return reg, states, nil
}
Loading
Loading