mirror of
https://github.com/hanzoai/ingress.git
synced 2026-10-30 14:58:23 +00:00
An adopted plaintext keeps the same digest as the pre-seal snapshot, so while the process kept adopting, a rollback that replanted that snapshot would be re-admitted — the same bytes adoption had just taken. Seal.Persisted, called by both stores once a sealed write reaches disk or the Secret, ends adoption: the store is sealed now, so a later unsealed store is a different store and is refused. Reads of the sealed store are unaffected. It fires on the write that LANDS, not the one attempted — a conflicted SharedStore write does not reach it, so the retry re-read of a still-unsealed object is unchanged. The signal is a method on the Seal contract rather than a runtime type check: the store tells the seal a sealed write persisted, and what a seal does with that is the seal's own concern; the identity seal does nothing.
170 lines
6.5 KiB
Go
170 lines
6.5 KiB
Go
package acme
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/hanzoai/ingress/pkg/observability/metrics"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/rs/zerolog/log"
|
|
|
|
"github.com/hanzoai/ingress/pkg/observability/logs"
|
|
)
|
|
|
|
// Seal is how the ACME state document becomes bytes at rest.
|
|
//
|
|
// It is an interface here, and its implementation lives in pkg/kms, because
|
|
// this package's job is what the document MEANS — accounts, certificates,
|
|
// resolvers — and not what a key is. Inverting it that way also keeps the ACME
|
|
// provider's import graph free of anything that talks to a network.
|
|
//
|
|
// A store is named, and every write is counted. Both travel into the seal
|
|
// because both are what the bytes are bound to: a document opens for the store
|
|
// it was written for, at the write it was made at, and nowhere else.
|
|
type Seal interface {
|
|
// Wrap returns the bytes to persist for the store called name, as its
|
|
// count-th write. Counting starts at one.
|
|
Wrap(name string, count uint64, plain []byte) ([]byte, error)
|
|
|
|
// Unwrap returns the document the store read, the write it was made at,
|
|
// and whether the bytes were under seal. A configured seal returns an
|
|
// error for bytes that carry no envelope unless the operator has adopted
|
|
// the store; then it returns them with sealed false, for the caller to
|
|
// write back under seal.
|
|
Unwrap(name string, stored []byte) (plain []byte, count uint64, sealed bool, err error)
|
|
|
|
// Persisted is called once a sealed write has reached the store. A seal
|
|
// that adopted an unsealed store ends adoption here: the store is sealed
|
|
// now, so a later unsealed store is a different store and is refused. It is
|
|
// called on the write that LANDS, not the one attempted.
|
|
Persisted()
|
|
}
|
|
|
|
// plain is the seal of a deployment that has configured none: the document is
|
|
// written as it always was. It exists so that no store has to ask whether it
|
|
// has a seal — a nil check that one of them would eventually forget, on the
|
|
// path whose entire job is to not write private keys in the clear.
|
|
type plain struct{}
|
|
|
|
// Plain returns the identity seal — JSON at rest, which is what this edge did
|
|
// everywhere before KMS sealing existed.
|
|
func Plain() Seal { return plain{} }
|
|
|
|
func (plain) Wrap(_ string, _ uint64, doc []byte) ([]byte, error) { return doc, nil }
|
|
|
|
func (plain) Unwrap(_ string, stored []byte) ([]byte, uint64, bool, error) {
|
|
return stored, 0, true, nil
|
|
}
|
|
|
|
// Persisted is nothing to the identity seal: it never adopted, so it has no
|
|
// adoption to end.
|
|
func (plain) Persisted() {}
|
|
|
|
// unsealed counts the reads that did not open. It is a counter because the
|
|
// interesting shape is the rate: one at boot is a store this edge was not
|
|
// pointed at, a steady trickle is a store something else is writing.
|
|
var unsealed = prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: metrics.MetricNamePrefix + "acme_unseal_failures_total",
|
|
Help: "Reads of the ACME state document that could not be opened.",
|
|
}, []string{"store"})
|
|
|
|
func init() { metrics.Register(unsealed) }
|
|
|
|
// counter is a store's view of how many writes its document has had.
|
|
//
|
|
// It only moves forward. A document that reports a write BELOW the one this
|
|
// replica has already read is not the current document — it is an earlier copy
|
|
// of it — and the store keeps what it has rather than stepping back to it.
|
|
type counter struct {
|
|
mu sync.Mutex
|
|
n uint64
|
|
}
|
|
|
|
// read records a document at write n, and refuses one that is behind.
|
|
//
|
|
// Zero is not a write — it is a document that carries no count, which is what
|
|
// an unsealed store is and all an unsealed store can be. It makes no claim
|
|
// about being current, so it neither advances the count nor steps it back. A
|
|
// sealed document can never arrive at zero: Wrap counts from one and Unwrap
|
|
// refuses an envelope claiming otherwise.
|
|
func (c *counter) read(n uint64) error {
|
|
if n == 0 {
|
|
return nil
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if n < c.n {
|
|
return fmt.Errorf("acme store: document is at write %d, this replica has read %d", n, c.n)
|
|
}
|
|
c.n = n
|
|
return nil
|
|
}
|
|
|
|
// next is the write a store is about to make. It reserves without recording.
|
|
//
|
|
// The count moves when a write LANDS, not when one is attempted. A store's
|
|
// write can be refused after it is encoded — a resourceVersion conflict, a
|
|
// filesystem error — and recording it then would leave the store's own view of
|
|
// the document ahead of the document, so the next read of the very same bytes
|
|
// would look like a step back and every write after it would be refused.
|
|
func (c *counter) next() uint64 {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.n + 1
|
|
}
|
|
|
|
// wrote records a write that landed. It is the only other way the count moves,
|
|
// and it moves it forward or not at all.
|
|
func (c *counter) wrote(n uint64) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if n > c.n {
|
|
c.n = n
|
|
}
|
|
}
|
|
|
|
// encodeStored renders the state document for storage as the count-th write of
|
|
// the store called name.
|
|
//
|
|
// Indented, because a store a human can look at is worth two spaces, and
|
|
// because the sealed envelope carries its ciphertext base64 either way.
|
|
func encodeStored(seal Seal, name string, count uint64, state map[string]*StoredData) ([]byte, error) {
|
|
doc, err := json.MarshalIndent(state, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("acme store: encode: %w", err)
|
|
}
|
|
sealed, err := seal.Wrap(name, count, doc)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("acme store: seal: %w", err)
|
|
}
|
|
return sealed, nil
|
|
}
|
|
|
|
// decodeStored parses the state document, treating empty as empty rather than
|
|
// as an error — a store that has never been written is the normal first boot.
|
|
//
|
|
// A read that does not open is an error and is counted. Falling through to an
|
|
// empty document would look like a first boot and would order the estate again
|
|
// on top of a store that is already there.
|
|
func decodeStored(seal Seal, name string, stored []byte) (map[string]*StoredData, uint64, bool, error) {
|
|
out := map[string]*StoredData{}
|
|
if len(stored) == 0 {
|
|
return out, 0, true, nil
|
|
}
|
|
doc, count, sealed, err := seal.Unwrap(name, stored)
|
|
if err != nil {
|
|
unsealed.WithLabelValues(name).Inc()
|
|
log.Error().Err(err).Str(logs.ProviderName, "acme").Str("store", name).
|
|
Msg("ACME state could not be opened")
|
|
return nil, 0, sealed, fmt.Errorf("acme store: unseal: %w", err)
|
|
}
|
|
if err := json.Unmarshal(doc, &out); err != nil {
|
|
unsealed.WithLabelValues(name).Inc()
|
|
log.Error().Err(err).Str(logs.ProviderName, "acme").Str("store", name).
|
|
Msg("ACME state could not be read")
|
|
return nil, 0, sealed, fmt.Errorf("acme store: decode: %w", err)
|
|
}
|
|
return out, count, sealed, nil
|
|
}
|