diff --git a/cmd/environment/configuration_profile.go b/cmd/environment/configuration_profile.go index 933f4710..23d01315 100644 --- a/cmd/environment/configuration_profile.go +++ b/cmd/environment/configuration_profile.go @@ -8,6 +8,8 @@ import ( "regexp" "strings" + "github.com/codefly-dev/cli/pkg/run/fixture" + "github.com/codefly-dev/cli/pkg/environments" "github.com/codefly-dev/core/configurations" "github.com/codefly-dev/core/shared" @@ -76,7 +78,7 @@ func materializeConfigurationProfile(ctx context.Context, directory string, env } // validateConfigurationSourceLayer refuses a source layer that is not an ordinary -// directory of flat .env inputs: a symlink, a derivation from the destination, an +// directory of flat .env inputs (and at most the module envelope): a symlink, a derivation from the destination, an // environment-specific runnable binding, or a secret file holding a value rather // than an empty placeholder. func validateConfigurationSourceLayer(layer, destination string) error { @@ -92,8 +94,11 @@ func validateConfigurationSourceLayer(layer, destination string) error { return err } for _, entry := range entries { - if !strings.HasSuffix(entry.Name(), ".env") && entry.Name() != configurations.ProfileDerivationFile { - return fmt.Errorf("configuration-source supports only flat .env inputs and profile derivation metadata: %s", entry.Name()) + // The module envelope is the one single-file declaration a source profile + // may carry: a cell deriving from it is delivered that ceiling (fixture.EnvelopeFile). + if !strings.HasSuffix(entry.Name(), ".env") && entry.Name() != configurations.ProfileDerivationFile && + entry.Name() != fixture.EnvelopeFile { + return fmt.Errorf("configuration-source supports only flat .env inputs, the module envelope and profile derivation metadata: %s", entry.Name()) } info, infoErr := entry.Info() if infoErr != nil || !info.Mode().IsRegular() { diff --git a/cmd/environment/configuration_profile_test.go b/cmd/environment/configuration_profile_test.go index 132b62cd..1c9f485f 100644 --- a/cmd/environment/configuration_profile_test.go +++ b/cmd/environment/configuration_profile_test.go @@ -134,3 +134,17 @@ func TestProfileImportRefusesAuthoredOrSymlinkedFiles(t *testing.T) { }) } } + +// A source profile may carry the module envelope: every cell deriving from it is +// delivered that ceiling (fixture.EnvelopeFile), so importing a cell onto a +// profile that states one must not refuse it as an unvalidated input. +func TestProfileImportAcceptsTheSourceProfilesModuleEnvelope(t *testing.T) { + root := profileSource(t) + require.NoError(t, os.WriteFile(filepath.Join(root, "configurations", "hosted", "module-envelope.json"), []byte(`{"principals":{}}`+"\n"), 0o644)) + require.NoError(t, materializeConfigurationProfile(t.Context(), root, profileEnvironment("blue"), false)) + _, err := os.Stat(filepath.Join(root, "configurations", "blue", configurations.ProfileDerivationFile)) + require.NoError(t, err) + // It is accepted by its exact name only. + require.NoError(t, os.WriteFile(filepath.Join(root, "configurations", "hosted", "module-envelope.backup.json"), []byte("{}\n"), 0o644)) + require.ErrorContains(t, materializeConfigurationProfile(t.Context(), root, profileEnvironment("green"), false), "only flat .env inputs") +} diff --git a/docs/commands.md b/docs/commands.md index 837df00e..92805601 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -2467,7 +2467,8 @@ An imported environment may declare `configuration-source: hosted` and select in `configurations/blue/` deriving from `hosted`, using Core's profile inheritance. The profile chain selects the first directory at each location; inheritance adds shared groups and per-profile values. Import validates every inherited source: -only flat `.env` inputs and profile derivation metadata are accepted; secret +only flat `.env` inputs, the module envelope (`module-envelope.json`, which every +environment deriving from the profile inherits) and profile derivation metadata are accepted; secret files contain empty placeholders, symlinks are refused, and sources may not contain environment-specific runnable bindings. Dry-run writes nothing. Import refuses authored destination files and retires only its own earlier copied diff --git a/pkg/run/fixture/fixture.go b/pkg/run/fixture/fixture.go index 364e7be9..7c1153f8 100644 --- a/pkg/run/fixture/fixture.go +++ b/pkg/run/fixture/fixture.go @@ -284,14 +284,16 @@ func composedDirsOf(workspace *resources.Workspace) []string { // workspace holding no directory for it states no envelope for it, rather // than lending the envelope it states for another environment. func grantsPath(workspaceDir, profile string) (string, error) { - path, _, err := configurations.ProfileFile(context.Background(), workspaceDir, "configurations", []string{profile}, envelopeFile) + path, _, err := configurations.ProfileFile(context.Background(), workspaceDir, "configurations", []string{profile}, EnvelopeFile) if err != nil { return "", fmt.Errorf("resolve the module envelope of configuration profile %q in %s: %w", profile, workspaceDir, err) } return path, nil } -const envelopeFile = "module-envelope.json" +// EnvelopeFile is the module envelope's file name in a configuration profile +// directory: a single-file declaration a derived profile inherits (grantsPath). +const EnvelopeFile = "module-envelope.json" // composedWorkspacesOf is every workspace the composition inherits from, the // deepest first, so a base's base is read before the base.