UI : vocabulaire unifié, comptes + mots de passe, choix par élément, clé AES auto
This commit is contained in:
1 parent
ee8343c69f
commit
2ca2f87307
19 files changed
+377
-143
No files matched your search
@@ -72,6 +72,36 @@ var KindDeps = map[model.Kind][]model.Kind{
|
||||
model.KindUser: {model.KindGroup},
|
||||
}
|
||||
|
||||
// KindBundles are kinds chosen as one item: the accounts of the pve realm go
|
||||
// with their passwords (the API holds the accounts, shadow.cfg the passwords).
|
||||
var KindBundles = [][]model.Kind{{model.KindUser, model.KindPassword}}
|
||||
|
||||
// Bundle adds to a list of kinds the other kinds of their bundles, in the
|
||||
// order of model.Kinds.
|
||||
func Bundle(kinds []model.Kind) []model.Kind {
|
||||
in := map[model.Kind]bool{}
|
||||
for _, k := range kinds {
|
||||
in[k] = true
|
||||
}
|
||||
for _, b := range KindBundles {
|
||||
for _, k := range b {
|
||||
if in[k] {
|
||||
for _, x := range b {
|
||||
in[x] = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
out := []model.Kind{}
|
||||
for _, k := range model.Kinds {
|
||||
if in[k] {
|
||||
out = append(out, k)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Sensitive is implemented by adapters whose fields must never be shown nor
|
||||
// journaled (password hashes): they are replaced by Redact.
|
||||
type Sensitive interface {
|
||||
@@ -102,7 +132,7 @@ func Redact(v any) any {
|
||||
return v
|
||||
}
|
||||
sum := sha256.Sum256([]byte(s))
|
||||
return "<masqué " + hex.EncodeToString(sum[:3]) + ">"
|
||||
return "<hidden " + hex.EncodeToString(sum[:3]) + ">"
|
||||
}
|
||||
|
||||
// Parents returns the keys of the objects o needs on the target.
|
||||
|
||||
@@ -56,7 +56,7 @@ func Start(ctx context.Context, st *store.Store) (*Clusters, error) {
|
||||
ref.Roles["ConfSyncOps"] = "Sys.Audit,VM.Audit,VM.PowerMgmt"
|
||||
ref.Users["alice@pve"] = map[string]any{"enable": 1, "firstname": "Alice", "email": "alice@example.org", "groups": "admins"}
|
||||
ref.Users["bob@pve"] = map[string]any{"enable": 1, "firstname": "Bob", "email": "bob@example.org", "groups": "ops", "expire": 1893456000}
|
||||
ref.Users["carol@pam"] = map[string]any{"enable": 1, "comment": "compte Linux, non répliqué"}
|
||||
ref.Users["carol@pam"] = map[string]any{"enable": 1, "comment": "Linux account, not replicated"}
|
||||
ref.Shadow["alice"] = "$5$Kd1Xa9$Q8yJdemoAliceHashOnlyForTheDemo0123456789AB"
|
||||
ref.Shadow["bob"] = "$5$Pq7Zr2$W3vLdemoBobHashOnlyForTheDemo0123456789ABCDE"
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ func ApplyWithLog(ctx context.Context, c *pve.Client, plan Plan, notify func(Ste
|
||||
err := c.Post(ctx, "cluster/sdn/lock", nil, &token)
|
||||
res.log("sdn lock", err)
|
||||
if err != nil {
|
||||
res.Err = fmt.Errorf("SDN lock (pending changes on the client?): %w", err)
|
||||
res.Err = fmt.Errorf("SDN lock (pending changes on the target?): %w", err)
|
||||
return res
|
||||
}
|
||||
rollback := func(cause error) {
|
||||
|
||||
@@ -158,13 +158,13 @@ func MakePlan(cfg *config.Config, cl *config.Client, ref, actual []model.Object)
|
||||
o, exists := cur[r.ID]
|
||||
if !exists {
|
||||
if a.Singleton() {
|
||||
plan.Errors = append(plan.Errors, fmt.Sprintf("%s: not found on client", r.Key()))
|
||||
plan.Errors = append(plan.Errors, fmt.Sprintf("%s: not found on the target", r.Key()))
|
||||
continue
|
||||
}
|
||||
for _, f := range a.Required(model.Object{Kind: k, ID: r.ID, Fields: desired}) {
|
||||
if _, ok := desired[f]; !ok {
|
||||
plan.Errors = append(plan.Errors,
|
||||
fmt.Sprintf("%s: field %q required for creation, add it to the mapping of client %s", r.Key(), f, cl.Name))
|
||||
fmt.Sprintf("%s: field %q required for creation, add it to the mapping of target %s", r.Key(), f, cl.Name))
|
||||
}
|
||||
}
|
||||
plan.Changes = append(plan.Changes, Change{Kind: k, ID: r.ID, Op: OpCreate, Set: desired,
|
||||
|
||||
@@ -151,3 +151,37 @@ func (p Plan) Digest() string {
|
||||
sum := sha256.Sum256(b)
|
||||
return hex.EncodeToString(sum[:12])
|
||||
}
|
||||
|
||||
// Skip removes from a plan the changes of the kinds left out of a replication
|
||||
// ("not synchronized" in the dry-run), then keeps what the remaining changes
|
||||
// need as Filter does: a group to create for an account stays in the plan.
|
||||
// keys narrows the plan to some objects first (empty: every object).
|
||||
func Skip(p Plan, keys []string, skip []model.Kind) Plan {
|
||||
if len(skip) == 0 {
|
||||
return Filter(p, keys)
|
||||
}
|
||||
out := map[model.Kind]bool{}
|
||||
for _, k := range skip {
|
||||
out[k] = true
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
for _, c := range p.Changes {
|
||||
keys = append(keys, c.Key())
|
||||
}
|
||||
for _, e := range p.Errors {
|
||||
if k, ok := ErrorKey(e); ok {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
var kept []string
|
||||
for _, k := range keys {
|
||||
if kind, _, _ := strings.Cut(k, "/"); !out[model.Kind(kind)] {
|
||||
kept = append(kept, k)
|
||||
}
|
||||
}
|
||||
if len(kept) == 0 {
|
||||
return Plan{Client: p.Client, Warnings: p.Warnings}
|
||||
}
|
||||
return Filter(p, kept)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"pve-confsync/internal/model"
|
||||
)
|
||||
|
||||
// Items left out of a run ("do not replicate") leave the plan, except what the
|
||||
// remaining changes need: the group to create for a new account.
|
||||
func TestSkipKinds(t *testing.T) {
|
||||
p := Plan{Client: "c", Changes: []Change{
|
||||
{Kind: model.KindGroup, ID: "ops", Op: OpCreate, Set: map[string]any{"comment": "Ops"}},
|
||||
{Kind: model.KindGroup, ID: "dev", Op: OpUpdate, Set: map[string]any{"comment": "Dev"}},
|
||||
{Kind: model.KindUser, ID: "bob@pve", Op: OpCreate, Set: map[string]any{"groups": "ops"}},
|
||||
{Kind: model.KindPassword, ID: "bob@pve", Op: OpCreate, Set: map[string]any{"hash": "$5$x"}},
|
||||
{Kind: model.KindVNet, ID: "v1", Op: OpUpdate, Set: map[string]any{"alias": "a"}},
|
||||
}, Errors: []string{"sdn-zone/z1: field \"bridge\" required for creation, add it to the mapping of target c"}}
|
||||
keys := func(p Plan) []string {
|
||||
var out []string
|
||||
for _, c := range p.Changes {
|
||||
out = append(out, c.Key())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
got := Skip(p, nil, []model.Kind{model.KindGroup, model.KindZone, model.KindVNet})
|
||||
if want := []string{"pve-group/ops", "pve-user/bob@pve", "pve-password/bob@pve"}; !reflect.DeepEqual(keys(got), want) {
|
||||
t.Fatalf("changes: %v, want %v", keys(got), want)
|
||||
}
|
||||
if len(got.Errors) != 0 {
|
||||
t.Fatalf("the error of a skipped kind must not block: %v", got.Errors)
|
||||
}
|
||||
if got := Skip(p, nil, model.Kinds); len(got.Changes) != 0 || len(got.Errors) != 0 {
|
||||
t.Fatalf("everything skipped: %+v", got)
|
||||
}
|
||||
if got := Skip(p, nil, nil); len(got.Changes) != len(p.Changes) || len(got.Errors) != 1 {
|
||||
t.Fatalf("nothing skipped: %+v", got)
|
||||
}
|
||||
}
|
||||
+45
-45
@@ -88,11 +88,15 @@ type selection struct {
|
||||
Targets []string `json:"targets"` // global scope: the targets (empty: all of them)
|
||||
Client string `json:"client"` // client and object scopes
|
||||
Objects []string `json:"objects"` // object scope: "kind/id"
|
||||
Skip []model.Kind `json:"skip"` // kinds left out of this run ("not synchronized")
|
||||
Digests map[string]string `json:"digests"` // apply: digest of each reviewed plan
|
||||
Confirm string `json:"confirm"` // apply with SDN changes: target name (primary name for global)
|
||||
}
|
||||
|
||||
func (sel *selection) check() error {
|
||||
if len(sel.Skip) > 0 {
|
||||
sel.Skip = adapter.Bundle(sel.Skip)
|
||||
}
|
||||
switch sel.Scope {
|
||||
case "global":
|
||||
sel.Client, sel.Objects = "", nil
|
||||
@@ -137,7 +141,7 @@ func (s *Server) plan(ctx context.Context, sel selection) (*config.Config, *serv
|
||||
defer cancel()
|
||||
st := service.Analyze(rctx, cfg, sel.clients())
|
||||
s.remember(st, sel.all())
|
||||
return cfg, st, st.Plans(sel.Objects), nil
|
||||
return cfg, st, st.Plans(sel.Objects, sel.Skip), nil
|
||||
}
|
||||
|
||||
type runDetail struct {
|
||||
@@ -190,16 +194,25 @@ func planClients(plans []service.ClientPlan) []string {
|
||||
}
|
||||
|
||||
func target(sel selection) string {
|
||||
switch sel.Scope {
|
||||
case "client":
|
||||
return sel.Client
|
||||
case "object":
|
||||
return sel.Client + ": " + strings.Join(sel.Objects, ", ")
|
||||
var t string
|
||||
switch {
|
||||
case sel.Scope == "client":
|
||||
t = sel.Client
|
||||
case sel.Scope == "object":
|
||||
t = sel.Client + ": " + strings.Join(sel.Objects, ", ")
|
||||
case len(sel.Targets) > 0:
|
||||
t = "targets " + strings.Join(sel.Targets, ", ")
|
||||
default:
|
||||
t = "all targets"
|
||||
}
|
||||
if len(sel.Targets) > 0 {
|
||||
return "cibles " + strings.Join(sel.Targets, ", ")
|
||||
if len(sel.Skip) > 0 {
|
||||
var ks []string
|
||||
for _, k := range sel.Skip {
|
||||
ks = append(ks, string(k))
|
||||
}
|
||||
t += " (not replicated: " + strings.Join(ks, ", ") + ")"
|
||||
}
|
||||
return "toutes les cibles"
|
||||
return t
|
||||
}
|
||||
|
||||
// dryRunSummary counts the changes: "3 changes on 2 clients", or the errors.
|
||||
@@ -217,11 +230,11 @@ func dryRunSummary(plans []service.ClientPlan) (string, string) {
|
||||
}
|
||||
switch {
|
||||
case blocked > 0:
|
||||
return store.RunBlocked, fmt.Sprintf("%d changement(s) sur %d cible(s), %d cible(s) en erreur", changes, clients, blocked)
|
||||
return store.RunBlocked, fmt.Sprintf("%d change(s) on %d target(s), %d target(s) in error", changes, clients, blocked)
|
||||
case changes == 0:
|
||||
return store.RunNoop, "aucun écart"
|
||||
return store.RunNoop, "no difference"
|
||||
}
|
||||
return store.RunOK, fmt.Sprintf("%d changement(s) sur %d cible(s)", changes, clients)
|
||||
return store.RunOK, fmt.Sprintf("%d change(s) on %d target(s)", changes, clients)
|
||||
}
|
||||
|
||||
// handleApply recomputes the plans, checks that they are the ones reviewed in the
|
||||
@@ -239,7 +252,7 @@ func (s *Server) handleApply(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
id := identity(r)
|
||||
if !s.applyMu.TryLock() {
|
||||
writeError(w, http.StatusConflict, "une synchronisation est déjà en cours", "busy")
|
||||
writeError(w, http.StatusConflict, "a replication is already running", "busy")
|
||||
return
|
||||
}
|
||||
started := false
|
||||
@@ -266,8 +279,8 @@ func (s *Server) handleApply(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusConflict, p.Client+": "+strings.Join(p.Plan.Errors, "; "), "plan")
|
||||
return
|
||||
case sel.Digests[p.Client] != p.Digest:
|
||||
s.audit(r, id, "apply", target(sel), false, p.Client+": l'état a changé depuis le dry-run")
|
||||
writeJSON(w, http.StatusConflict, map[string]any{"error": p.Client + ": l'état a changé depuis le dry-run, relancer le dry-run",
|
||||
s.audit(r, id, "apply", target(sel), false, p.Client+": the state changed since the dry-run")
|
||||
writeJSON(w, http.StatusConflict, map[string]any{"error": p.Client + ": the state changed since the dry-run, run it again",
|
||||
"code": "stale", "plans": plans})
|
||||
return
|
||||
case p.Plan.Empty():
|
||||
@@ -277,7 +290,7 @@ func (s *Server) handleApply(w http.ResponseWriter, r *http.Request) {
|
||||
todo = append(todo, p)
|
||||
}
|
||||
if len(todo) == 0 {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"run": 0, "summary": "aucun écart"})
|
||||
writeJSON(w, http.StatusOK, map[string]any{"run": 0, "summary": "no difference"})
|
||||
return
|
||||
}
|
||||
if sdn {
|
||||
@@ -286,7 +299,7 @@ func (s *Server) handleApply(w http.ResponseWriter, r *http.Request) {
|
||||
want = cfg.Reference.Name
|
||||
}
|
||||
if sel.Confirm != want {
|
||||
writeError(w, http.StatusBadRequest, "changements SDN : confirmation requise ("+want+")", "confirm")
|
||||
writeError(w, http.StatusBadRequest, "SDN changes: confirmation required ("+want+")", "confirm")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -333,7 +346,7 @@ func (s *Server) runApply(r *http.Request, id Identity, runID int64, sel selecti
|
||||
res := det.Results[p.Client]
|
||||
res.Status = store.RunRunning
|
||||
save(store.RunRunning, fmt.Sprintf("%s en cours", p.Client))
|
||||
err := st.Apply(ctx, p, sel.Objects, func(step engine.Step) {
|
||||
err := st.Apply(ctx, p, sel.Objects, sel.Skip, func(step engine.Step) {
|
||||
mu.Lock()
|
||||
res.Steps = append(res.Steps, step)
|
||||
mu.Unlock()
|
||||
@@ -347,14 +360,14 @@ func (s *Server) runApply(r *http.Request, id Identity, runID int64, sel selecti
|
||||
} else {
|
||||
done++
|
||||
res.Status = store.RunOK
|
||||
s.audit(r, id, "apply", p.Client, true, fmt.Sprintf("%d changement(s), vérifié", len(p.Plan.Changes)))
|
||||
s.audit(r, id, "apply", p.Client, true, fmt.Sprintf("%d change(s), verified", len(p.Plan.Changes)))
|
||||
}
|
||||
}
|
||||
status := store.RunOK
|
||||
summary := fmt.Sprintf("%d cible(s) synchronisée(s) et vérifiée(s)", done)
|
||||
summary := fmt.Sprintf("%d target(s) replicated and verified", done)
|
||||
if failed > 0 {
|
||||
status = store.RunError
|
||||
summary = fmt.Sprintf("%d cible(s) en échec, %d synchronisée(s)", failed, done)
|
||||
summary = fmt.Sprintf("%d target(s) failed, %d replicated", failed, done)
|
||||
}
|
||||
save(status, summary)
|
||||
s.invalidate()
|
||||
@@ -423,12 +436,12 @@ func (s *Server) handleSaveCluster(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
err := s.store.SaveCluster(r.Context(), old, c)
|
||||
detail := fmt.Sprintf("url=%s rôle=%s tls=%s ssh=%s:%d", c.URL, c.Role, c.TLS, c.SSHHost, c.SSHPort)
|
||||
detail := fmt.Sprintf("url=%s role=%s tls=%s ssh=%s:%d", c.URL, c.Role, c.TLS, c.SSHHost, c.SSHPort)
|
||||
if c.Password != "" {
|
||||
detail += " mot de passe=modifié"
|
||||
detail += " password=changed"
|
||||
}
|
||||
if old != "" && old != c.Name {
|
||||
detail += " renommé depuis " + old
|
||||
detail += " renamed from " + old
|
||||
}
|
||||
s.audit(r, Identity{}, "cluster-save", c.Name, err == nil, errText(err, detail))
|
||||
if err != nil {
|
||||
@@ -546,10 +559,10 @@ func (s *Server) handleCheckCluster(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
detail := res.Error
|
||||
if fp != "" {
|
||||
detail += " certificat TLS enregistré " + fp
|
||||
detail += " TLS certificate recorded " + fp
|
||||
}
|
||||
if res.SSHHostKey != "" {
|
||||
detail += " clé SSH enregistrée " + res.SSHFingerprint
|
||||
detail += " SSH key recorded " + res.SSHFingerprint
|
||||
}
|
||||
s.audit(r, Identity{}, "cluster-check", name, res.OK, strings.TrimSpace(detail))
|
||||
if fp != "" || res.SSHHostKey != "" {
|
||||
@@ -563,7 +576,7 @@ func (s *Server) handleCheckCluster(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) handleForget(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.PathValue("name")
|
||||
err := s.store.Forget(r.Context(), name)
|
||||
s.audit(r, Identity{}, "cluster-forget", name, err == nil, errText(err, "empreinte TLS et clé SSH oubliées"))
|
||||
s.audit(r, Identity{}, "cluster-forget", name, err == nil, errText(err, "TLS fingerprint and SSH key forgotten"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, err.Error(), "notfound")
|
||||
return
|
||||
@@ -578,21 +591,7 @@ func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusInternalServerError, err.Error(), "store")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"settings": st, "allKinds": model.Kinds, "deps": adapter.KindDeps, "defaults": defaultPolicies()})
|
||||
}
|
||||
|
||||
// defaultPolicies describes the built-in policies, shown in the settings.
|
||||
func defaultPolicies() map[model.Kind]string {
|
||||
return map[model.Kind]string{
|
||||
model.KindDatacenter: "tag-style, registered-tags, user-tag-access, next-id : sync ; le reste : ignore ; webauthn, u2f, http_proxy : jamais",
|
||||
model.KindGroup: "sync (commentaire ; les membres viennent des comptes)",
|
||||
model.KindRole: "sync (rôles personnalisés ; les rôles intégrés ne sont pas touchés)",
|
||||
model.KindUser: "sync (comptes du realm pve : nom, e-mail, groupes, expiration, activation…)",
|
||||
model.KindPassword: "sync (hachage copié par SSH dans /etc/pve/priv/shadow.cfg, jamais affiché)",
|
||||
model.KindZone: "nodes, bridge, peers, fabric, dns, reversedns, dnszone : override (mapping de la cible) ; le reste : sync",
|
||||
model.KindVNet: "sync",
|
||||
model.KindSubnet: "sync",
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"settings": st, "allKinds": model.Kinds, "deps": adapter.KindDeps, "bundles": adapter.KindBundles})
|
||||
}
|
||||
|
||||
func (s *Server) handleSaveSettings(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -601,6 +600,7 @@ func (s *Server) handleSaveSettings(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, err.Error(), "request")
|
||||
return
|
||||
}
|
||||
st.Kinds = adapter.Bundle(st.Kinds)
|
||||
cfg := &config.Config{Kinds: st.Kinds, Objects: st.Objects, Policies: st.Policies, Prune: st.Prune,
|
||||
Reference: config.Cluster{Name: "\x00ref"}}
|
||||
err := cfg.Validate()
|
||||
@@ -611,7 +611,7 @@ func (s *Server) handleSaveSettings(w http.ResponseWriter, r *http.Request) {
|
||||
err = s.store.SaveSettings(r.Context(), st)
|
||||
}
|
||||
b, _ := json.Marshal(st)
|
||||
s.audit(r, Identity{}, "settings-save", "synchronisation", err == nil, errText(err, string(b)))
|
||||
s.audit(r, Identity{}, "settings-save", "replication", err == nil, errText(err, string(b)))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error(), "invalid")
|
||||
return
|
||||
@@ -645,9 +645,9 @@ func (s *Server) handleSaveUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
name := r.PathValue("name")
|
||||
err := s.store.SaveUser(r.Context(), name, req.Role, req.Password)
|
||||
detail := "rôle " + req.Role
|
||||
detail := "role " + req.Role
|
||||
if req.Password != "" {
|
||||
detail += ", mot de passe défini"
|
||||
detail += ", password set"
|
||||
}
|
||||
s.audit(r, Identity{}, "user-save", name, err == nil, errText(err, detail))
|
||||
if err != nil {
|
||||
|
||||
@@ -344,7 +344,7 @@ func (a *Auth) handleSetup(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
id := Identity{User: u.Name, Role: u.Role, Mode: "local"}
|
||||
log.Printf("auth: first administrator %q created from %s", u.Name, clientIP(r))
|
||||
a.audit(r, id, "setup", u.Name, true, "premier administrateur créé")
|
||||
a.audit(r, id, "setup", u.Name, true, "first administrator created")
|
||||
a.setSession(w, r, session{User: u.Name, Role: u.Role, Mode: "local", Epoch: u.Epoch})
|
||||
http.Redirect(w, r, "/#clusters", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
@@ -276,6 +277,11 @@ func TestFromScratch(t *testing.T) {
|
||||
if code, d := adm.do("POST", "/api/clusters/ref/role", map[string]any{"role": "primary"}, true); code != http.StatusOK {
|
||||
t.Fatalf("role: %d %v", code, d)
|
||||
}
|
||||
// accounts left out of the run ("do not replicate"): their passwords go with them
|
||||
skipped := map[string]any{"scope": "global", "targets": []string{"tgt"}, "skip": []string{"pve-user", "pve-group", "pve-role"}}
|
||||
if code, d := adm.do("POST", "/api/dryrun", skipped, true); code != http.StatusOK || strings.Contains(fmt.Sprint(d["plans"]), "pve-") {
|
||||
t.Fatalf("dry-run without the accounts: %d %v", code, d)
|
||||
}
|
||||
sel := map[string]any{"scope": "global", "targets": []string{"tgt"}}
|
||||
code, d := adm.do("POST", "/api/dryrun", sel, true)
|
||||
if code != http.StatusOK {
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestCheckSSHHostKeyMismatch(t *testing.T) {
|
||||
SSHHostKey: strings.TrimSpace(string(ssh.MarshalAuthorizedKey(other)))}
|
||||
|
||||
res := CheckCluster(context.Background(), cl, false, false)
|
||||
if res.OK || !res.SSHMismatch || !res.API || res.SSHHostKey != "" || !strings.Contains(res.Error, "clé d'hôte") {
|
||||
if res.OK || !res.SSHMismatch || !res.API || res.SSHHostKey != "" || !strings.Contains(res.Error, "host key") {
|
||||
t.Fatalf("mismatch expected: %+v", res)
|
||||
}
|
||||
res = CheckCluster(context.Background(), cl, false, true)
|
||||
|
||||
@@ -315,7 +315,7 @@ type ClientPlan struct {
|
||||
|
||||
// Plans returns the plans of the given clients restricted to the given objects
|
||||
// (empty: everything), as a dry-run shows them and an apply checks them.
|
||||
func (st *State) Plans(keys []string) []ClientPlan {
|
||||
func (st *State) Plans(keys []string, skip []model.Kind) []ClientPlan {
|
||||
var out []ClientPlan
|
||||
for _, c := range st.Clients {
|
||||
cp := ClientPlan{Client: c.Name}
|
||||
@@ -325,7 +325,7 @@ func (st *State) Plans(keys []string) []ClientPlan {
|
||||
case c.Error != "":
|
||||
cp.Error = c.Error
|
||||
default:
|
||||
cp.Plan = engine.Filter(c.Plan, keys)
|
||||
cp.Plan = engine.Skip(c.Plan, keys, skip)
|
||||
cp.Digest = cp.Plan.Digest()
|
||||
cp.SDN = cp.Plan.HasSDN()
|
||||
}
|
||||
@@ -336,7 +336,7 @@ func (st *State) Plans(keys []string) []ClientPlan {
|
||||
|
||||
// Apply writes one plan on a client, then checks that the selected objects are
|
||||
// in line with the reference (same reference objects as the plan).
|
||||
func (st *State) Apply(ctx context.Context, cp ClientPlan, keys []string, log func(engine.Step)) error {
|
||||
func (st *State) Apply(ctx context.Context, cp ClientPlan, keys []string, skip []model.Kind, log func(engine.Step)) error {
|
||||
cl, ok := st.cfg.Client(cp.Client)
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown client %q", cp.Client)
|
||||
@@ -351,7 +351,7 @@ func (st *State) Apply(ctx context.Context, cp ClientPlan, keys []string, log fu
|
||||
}
|
||||
start := time.Now()
|
||||
err = engine.Verify(ctx, c, func(actual []model.Object) engine.Plan {
|
||||
return engine.Filter(engine.MakePlan(st.cfg, cl, st.ref, actual), keys)
|
||||
return engine.Skip(engine.MakePlan(st.cfg, cl, st.ref, actual), keys, skip)
|
||||
}, st.cfg.Kinds)
|
||||
s := engine.Step{At: start.UTC(), Action: "verify"}
|
||||
if err != nil {
|
||||
@@ -394,7 +394,7 @@ func CheckCluster(ctx context.Context, cl config.Cluster, pinTLS, trust bool) Ch
|
||||
cl.Fingerprint, res.TLSFingerprint = fp, fp
|
||||
}
|
||||
if cl.Insecure {
|
||||
res.Warnings = append(res.Warnings, "certificat TLS non vérifié (labo uniquement)")
|
||||
res.Warnings = append(res.Warnings, "TLS certificate not verified (lab only)")
|
||||
}
|
||||
key := cl.SSHHostKey
|
||||
cl.SSHHostKey = "" // API only for now
|
||||
@@ -433,7 +433,7 @@ func CheckCluster(ctx context.Context, cl config.Cluster, pinTLS, trust bool) Ch
|
||||
if strings.Contains(err.Error(), "host key mismatch") {
|
||||
_, fp, _ := sshx.Scan(ctx, o)
|
||||
res.SSHMismatch, res.SSHFingerprint = true, fp
|
||||
return fail(fmt.Errorf("SSH: le nœud %s présente la clé d'hôte %s au lieu de %s enregistrée", o.Host, fp, sshx.Fingerprint(key)))
|
||||
return fail(fmt.Errorf("SSH: node %s presents host key %s instead of the recorded %s", o.Host, fp, sshx.Fingerprint(key)))
|
||||
}
|
||||
return fail(fmt.Errorf("SSH: %w", err))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user