Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/wiki/Compat-V15.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ The table is generated. Never hand-edit it; run `compat-markers.sh --write-wiki`
| D-0330 | remove <plugin> drops plugin data unless --keep-data | keeps it unless --delete-data, confirmed by --yes or a terminal | `cmd/commands/plugin_remove_data.go` |
| D-0430 | "Error: trust warnings present (exit code 2)" and exit 1 | silent exit 2, the canon's warning class | `cmd/commands/trust_status_detailed.go` |
| D-0437 | doctor only repairs generated files | diagnoses and repairs deferred boot-plugin table tracking | `cmd/commands/doctor_plugin_tables.go` |
| D-0478 | PLUGIN_DIR "~/..." used as written, no plugin snippets on default config | "~" expanded, installed plugins' snippets injected | `internal/build/plugin_configs.go` |
| D-0478 | PLUGIN_DIR "~/..." used as written, no plugin snippets on default config | "~" expanded, installed plugins' snippets injected | `internal/config/plugin_dir.go` |
| D-0484 | pin store ancestor walk warns once and continues | refuses | `sdk/go/remote/knownhosts_compat.go` |
| D-0490 | a fragment may include another file, extend a service of one, build on the host network, join a network it does not declare or redefine a core service (warning) | E129 fails the build | `internal/build/plugins_network_normalize.go` |
| D-0491 | replacing another owner's or a core nginx/sites file warns | E055 fails the build | `internal/build/plugin_nginx_collision.go` |
| D-0676 | bare stream path streams as path:// with a notice | refused | `internal/backup/stream_dest_compat.go` |
| D-0752 | plugin disable never confirmed | prod-class and hand-edit confirmation before the marker is written. | `cmd/commands/plugin_disable_gate.go` |
Expand Down
1 change: 1 addition & 0 deletions .github/wiki/Plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The reason a tier was chosen is one of `installed`, `override`, `entitlement`, `
- **Foreign networks (E129).** A fragment may use an external network only if it is the project network (`${DOCKER_NETWORK}`, or `<project>_network`); aliases and merge keys are resolved before this check, `external` written as a string (`"true"`, `yes`, `${VAR}`) counts as external, and a network with an explicit `name:` that is not project-scoped counts too (the name must be `${COMPOSE_PROJECT_NAME}_<plugin>_<suffix>`, and the suffix is not `default`), because compose would join an existing network of that name. Any other external network fails the build in v1.5 mode (E129 names the plugin and network) and warns in v1.4 mode. Other v1.5 network cases, each E129 with its own cause in the message (v1.4 warns for the first two and ignores the rest):
- A fragment with more than one YAML document (`---`): compose merges them all, the check reads the first.
- A service `network_mode` other than `none` or `service:<a service of this fragment>` (`host`, `container:x`, a network name, a variable).
- A fragment that reaches outside its own stack (D-0490; v1.4 warns): a top-level `include:`, a service that `extends` a service of another `file:`, a `build.network` other than `none` or `default`, a service that lists a network the fragment does not declare (other than the project network or `default`), or a service named like a core service (`postgres`, `hasura`, `auth`, `nginx`).
- A service supplied by a `<<` merge key under `services:` with no `networks:`: it has no place to attach the project network, so write it out.
- A fragment that decodes as YAML but not as compose values, so the policy could not be checked.
- A legacy project network written in a form the build cannot rewrite in place (see Networks).
Expand Down
12 changes: 1 addition & 11 deletions cmd/commands/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/nself-org/cli/internal/config"
Expand Down Expand Up @@ -189,16 +188,7 @@ func init() {
// --- helpers ---

// resolvePluginDir returns the plugin directory from config or a sensible default.
func resolvePluginDir() string {
if d := os.Getenv("NSELF_PLUGIN_DIR"); d != "" {
return d
}
home, err := os.UserHomeDir()
if err != nil {
return filepath.Join("/tmp", ".nself", "plugins")
}
return filepath.Join(home, ".nself", "plugins")
}
func resolvePluginDir() string { return config.DefaultPluginDir() }

// loadConfig loads the project config from the current working directory.
func loadConfig() (*config.Config, error) {
Expand Down
2 changes: 1 addition & 1 deletion internal/apidocs/openapi_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func NginxConf(docsServePath, baseDomain string) string {
if baseDomain == "" {
baseDomain = "localhost"
}
certDir := strings.ReplaceAll(baseDomain, ".", "-")
certDir := nginxtopo.CertDirName(baseDomain)
return fmt.Sprintf(`# Generated by nself build - DO NOT EDIT MANUALLY
# Service: docs.%s

Expand Down
6 changes: 3 additions & 3 deletions internal/build/lockfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,11 +578,11 @@ func TestLockUsesRediscoveredComposeFiles(t *testing.T) {
lockBuild(t, f)
frag := filepath.Join(f.plugins, "tweak")
writeFixtureFile(t, filepath.Join(frag, "plugin.json"), `{"name":"tweak","port":3999,"language":"go"}`, 0o644)
writeFixtureFile(t, filepath.Join(frag, pluginComposeFilename), "services:\n hasura:\n image: hasura/graphql-engine:v2.51.0\n", 0o644)
writeFixtureFile(t, filepath.Join(frag, pluginComposeFilename), "services:\n redis:\n image: redis:7.4.1\n", 0o644)
lockBuild(t, f)
l := readLock(t, f)
if got := l.Unpinned["hasura"] + l.Images["hasura"]; got != "hasura/graphql-engine:v2.51.0" {
t.Fatalf("a fragment added in this build is missing from the lock: hasura %q", got)
if got := l.Unpinned["redis"] + l.Images["redis"]; got != "redis:7.4.1" {
t.Fatalf("a fragment added in this build is missing from the lock: redis %q", got)
}
cur, err := CurrentLock(f.workdir)
if err != nil {
Expand Down
5 changes: 1 addition & 4 deletions internal/build/orchestrator_nginx.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,7 @@ func (s *snippetSet) parse() {
// skipped (pluginSnippetSkip, hasSSL = the model's TLS) the same way; a
// skipped snippet is recorded in global, not dropped. Read errors are left to the injection step, which reports them.
func (s *snippetSet) plugins(workdir string, cfg *config.Config, hasSSL bool) {
pluginDir, err := resolvePluginNginxDir("", cfg)
if err != nil {
return
}
pluginDir := resolvePluginNginxDir("", cfg)
entries, err := os.ReadDir(pluginDir)
if err != nil {
return
Expand Down
47 changes: 3 additions & 44 deletions internal/build/plugin_configs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import (
"path/filepath"
"strings"

"github.com/nself-org/cli/internal/compat"
"github.com/nself-org/cli/internal/config"
"github.com/nself-org/cli/internal/ssl"
"github.com/nself-org/cli/internal/nginxtopo"
)

// RenderPluginConfigs walks installed plugins' configs/ directories
Expand Down Expand Up @@ -124,13 +123,7 @@ func buildTemplateVars(workdir string, cfg *config.Config) map[string]string {
network = cfg.ProjectName + "_network"
}

pluginDir := expandPluginDir(cfg.PluginSystem.Dir)
if pluginDir == "" {
home, _ := os.UserHomeDir()
if home != "" {
pluginDir = filepath.Join(home, ".nself", "plugins")
}
}
pluginDir := config.ResolvePluginDir(cfg.PluginSystem.Dir)

return map[string]string{
"SSL_DIR": pluginSSLDir(cfg.BaseDomain),
Expand All @@ -155,39 +148,5 @@ func buildTemplateVars(workdir string, cfg *config.Config) map[string]string {
// certificates/ level. An empty domain falls back to localhost, as the
// generator does.
func pluginSSLDir(baseDomain string) string {
dir := ssl.DomainToDirName(baseDomain)
if dir == "" {
dir = "localhost"
}
return "certificates/" + dir
}

// expandPluginDir is expandHomeDir under v1.5 only (D-0634). v1.4.13 used
// PLUGIN_DIR as written, so the default "~/.nself/plugins" never resolved: a
// default-config project got no plugin nginx snippets, and
// ${NSELF_PLUGIN_DIR} rendered as "~/.nself/plugins". Expanding it in a patch
// would publish every installed plugin at <plugin>.<base> on the first
// rebuild after an upgrade, and fail the build on a snippet the user had
// copied into nginx/sites by hand, so v1.4 keeps the old value.
func expandPluginDir(dir string) string {
// compat.V15(D-0478): PLUGIN_DIR "~/..." used as written, no plugin snippets on default config -> "~" expanded, installed plugins' snippets injected
if compat.V15() {
return expandHomeDir(dir)
}
return dir
}

// expandHomeDir turns a leading "~" or "~/" into the user's home dir.
// config.ApplyDefaults sets PLUGIN_DIR to the literal "~/.nself/plugins",
// which os.Stat would otherwise read relative to the working dir, so no
// plugin snippet was ever found. Anything else is returned unchanged.
func expandHomeDir(dir string) string {
if dir != "~" && !strings.HasPrefix(dir, "~/") {
return dir
}
home, err := os.UserHomeDir()
if err != nil || home == "" {
return dir
}
return filepath.Join(home, strings.TrimPrefix(dir, "~"))
return "certificates/" + nginxtopo.CertDirNameOrLocalhost(baseDomain)
}
4 changes: 2 additions & 2 deletions internal/build/plugin_configs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ func TestExpandHomeDir(t *testing.T) {
"": "",
}
for in, want := range cases {
if got := expandHomeDir(in); got != want {
t.Errorf("expandHomeDir(%q) = %q, want %q", in, got, want)
if got := config.ExpandHomeDir(in); got != want {
t.Errorf("ExpandHomeDir(%q) = %q, want %q", in, got, want)
}
}
}
Expand Down
19 changes: 4 additions & 15 deletions internal/build/plugin_nginx.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,20 +152,12 @@ func pluginSnippetSkip(rendered string, vars map[string]string, hasSSL bool) str

// resolvePluginNginxDir returns the plugin directory the nginx snippets are
// read from: pluginDir, else the configured one, else ~/.nself/plugins. A
// leading "~" is expanded under v1.5 only (expandPluginDir, D-0634).
func resolvePluginNginxDir(pluginDir string, cfg *config.Config) (string, error) {
// leading "~" is expanded under v1.5 only (config.ExpandPluginDir, D-0634).
func resolvePluginNginxDir(pluginDir string, cfg *config.Config) string {
if pluginDir == "" {
pluginDir = cfg.PluginSystem.Dir
}
pluginDir = expandPluginDir(pluginDir)
if pluginDir == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolving home directory: %w", err)
}
pluginDir = filepath.Join(home, ".nself", "plugins")
}
return pluginDir, nil
return config.ResolvePluginDir(pluginDir)
}

// pluginConfPaths lists a plugin's nginx/*.conf files (none when it ships no
Expand Down Expand Up @@ -203,10 +195,7 @@ func renderPluginSnippet(content string, vars map[string]string) string {

// injectPluginNginxRoutesVia is InjectPluginNginxRoutes writing through sink.
func injectPluginNginxRoutesVia(sink Sink, workdir, pluginDir string, cfg *config.Config, hasSSL bool) (int, error) {
pluginDir, err := resolvePluginNginxDir(pluginDir, cfg)
if err != nil {
return 0, err
}
pluginDir = resolvePluginNginxDir(pluginDir, cfg)

// If the plugin directory does not exist, there is nothing to inject.
if _, err := os.Stat(pluginDir); os.IsNotExist(err) {
Expand Down
4 changes: 2 additions & 2 deletions internal/build/plugin_nginx_routes_build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ server { listen 80; server_name idme.${BASE_DOMAIN}; return 301 https://$host$re
server {
listen 443 ssl http2;
server_name idme.${BASE_DOMAIN};
ssl_certificate /etc/nginx/ssl/${BASE_DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/${BASE_DOMAIN}/privkey.pem;
ssl_certificate /etc/nginx/ssl/certificates/${BASE_DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/certificates/${BASE_DOMAIN}/privkey.pem;
location / { proxy_pass http://idme_backend; proxy_read_timeout 60s; weird_thing on; }
}
`
Expand Down
4 changes: 2 additions & 2 deletions internal/build/plugin_nginx_routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ server { listen 80; server_name idme.${BASE_DOMAIN}; return 301 https://$host$re
server {
listen 443 ssl http2;
server_name idme.${BASE_DOMAIN};
ssl_certificate /etc/nginx/ssl/${BASE_DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/${BASE_DOMAIN}/privkey.pem;
ssl_certificate /etc/nginx/ssl/certificates/${BASE_DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/certificates/${BASE_DOMAIN}/privkey.pem;
location / { proxy_pass http://idme_backend; proxy_read_timeout 60s; weird_thing on; }
}
`
Expand Down
12 changes: 2 additions & 10 deletions internal/build/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"sync"

"github.com/nself-org/cli/internal/compat"
"github.com/nself-org/cli/internal/config"
"github.com/nself-org/cli/internal/docker"
"github.com/nself-org/cli/internal/errs"
"github.com/nself-org/cli/internal/license/entitlement"
Expand All @@ -44,16 +45,7 @@ var refreshPluginEntitlement = entitlement.RefreshForPlugins
// (~/.nself/plugins). The NSELF_PLUGIN_DIR environment variable overrides the
// default — used for per-project plugin sets, hermetic tests, and CI. Falls
// back to /tmp/.nself/plugins when the home directory cannot be determined.
func DefaultPluginDir() string {
if dir := strings.TrimSpace(os.Getenv("NSELF_PLUGIN_DIR")); dir != "" {
return dir
}
home, err := os.UserHomeDir()
if err != nil {
return filepath.Join("/tmp", ".nself", "plugins")
}
return filepath.Join(home, ".nself", "plugins")
}
func DefaultPluginDir() string { return config.DefaultPluginDir() }

// DiscoverPluginComposeFiles scans pluginDir for installed plugins that
// contain a docker-compose.plugin.yml file. It returns absolute paths to
Expand Down
16 changes: 13 additions & 3 deletions internal/build/plugins_core_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"os"
"path/filepath"
"regexp"
"strings"

"github.com/nself-org/cli/internal/compat"
"github.com/nself-org/cli/internal/compose"
Expand Down Expand Up @@ -188,9 +189,7 @@ func normalizeComposePluginCoreEnv(content []byte, pluginDir, pluginName string)
// A pure third-party stack has no port (free/monitoring): nothing to
// inject by design, so no warning.
if port > 0 {
warnOnce(fmt.Sprintf(
"plugin %q: %s has several services and none is named %s, nself-%s or plugin-%s, so the core env vars were not injected",
pluginName, filepath.Join(pluginDir, pluginName, pluginComposeFilename), pluginName, pluginName, pluginName))
warnOnce(noMatchServiceWarning(pluginDir, pluginName, manifestName))
}
return content
}
Expand All @@ -217,6 +216,17 @@ func normalizeComposePluginCoreEnv(content []byte, pluginDir, pluginName string)
return append(out, content[end:]...)
}

// noMatchServiceWarning names every service name the lookup tried, built from
// pluginServiceCandidates so the message cannot drift from the lookup (D-0604):
// the manifest-name forms appear when plugin.json's name differs from the
// directory name.
func noMatchServiceWarning(pluginDir, pluginName, manifestName string) string {
return fmt.Sprintf(
"plugin %q: %s has several services and none is named one of %s, so the core env vars were not injected",
pluginName, filepath.Join(pluginDir, pluginName, pluginComposeFilename),
strings.Join(pluginServiceCandidates(pluginName, manifestName), ", "))
}

// warnUnrecognisedEnvBlock reports a plugin compose fragment whose
// environment configuration normalizeComposePluginCoreEnv cannot safely
// rewrite: an env_file-only fragment with no environment: block and no
Expand Down
24 changes: 24 additions & 0 deletions internal/build/plugins_core_env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,27 @@ func TestAddPluginCoreEnvVars_Populated(t *testing.T) {
t.Errorf("empty NotifySecret must not be written, got: %v", vars["NOTIFY_INTERNAL_SECRET"])
}
}

// TestNormalizeComposePluginCoreEnv_NoMatchWarningListsManifestNames: when
// plugin.json's name differs from the directory name, the no-match warning
// lists the manifest-name candidates the lookup also tried (D-0604).
func TestNormalizeComposePluginCoreEnv_NoMatchWarningListsManifestNames(t *testing.T) {
resetWarnOnce()
pluginDir := t.TempDir()
dir := filepath.Join(pluginDir, "dirname")
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"name":"manifestname","port":3100}`), 0644); err != nil {
t.Fatal(err)
}
in := "services:\n a:\n image: x\n b:\n image: y\n"
stderr := captureStderr(t, func() {
normalizeComposePluginCoreEnv([]byte(in), pluginDir, "dirname")
})
for _, want := range []string{"dirname", "nself-dirname", "plugin-dirname", "manifestname", "nself-manifestname", "plugin-manifestname"} {
if !strings.Contains(stderr, want) {
t.Errorf("warning must name candidate %q, got: %q", want, stderr)
}
}
}
Loading
Loading