diff --git a/.github/wiki/Compat-V15.md b/.github/wiki/Compat-V15.md index bf280ae6e..df7efac1f 100644 --- a/.github/wiki/Compat-V15.md +++ b/.github/wiki/Compat-V15.md @@ -170,6 +170,7 @@ The table is generated. Never hand-edit it; run `compat-markers.sh --write-wiki` | P7-PLUG-60 | media-hosting recommends MinIO | recommends core object storage and the MinIO plugin for existing data | `cmd/commands/init_presets.go` | | P7-PLUG-60 | no object storage line | OBJECT_STORAGE_ENABLED=false listed with the optional services | `internal/setup/setup_env_files.go` | | P7-PLUG-60 | optional core MinIO | core SeaweedFS or MinIO plugin | `internal/compose/object_storage.go` | +| P7-PLUG-60 | the minio fragment merges into every project once installed | only where ActiveStore picks the plugin | `internal/compose/object_storage.go` | | P7-PLUG-63 | bundle reply trusted for any key and bundle | only a signed reply naming this key, this bundle and a live window | `internal/license/cache_entry.go` | | P7-PLUG-63 | cache age from the unsigned fetched_at | the older of fetched_at and the signed jwt iat | `internal/license/cache_entry.go` | | P7-PLUG-63 | cache trusted whatever the clock says | refused when the clock is behind the signed iat or the highest time seen | `internal/license/cache.go` | diff --git a/internal/build/orchestrator_build_compose.go b/internal/build/orchestrator_build_compose.go index 111363ba0..9076bd44d 100644 --- a/internal/build/orchestrator_build_compose.go +++ b/internal/build/orchestrator_build_compose.go @@ -99,7 +99,9 @@ func (st *buildState) generateCompose() error { st.filesGenerated++ // ── Step 9.5: Discover plugin compose files ──────────────────── - pluginComposeFiles, err := discoverPluginComposeFilesFx(st.fx, st.sink, st.workdir, st.pluginDir, &st.entitlementRefreshed) + pluginComposeFiles, err := discoverPluginComposeFilesWanted(st.fx, st.sink, st.workdir, st.pluginDir, func(name string) bool { + return compose.PluginFragmentWanted(name, st.cfg) + }, &st.entitlementRefreshed) if err != nil { var refresh *EntitlementRefreshError if !errors.As(err, &refresh) || errs.Describe(err) == nil || errs.Describe(err).Code != "E125" { diff --git a/internal/build/plugins.go b/internal/build/plugins.go index 2451c2f8a..9c2120fca 100644 --- a/internal/build/plugins.go +++ b/internal/build/plugins.go @@ -66,6 +66,13 @@ func DiscoverPluginComposeFiles(workdir, pluginDir string) ([]string, error) { // discoverPluginComposeFilesFx normalizes installed fragments into the project // Sink: plan mode records overlays without changing either filesystem. func discoverPluginComposeFilesFx(fx Effects, sink Sink, workdir, pluginDir string, refreshed ...*bool) ([]string, error) { + return discoverPluginComposeFilesWanted(fx, sink, workdir, pluginDir, nil, refreshed...) +} + +// discoverPluginComposeFilesWanted is discoverPluginComposeFilesFx with a +// project filter: a plugin for which wanted returns false is left out of the +// stack without touching its fragment (nil keeps every enabled plugin). +func discoverPluginComposeFilesWanted(fx Effects, sink Sink, workdir, pluginDir string, wanted func(name string) bool, refreshed ...*bool) ([]string, error) { entries, err := os.ReadDir(pluginDir) if err != nil { if os.IsNotExist(err) { @@ -89,6 +96,9 @@ func discoverPluginComposeFilesFx(fx Effects, sink Sink, workdir, pluginDir stri if _, err := os.Stat(disabledPath); err == nil { continue } + if wanted != nil && !wanted(entry.Name()) { + continue + } composePath := filepath.Join(pluginDir, entry.Name(), pluginComposeFilename) absPath, err := filepath.Abs(composePath) if err != nil { diff --git a/internal/build/plugins_minio_intent_test.go b/internal/build/plugins_minio_intent_test.go new file mode 100644 index 000000000..f990a0ad8 --- /dev/null +++ b/internal/build/plugins_minio_intent_test.go @@ -0,0 +1,59 @@ +package build + +// plugins_minio_intent_test.go: D-0702. The minio plugin is installed machine-wide, +// so its compose fragment joins a project's stack only where the project wants +// MinIO (v1.5); v1.4 keeps merging every enabled plugin. Another plugin merges +// in every case, and the check reads the manifest a real Build writes. + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/nself-org/cli/internal/compat/compattest" +) + +func TestBuildMergesMinioFragmentOnlyWithMinioIntent(t *testing.T) { + tests := []struct { + name string + v15, enabled bool + wantMinio bool + }{ + {"v1.4 intent off", false, false, true}, + {"v1.4 intent on", false, true, true}, + {"v1.5 intent off", true, false, false}, + {"v1.5 intent on", true, true, true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + f := freshProject(t, "dev-minimal") // isolates the env, so the mode is set after it + compattest.Set(t, tc.v15) + t.Setenv("OBJECT_STORAGE_ENABLED", "false") + writePlanPlugin(t, f.plugins, "minio", 3911, false) + writePlanPlugin(t, f.plugins, "nself-alpha", 3901, false) + if tc.enabled { + env := filepath.Join(f.workdir, ".env") + data, err := os.ReadFile(env) + if err != nil { + t.Fatal(err) + } + writeFixtureFile(t, env, string(data)+"\nMINIO_ENABLED=true\nMINIO_ROOT_USER=minio-user-Tq8Zk\nMINIO_ROOT_PASSWORD=Vb3Nc7Rx9Wt5Kq2Hy8Zp4Lm6Jd1Sa0Fe\n", 0o600) + } + if _, err := Build(f.workdir, BuildOptions{}); err != nil { + t.Fatalf("build: %v", err) + } + raw, err := os.ReadFile(filepath.Join(f.workdir, composeManifestFile)) + if err != nil { + t.Fatal(err) + } + manifest := filepath.ToSlash(string(raw)) + if got := strings.Contains(manifest, "/minio/"); got != tc.wantMinio { + t.Errorf("minio fragment in manifest = %v, want %v:\n%s", got, tc.wantMinio, manifest) + } + if !strings.Contains(manifest, "/nself-alpha/") { + t.Errorf("control plugin missing from manifest:\n%s", manifest) + } + }) + } +} diff --git a/internal/compose/object_storage.go b/internal/compose/object_storage.go index 68cc4353e..8309ac353 100644 --- a/internal/compose/object_storage.go +++ b/internal/compose/object_storage.go @@ -57,6 +57,22 @@ func ActiveStore(cfg *config.Config, installed bool) string { return "none" } +// Purpose: decide whether an installed plugin's compose fragment joins this project's stack. +// Inputs: the plugin directory name and the loaded config. +// Outputs: false only for the minio plugin in v1.5 when MinIO is not this project's active store. +// Constraints: the plugin is installed machine-wide, so intent comes from the project (D-0702); +// every other plugin, and v1.4, merge as before. +func PluginFragmentWanted(name string, cfg *config.Config) bool { + if name != "minio" { + return true + } + // compat.V15(P7-PLUG-60): the minio fragment merges into every project once installed -> only where ActiveStore picks the plugin + if compat.V15() { + return ActiveStore(cfg, true) == "minio-plugin" + } + return true +} + // ActiveStoreConnection returns the internal endpoint and configured region for // the store selected by ActiveStore. // Inputs: loaded config and whether the MinIO plugin is installed.