health: add an ETag to UnhealthyState for change detection

Updates tailscale/corp#30596

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
This commit is contained in:
James Sanderson
2025-07-28 11:38:34 +01:00
committed by James 'zofrex' Sanderson
parent 5154bbb0b3
commit 5731869565
4 changed files with 215 additions and 7 deletions

View File

@@ -4,6 +4,9 @@
package health
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"time"
"tailscale.com/tailcfg"
@@ -35,6 +38,36 @@ type UnhealthyState struct {
DependsOn []WarnableCode `json:",omitempty"`
ImpactsConnectivity bool `json:",omitempty"`
PrimaryAction *UnhealthyStateAction `json:",omitempty"`
// ETag identifies a specific version of an UnhealthyState. If the contents
// of the other fields of two UnhealthyStates are the same, the ETags will
// be the same. If the contents differ, the ETags will also differ. The
// implementation is not defined and the value is opaque: it might be a
// hash, it might be a simple counter. Implementations should not rely on
// any specific implementation detail or format of the ETag string other
// than string (in)equality.
ETag string `json:",omitzero"`
}
// hash computes a deep hash of UnhealthyState which will be stable across
// different runs of the same binary.
func (u UnhealthyState) hash() []byte {
hasher := sha256.New()
enc := json.NewEncoder(hasher)
// hash.Hash.Write never returns an error, so this will only fail if u is
// not marshalable, in which case we have much bigger problems.
_ = enc.Encode(u)
return hasher.Sum(nil)
}
// withETag returns a copy of UnhealthyState with an ETag set. The ETag will be
// the same for all UnhealthyState instances that are equal. If calculating the
// ETag errors, it returns a copy of the UnhealthyState with an empty ETag.
func (u UnhealthyState) withETag() UnhealthyState {
u.ETag = ""
u.ETag = hex.EncodeToString(u.hash())
return u
}
// UnhealthyStateAction represents an action (URL and link) to be presented to
@@ -107,7 +140,8 @@ func (t *Tracker) CurrentState() *State {
// that are unhealthy.
continue
}
wm[w.Code] = *w.unhealthyState(ws)
state := w.unhealthyState(ws)
wm[w.Code] = state.withETag()
}
for id, msg := range t.lastNotifiedControlMessages {
@@ -127,7 +161,7 @@ func (t *Tracker) CurrentState() *State {
}
}
wm[state.WarnableCode] = state
wm[state.WarnableCode] = state.withETag()
}
return &State{