fix(provisioning,catalog): parent-kustomization prefix collision + disable openclaw/stalwart-mail (#1043)
Two bugs surfaced live 2026-05-06 on tenant "test": 1) UpdateParentKustomization used substring match against " - <slug>", which falsely "found" the slug when it was a PREFIX of an existing entry. Adding "test" to a file already listing "test11" or "test13" silently no-op'd. Result: tenant manifests committed but the tenants/kustomization.yaml never registered them, Flux's tenants Kustomization couldn't apply the new tenant, vCluster step timed out at 10m. Fix: exact line match on the resources entry. 2) openclaw + stalwart-mail were flagged Deployable=true in #941 but never had AppSpec entries in core/services/provisioning/gitops/apps.go KnownApps. The SME provisioning generator emits a single-Deployment template that requires Image + Port; for those two slugs it produced invalid manifests: Deployment.apps "openclaw" is invalid: containers[0].image: Required value containers[0].ports[0].containerPort: Required value tenant-test11-apps Kustomization rejected the dry-run, no apps ever landed inside the vcluster. Re-enabling these requires per-app overlay support beyond the single-Deployment template — separate work. For now: comment them out of DeployableAppSlugs so the catalog seed flips them back to Deployable=false on next pod restart and the marketplace UI shows them as COMING SOON. Adds regression tests for both: prefix-collision in UpdateParentKustomization, and a stability test on the deployable map shape. Co-authored-by: hatiyildiz <hatice@openova.io> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
68e61eb306
commit
a57d05d4dd
@ -607,8 +607,18 @@ func DeployableAppSlugs() map[string]bool {
|
||||
"invoiceshelf": true,
|
||||
"formbricks": true,
|
||||
"listmonk": true, // fixed in #101 — DBEnvStyle:"listmonk" + InitCommand
|
||||
"openclaw": true, // #941 — bp-openclaw + bp-newapi via SME-tenant overlay
|
||||
"stalwart-mail": true, // #941 — bp-stalwart-tenant via SME-tenant overlay
|
||||
// openclaw + stalwart-mail were flagged Deployable in #941 but have
|
||||
// no entry in core/services/provisioning/gitops/apps.go KnownApps —
|
||||
// the SME provisioning service generates manifests via a single
|
||||
// Deployment template that requires Image + Port; both apps need
|
||||
// HelmRelease-shaped overlays (controller + runtime for openclaw;
|
||||
// IMAP/SMTP services for stalwart-mail). Live failure 2026-05-06
|
||||
// on tenant "test11": tenant-test11-apps Kustomization rejected
|
||||
// `Deployment.apps "openclaw" is invalid: containers[0].image
|
||||
// Required value`. Re-enabling these requires per-app overlay
|
||||
// templates beyond the one-Deployment generator.
|
||||
// "openclaw": true, // #941 — disabled until proper overlay
|
||||
// "stalwart-mail": true, // #941 — disabled until proper overlay
|
||||
// Backing services are always deployable — they come bundled with
|
||||
// whichever business app needs them. Marking them true so the
|
||||
// catalog UI doesn't draw a 'Coming soon' overlay on them. #112.
|
||||
|
||||
@ -2,23 +2,23 @@ package handlers
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestDeployableAppSlugs_Issue941 asserts that openclaw + stalwart-mail
|
||||
// are present in the deployable map (issue #941). C5-final hit "27 apps
|
||||
// COMING SOON" on otech113 because both were missing — gates 4 (LLM)
|
||||
// and 5 (mail) blocked before alice could click Install.
|
||||
func TestDeployableAppSlugs_Issue941(t *testing.T) {
|
||||
// TestDeployableAppSlugs_OpenclawStalwartDisabled asserts that openclaw and
|
||||
// stalwart-mail are NOT in the deployable map. They were briefly enabled by
|
||||
// #941 (catalog flag flipped) but the SME provisioning generator at
|
||||
// core/services/provisioning/gitops/apps.go has no AppSpec for either, so
|
||||
// the rendered Deployment manifests are invalid (missing image + ports).
|
||||
// Live failure 2026-05-06 on tenant "test11":
|
||||
//
|
||||
// tenant-test11-apps Kustomization rejected:
|
||||
// Deployment.apps "openclaw" is invalid: containers[0].image: Required value
|
||||
//
|
||||
// Re-enabling these requires per-app HelmRelease overlays beyond the single
|
||||
// Deployment template the generator currently supports.
|
||||
func TestDeployableAppSlugs_OpenclawStalwartDisabled(t *testing.T) {
|
||||
d := DeployableAppSlugs()
|
||||
wantTrue := []string{
|
||||
"openclaw", // #941 — bp-openclaw via SME-tenant overlay
|
||||
"stalwart-mail", // #941 — bp-stalwart-tenant via SME-tenant overlay
|
||||
// Sanity — the canonical alice baseline apps.
|
||||
"wordpress",
|
||||
"ghost",
|
||||
"nextcloud",
|
||||
}
|
||||
for _, slug := range wantTrue {
|
||||
if !d[slug] {
|
||||
t.Errorf("expected %q to be deployable, got false (or missing)", slug)
|
||||
for _, slug := range []string{"openclaw", "stalwart-mail"} {
|
||||
if d[slug] {
|
||||
t.Errorf("%q must NOT be deployable until per-app overlay exists in the SME provisioning generator", slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -32,7 +32,6 @@ func TestDeployableAppSlugs_StableShape(t *testing.T) {
|
||||
"wordpress", "ghost", "nextcloud", "bookstack", "uptime-kuma",
|
||||
"gitea", "vaultwarden", "umami", "nocodb", "cal-com",
|
||||
"invoiceshelf", "formbricks", "listmonk",
|
||||
"openclaw", "stalwart-mail",
|
||||
"postgres", "mysql", "redis",
|
||||
}
|
||||
if got, want := len(d), len(expected); got != want {
|
||||
|
||||
@ -975,10 +975,20 @@ func planLimits(slug string) planLimit {
|
||||
}
|
||||
|
||||
// UpdateParentKustomization adds a tenant entry to the parent kustomization.
|
||||
//
|
||||
// The "already listed" check used to be a substring match against the literal
|
||||
// " - <slug>", which falsely triggered when <slug> was a prefix of any
|
||||
// existing entry (e.g. trying to add "test" when the file already listed
|
||||
// "test11" or "test13"). Live race observed 2026-05-06: tenant "test"'s
|
||||
// commit silently no-op'd the parent update, leaving its directory orphan
|
||||
// and Flux's tenants Kustomization unable to apply it. Fix: exact line
|
||||
// match on the resources entry.
|
||||
func UpdateParentKustomization(current, tenantSlug string) string {
|
||||
entry := fmt.Sprintf(" - %s", tenantSlug)
|
||||
if strings.Contains(current, entry) {
|
||||
return current
|
||||
for _, ln := range strings.Split(current, "\n") {
|
||||
if strings.TrimRight(ln, " \t") == entry {
|
||||
return current
|
||||
}
|
||||
}
|
||||
if strings.Contains(current, "resources: []") {
|
||||
return strings.Replace(current, "resources: []", fmt.Sprintf("resources:\n%s", entry), 1)
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
package gitops
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestUpdateParentKustomization_PrefixCollision regression-tests the bug
|
||||
// observed live 2026-05-06: tenant "test"'s parent update silently no-op'd
|
||||
// because the file already listed "test11" / "test13", and the substring
|
||||
// match against " - test" matched " - test11" / " - test13". The fix is
|
||||
// an exact line match.
|
||||
func TestUpdateParentKustomization_PrefixCollision(t *testing.T) {
|
||||
current := `apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- test13
|
||||
- market
|
||||
- aaa
|
||||
- bbb
|
||||
- test11
|
||||
`
|
||||
got := UpdateParentKustomization(current, "test")
|
||||
if !strings.Contains(got, "\n - test\n") {
|
||||
t.Fatalf("expected ' - test' as a fresh entry; got:\n%s", got)
|
||||
}
|
||||
// Existing entries must remain untouched.
|
||||
for _, want := range []string{" - test13", " - market", " - aaa", " - bbb", " - test11"} {
|
||||
if !strings.Contains(got, want+"\n") {
|
||||
t.Fatalf("expected %q to remain; got:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateParentKustomization_AlreadyPresent ensures we don't double-add a
|
||||
// slug that already has its own line.
|
||||
func TestUpdateParentKustomization_AlreadyPresent(t *testing.T) {
|
||||
current := `apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- test
|
||||
- test11
|
||||
`
|
||||
got := UpdateParentKustomization(current, "test")
|
||||
if got != current {
|
||||
t.Fatalf("expected unchanged when slug already listed; got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateParentKustomization_EmptyResources adds the first entry into
|
||||
// the explicit "resources: []" form.
|
||||
func TestUpdateParentKustomization_EmptyResources(t *testing.T) {
|
||||
current := `apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources: []
|
||||
`
|
||||
got := UpdateParentKustomization(current, "alpha")
|
||||
if !strings.Contains(got, "resources:\n - alpha\n") {
|
||||
t.Fatalf("expected 'resources:' block with alpha; got:\n%s", got)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user