-
-
Notifications
You must be signed in to change notification settings - Fork 0
docs(acme): derive the provider table and --acme-dns-provider help from the registry #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dash
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/basecamp/kamal-proxy/internal/server/acme" | ||
| "github.com/basecamp/kamal-proxy/internal/server/acme/providers" | ||
| ) | ||
|
|
||
| // The --acme-dns-provider help enumerates the registry, not a hand-written | ||
| // list: a provider added to the registry appears here with no other change. | ||
| func TestRunCommand_DNSProviderHelpMatchesRegistry(t *testing.T) { | ||
| flag := newRunCommand().cmd.Flags().Lookup("acme-dns-provider") | ||
| require.NotNil(t, flag) | ||
|
|
||
| for _, name := range providers.Names() { | ||
| assert.Contains(t, flag.Usage, string(name)) | ||
| } | ||
| assert.Contains(t, flag.Usage, string(acme.ProviderAuto)) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| package providers | ||
|
mhenrixon marked this conversation as resolved.
|
||
|
|
||
| import ( | ||
| "fmt" | ||
| "slices" | ||
| "strings" | ||
|
|
||
| "github.com/basecamp/kamal-proxy/internal/server/acme" | ||
| ) | ||
|
|
||
| // Documentation derived from the registry: the --acme-dns-provider help list, | ||
| // the README's supported-provider table, and the marker-based replacement | ||
| // shared by the generator tool and the drift test. The registry is the single | ||
| // source of truth; nothing here is hand-maintained. | ||
|
|
||
| //go:generate go run ./gen | ||
|
|
||
| const ( | ||
| providerTableBeginMarker = "<!-- BEGIN GENERATED: dns-provider-table (go generate ./internal/server/acme/providers) -->" | ||
| providerTableEndMarker = "<!-- END GENERATED: dns-provider-table -->" | ||
| ) | ||
|
|
||
| // Names returns every registered provider name, sorted. | ||
| func Names() []acme.ProviderName { | ||
| names := make([]acme.ProviderName, 0, len(registry)) | ||
| for name := range registry { | ||
| names = append(names, name) | ||
| } | ||
| slices.Sort(names) | ||
| return names | ||
| } | ||
|
|
||
| // ProviderListForHelp renders the provider names for the --acme-dns-provider | ||
| // flag help: the registry, sorted, plus the auto pseudo-provider. | ||
| func ProviderListForHelp() string { | ||
| parts := make([]string, 0, len(registry)+1) | ||
| for _, name := range Names() { | ||
| parts = append(parts, string(name)) | ||
| } | ||
| parts = append(parts, string(acme.ProviderAuto)) | ||
| return strings.Join(parts, ", ") | ||
| } | ||
|
|
||
| // ProviderTableMarkdown renders the supported-provider table: one row per | ||
| // registry entry, leading with the name the --acme-dns-provider flag accepts | ||
| // (a display name like "AWS Route53" is not a flag value), then the display | ||
| // name linked to its lego documentation, the credential rule as the same | ||
| // OR-of-ANDs the boot check enforces, and the optional variables the entry | ||
| // names. | ||
| func ProviderTableMarkdown() string { | ||
| var b strings.Builder | ||
| b.WriteString("| Name | Provider | Credentials | Optional |\n") | ||
| b.WriteString("|------|----------|-------------|----------|\n") | ||
|
|
||
| for _, name := range Names() { | ||
| provider := registry[name] | ||
| fmt.Fprintf(&b, "| `%s` | [%s](%s) | %s | %s |\n", | ||
| name, provider.DisplayName, provider.Docs, | ||
| markdownCredentialSets(provider.credentialSets()), | ||
| markdownVars(provider.Optional)) | ||
| } | ||
|
|
||
| return b.String() | ||
| } | ||
|
|
||
| // ReplaceProviderTable substitutes the generated provider table between the | ||
| // README's markers, leaving everything else untouched. It errors when the | ||
| // markers are missing — silently appending a table nobody asked for is how a | ||
| // generator corrupts a document. | ||
| func ReplaceProviderTable(document string) (string, error) { | ||
| begin := strings.Index(document, providerTableBeginMarker) | ||
| end := strings.Index(document, providerTableEndMarker) | ||
| if begin == -1 || end == -1 || end < begin { | ||
| return "", fmt.Errorf("provider table markers not found (need %q ... %q)", | ||
| providerTableBeginMarker, providerTableEndMarker) | ||
| } | ||
|
|
||
| return document[:begin+len(providerTableBeginMarker)] + | ||
| "\n" + ProviderTableMarkdown() + | ||
| document[end:], nil | ||
| } | ||
|
|
||
| // markdownCredentialSets renders an OR-of-ANDs credential rule with each | ||
| // variable in backticks, e.g. "`CF_API_TOKEN` or (`CF_API_KEY` + `CF_API_EMAIL`)". | ||
| func markdownCredentialSets(sets [][]string) string { | ||
| parts := make([]string, 0, len(sets)) | ||
| for _, set := range sets { | ||
| joined := make([]string, 0, len(set)) | ||
| for _, envVar := range set { | ||
| joined = append(joined, "`"+envVar+"`") | ||
| } | ||
| if len(set) > 1 { | ||
| parts = append(parts, "("+strings.Join(joined, " + ")+")") | ||
| } else { | ||
| parts = append(parts, strings.Join(joined, " + ")) | ||
| } | ||
| } | ||
| return strings.Join(parts, " or ") | ||
| } | ||
|
|
||
| // markdownVars renders a plain list of env vars in backticks, or a dash for | ||
| // none. | ||
| func markdownVars(vars []string) string { | ||
| if len(vars) == 0 { | ||
| return "—" | ||
| } | ||
| quoted := make([]string, 0, len(vars)) | ||
| for _, envVar := range vars { | ||
| quoted = append(quoted, "`"+envVar+"`") | ||
| } | ||
| return strings.Join(quoted, ", ") | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| package providers | ||
|
|
||
| import ( | ||
| "os" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/basecamp/kamal-proxy/internal/server/acme" | ||
| ) | ||
|
|
||
| func TestNames_CoversRegistryExactlySorted(t *testing.T) { | ||
| names := Names() | ||
|
|
||
| require.Len(t, names, len(registry)) | ||
| for _, name := range names { | ||
| assert.Contains(t, registry, name) | ||
| } | ||
| assert.IsIncreasing(t, names) | ||
| } | ||
|
|
||
| func TestProviderTableMarkdown_RendersEveryRegistryEntry(t *testing.T) { | ||
| table := ProviderTableMarkdown() | ||
|
|
||
| for name, provider := range registry { | ||
| assert.Contains(t, table, "| `"+string(name)+"` | ["+provider.DisplayName+"]("+provider.Docs+")", | ||
| "provider %s must appear with its flag value and docs link", name) | ||
| } | ||
|
|
||
| // The credential column renders the full OR-of-ANDs rule, not prose: | ||
| // Cloudflare's third alternative was exactly what the hand-written table | ||
| // had already lost. | ||
| assert.Contains(t, table, "`CF_API_TOKEN` or `CF_DNS_API_TOKEN` or (`CF_API_KEY` + `CF_API_EMAIL`)") | ||
| assert.Contains(t, table, "`AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`") | ||
|
|
||
| // Optional vars come from the registry entry, not prose. | ||
| assert.Contains(t, table, "`NAMECHEAP_SANDBOX`") | ||
|
|
||
| // Deterministic: two renders are identical. | ||
| assert.Equal(t, table, ProviderTableMarkdown()) | ||
| } | ||
|
|
||
| func TestReplaceProviderTable_RewritesOnlyTheMarkedBlock(t *testing.T) { | ||
| doc := "before\n" + providerTableBeginMarker + "\nstale table\n" + providerTableEndMarker + "\nafter\n" | ||
|
|
||
| replaced, err := ReplaceProviderTable(doc) | ||
| require.NoError(t, err) | ||
|
|
||
| assert.True(t, strings.HasPrefix(replaced, "before\n")) | ||
| assert.True(t, strings.HasSuffix(replaced, "\nafter\n")) | ||
| assert.NotContains(t, replaced, "stale table") | ||
| assert.Contains(t, replaced, ProviderTableMarkdown()) | ||
|
|
||
| // Idempotent: replacing again changes nothing. | ||
| again, err := ReplaceProviderTable(replaced) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, replaced, again) | ||
| } | ||
|
|
||
| func TestReplaceProviderTable_FailsWithoutMarkers(t *testing.T) { | ||
| _, err := ReplaceProviderTable("a document with no markers") | ||
| require.Error(t, err) | ||
| } | ||
|
|
||
| // The committed README table must match what the registry renders; a provider | ||
| // added to the registry without regenerating fails here, named. | ||
| func TestREADMEProviderTable_MatchesRegistry(t *testing.T) { | ||
| data, err := os.ReadFile("../../../../README.md") | ||
| require.NoError(t, err) | ||
|
|
||
| regenerated, err := ReplaceProviderTable(string(data)) | ||
| require.NoError(t, err, "README.md must carry the provider table markers") | ||
|
|
||
| if string(data) == regenerated { | ||
| return | ||
| } | ||
|
|
||
| // Name the rows the registry renders that the committed table lacks — | ||
| // a brand-new provider AND a changed row (credentials, optional vars) | ||
| // both surface as an expected line the document does not contain. | ||
| stale := []string{} | ||
| for line := range strings.Lines(ProviderTableMarkdown()) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: When a provider is removed from the registry but its row remains in README, the equality gate fails while Prompt for AI agents |
||
| line = strings.TrimSuffix(line, "\n") | ||
| if line != "" && !strings.Contains(string(data), line) { | ||
| stale = append(stale, line) | ||
| } | ||
| } | ||
| t.Fatalf("README.md provider table is out of date (run `go generate ./internal/server/acme/providers`); stale or missing rows:\n%s", | ||
| strings.Join(stale, "\n")) | ||
| } | ||
|
|
||
| // The flag help must enumerate the registry, plus the auto pseudo-provider, | ||
| // with no hand-maintained copy anywhere. | ||
| func TestProviderListForHelp_MatchesRegistry(t *testing.T) { | ||
| help := ProviderListForHelp() | ||
|
|
||
| for name := range registry { | ||
| assert.Contains(t, help, string(name)) | ||
| } | ||
| assert.True(t, strings.HasSuffix(help, ", "+string(acme.ProviderAuto))) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // Command gen rewrites the README's supported-DNS-provider table from the | ||
| // provider registry. Run via `go generate ./internal/server/acme/providers`; | ||
| // TestREADMEProviderTable_MatchesRegistry fails the build when the committed | ||
| // table does not match what this would write. | ||
| package main | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
|
|
||
| "github.com/basecamp/kamal-proxy/internal/server/acme/providers" | ||
| ) | ||
|
|
||
| func main() { | ||
| const readme = "../../../../README.md" | ||
|
|
||
| data, err := os.ReadFile(readme) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "gen: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| replaced, err := providers.ReplaceProviderTable(string(data)) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "gen: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| if replaced == string(data) { | ||
| return | ||
| } | ||
|
|
||
| if err := os.WriteFile(readme, []byte(replaced), 0644); err != nil { | ||
| fmt.Fprintf(os.Stderr, "gen: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
| fmt.Println("gen: README.md provider table regenerated") | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.