diff --git a/go/provider/provider.go b/go/provider/provider.go index c183514..df315a5 100644 --- a/go/provider/provider.go +++ b/go/provider/provider.go @@ -215,6 +215,11 @@ func New(opts Options) *Provider { plans: map[string][]byte{}, provisions: map[string][]byte{}, said: map[string]string{}, poke: make(chan struct{}, 1)} } +// ErrNotReady is what a Make returns, wrapped or alone, when the provider cannot make a consumer's provision yet but +// will without anyone's hand (its keys not read since a restart): the entry waits and keeps serving what it served. +// Any other error is a failure, and its entry serves nothing. +var ErrNotReady = errors.New("not ready yet") + // What a pass did for one bind. const ( Answered = "answered" @@ -493,7 +498,22 @@ func (p *Provider) answer(ctx context.Context, e Entry, have []byte) Outcome { } } if p.opts.Make != nil { - if err := p.opts.Make(ctx, Consumer{Entry: e, Credential: credential}); err != nil { + if err := p.opts.Make(ctx, Consumer{Entry: e, Credential: credential}); errors.Is(err, ErrNotReady) { + // **Not ready is no failure** (the regression of 2026-10-30: gitea after a restart, its keys not read yet, + // stopped serving every consumer): the entry says waits with why, and keeps what the last entry served, so a + // consumer goes on with it until the provider can make it again. + why := "the provider is not ready to make it yet: " + redact(err.Error(), credential) + waiting := e + var prev Entry + if len(have) > 0 && json.Unmarshal(have, &prev) == nil && prev.State == provisions.Provided { + waiting.Values, waiting.VaultName, waiting.User = prev.Values, prev.VaultName, prev.User + } else { + waiting.Values, waiting.VaultName, waiting.User = nil, "", "" + } + waiting.State, waiting.Why = provisions.Waits, why + p.write(ctx, waiting, have, Outcome{Key: key, Status: Waiting, Why: why}) + return Outcome{Key: key, Status: Waiting, Why: why} + } else if err != nil { why := "the provider could not make it: " + redact(err.Error(), credential) // Said in its entry, which the holder of mesh-provisioner reads (ADR 0570 clause 3), with nothing served. failed := e diff --git a/go/provider/provider_test.go b/go/provider/provider_test.go index 806bb34..eec9d1b 100644 --- a/go/provider/provider_test.go +++ b/go/provider/provider_test.go @@ -525,6 +525,33 @@ func TestAFailedMakeIsSaidWithoutTheCredential(t *testing.T) { strings.Contains(string(raw), "hunter2") { t.Fatalf("the failed entry is %s", raw) } + + // A provider not ready yet (gitea after a restart, its keys not read; the regression of 2026-10-30) is no failure: + // a first entry waits serving nothing, and once made, a later not ready keeps serving what it served. + m.fail = fmt.Errorf("keys not read yet: %w", ErrNotReady) + _ = bus.Delete(context.Background(), BucketOf("postgres"), "ace.letta.postgres-database") + if out, _ := p.Pass(context.Background()); len(out) != 1 || out[0].Status != Waiting { + t.Fatalf("not ready at first: %+v", out) + } + raw = bus.get(BucketOf("postgres"), "ace.letta.postgres-database") + if err := json.Unmarshal(raw, &e); err != nil || e.State != "waits" || !strings.Contains(e.Why, "keys not read yet") || e.Values != nil { + t.Fatalf("the first entry not ready is %s", raw) + } + m.fail = nil + if out, _ := p.Pass(context.Background()); len(out) != 1 || out[0].Status != Answered { + t.Fatalf("made: %+v", out) + } + var made Entry + _ = json.Unmarshal(bus.get(BucketOf("postgres"), "ace.letta.postgres-database"), &made) + m.fail = ErrNotReady + if out, _ := p.Pass(context.Background()); len(out) != 1 || out[0].Status != Waiting { + t.Fatalf("not ready after a restart: %+v", out) + } + raw = bus.get(BucketOf("postgres"), "ace.letta.postgres-database") + if err := json.Unmarshal(raw, &e); err != nil || e.State != "waits" || e.VaultName != made.VaultName || e.User != made.User || + fmt.Sprint(e.Values) != fmt.Sprint(made.Values) || len(made.Values) == 0 { + t.Fatalf("not ready after it served, the entry is %s, it served %+v", raw, made) + } } // Run watches the binds on the stand-in bus: a consumer that binds is answered, and one that unbinds is removed,