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
14 changes: 14 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,20 @@ writer follows the gateway's current address. The `--output-env` file is a map
of the run's current environment, rewritten whole: a key written again
replaces its line, so a reader never meets two values for one key.

**What `--output-env` writes, and for which environment.** The file is the
service's process environment as the agent receives it, owner-only (`0600`):
its own configuration and runtime configurations, the workspace groups it is
handed, and — written once at Start, after the dependency barrier — its
dependencies' configurations; secret values are in it, which is why it is
owner-only and never committed. A workspace group that is one YAML or JSON
**structured document** rather than key/value pairs (the composition root's
merged `fixture-composition`, for example) is carried under a key that hashes
the group's origin, its name and the **selected run environment** — the same
key the SDK derives to read it back — so every write names the environment the
run selected (`--env`), the excluded root's file included. A writer handed no
environment refuses by name rather than writing a document no reader could
find.

**The delivery host's federation surface.** In a host environment the native
run hands the service `host.delivery` names, when it declares the `federation`
workspace configuration, the three keys that turn its delivery surface on,
Expand Down
5 changes: 5 additions & 0 deletions pkg/orchestration/excluded_root_output_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,14 @@ func (flow *Flow) exportExcludedOriginEnvironment(ctx context.Context) error {
return fmt.Errorf("resolve excluded root dependency endpoints: %w", err)
}

environment, err := flow.world.Env.Proto()
if err != nil {
return fmt.Errorf("name the run environment for the excluded root's output environment: %w", err)
}
if err := AppendServiceProcessConfigurationsToFile(
ctx,
flow.outputEnvPath,
environment,
runtimeContext,
serviceConfiguration,
workspaceConfigurations,
Expand Down
30 changes: 29 additions & 1 deletion pkg/orchestration/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,9 +372,14 @@ func (runner *Runner) Init(ctx context.Context) (*OutputProperty, error) {
// every dependency secret in the owner-only file (and record stale
// pre-barrier values). Own, workspace, and this service's own runtime
// configurations are final at Init, so they are written now.
environment, envErr := runner.world.Env.Proto()
if envErr != nil {
return nil, w.Wrapf(envErr, "cannot name the run environment for the output environment")
}
err = AppendServiceProcessConfigurationsToFile(
ctx,
runner.outputEnv,
environment,
runtimeContext,
conf,
workspaceConfigurations,
Expand Down Expand Up @@ -980,9 +985,14 @@ func (runner *Runner) Start(ctx context.Context) (*OutputProperty, error) {
if dependencyErr != nil {
return nil, w.Wrapf(dependencyErr, "cannot load dependency configurations for output environment")
}
environment, envErr := runner.world.Env.Proto()
if envErr != nil {
return nil, w.Wrapf(envErr, "cannot name the run environment for the output environment")
}
err = AppendServiceProcessConfigurationsToFile(
ctx,
runner.outputEnv,
environment,
runtimeContext,
nil,
nil,
Expand Down Expand Up @@ -1829,11 +1839,12 @@ func (runner *Runner) WithRemote(environment *environments.Environment) {
runner.remoteEnvironment = environment
}

func AppendEnvironmentVariablesToFile(ctx context.Context, filePath string, confs []*basev0.Configuration) error {
func AppendEnvironmentVariablesToFile(ctx context.Context, filePath string, environment *basev0.Environment, confs []*basev0.Configuration) error {
w := wool.Get(ctx).In("resources.AppendToFile", wool.Field("filePath", filePath))
// filter out for native
filtered := resources.FilterConfigurations(confs, resources.NewRuntimeContextNative())
m := resources.NewEnvironmentVariableManager()
m.SetEnvironment(environment)
err := m.AddConfigurations(ctx, filtered...)
if err != nil {
return w.Wrapf(err, "cannot add configurations")
Expand All @@ -1852,17 +1863,34 @@ func AppendEnvironmentVariablesToFile(ctx context.Context, filePath string, conf
// runtime configurations are filtered to the selected backend. The output is
// owner-only because these inputs intentionally include provider credentials
// and dependency connection secrets.
//
// The run's environment is part of what is written, not decoration: a
// structured configuration document (a workspace group that is one YAML or
// JSON document rather than key/value pairs, such as the composition root's
// fixture-composition) is carried under a key that hashes its origin, its name
// AND the environment, which is how the SDK finds it again
// (resources.ConfigurationDocumentKey). The agent's own Init request carries
// the environment and so could always deliver such a document; this writer
// built its manager without one, so the first root-owned document a composition
// handed every service made `--output-env` refuse the whole run at Init with
// core's "configuration document requires origin, name and environment" — the
// shape platform-core's composition gate met on 0.1.219.
func AppendServiceProcessConfigurationsToFile(
ctx context.Context,
filePath string,
environment *basev0.Environment,
runtimeContext *basev0.RuntimeContext,
serviceConfiguration *basev0.Configuration,
workspaceConfigurations []*basev0.Configuration,
dependencyConfigurations []*basev0.Configuration,
runtimeConfigurations []*basev0.Configuration,
) error {
w := wool.Get(ctx).In("resources.AppendServiceProcessConfigurationsToFile", wool.Field("filePath", filePath))
if environment == nil || environment.GetName() == "" {
return w.NewError("the service process environment names no run environment, so a configuration document could not be carried under the key its reader derives")
}
manager := resources.NewEnvironmentVariableManager()
manager.SetEnvironment(environment)
if err := manager.AddConfigurations(ctx, serviceConfiguration); err != nil {
return w.Wrapf(err, "cannot add service configuration")
}
Expand Down
62 changes: 56 additions & 6 deletions pkg/orchestration/runner_output_env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestAppendEnvironmentVariablesToFileIsOwnerOnly(t *testing.T) {
t.Fatal(err)
}

if err := AppendEnvironmentVariablesToFile(context.Background(), path, nil); err != nil {
if err := AppendEnvironmentVariablesToFile(context.Background(), path, localRunEnvironment(), nil); err != nil {
t.Fatal(err)
}
info, err := os.Stat(path)
Expand All @@ -40,7 +40,7 @@ func TestAppendEnvironmentVariablesToFileRejectsSymlink(t *testing.T) {
t.Fatal(err)
}

if err := AppendEnvironmentVariablesToFile(context.Background(), link, nil); err == nil {
if err := AppendEnvironmentVariablesToFile(context.Background(), link, localRunEnvironment(), nil); err == nil {
t.Fatal("expected symlink rejection")
}
}
Expand Down Expand Up @@ -130,9 +130,17 @@ func TestAppendServiceProcessConfigurationsToFileIncludesSecretsAndFiltersDepend
)
runtime := configuration("mind/mind", "runtime", "session_id", "session-123", false, native)

// The composition root's own structured document, handed to every service
// of the run as platform-core's merged fixture-composition is: it is
// carried under a key that hashes origin, name AND environment, so a
// writer that names no environment cannot write it at all.
document := &basev0.Configuration{Origin: resources.ConfigurationWorkspace, RuntimeContext: native, Infos: []*basev0.ConfigurationInformation{{
Name: "fixture-composition",
Data: &basev0.ConfigurationData{Kind: "yaml", Content: []byte("schema: codefly.dev/authority/fixture-composition/v1\nroles: []\n")},
}}}
if err := AppendServiceProcessConfigurationsToFile(
context.Background(), path, native, service,
[]*basev0.Configuration{workspace},
context.Background(), path, localRunEnvironment(), native, service,
[]*basev0.Configuration{workspace, document},
[]*basev0.Configuration{postgres, containerOnly},
[]*basev0.Configuration{runtime},
); err != nil {
Expand All @@ -148,11 +156,21 @@ func TestAppendServiceProcessConfigurationsToFileIncludesSecretsAndFiltersDepend
"CODEFLY__WORKSPACE_CONFIGURATION__DOGFOOD__CAMPAIGN=headless\n",
"CODEFLY__SERVICE_SECRET_CONFIGURATION__INFRA__POSTGRES__POSTGRES__READ_WRITE_CONNECTION=postgres://runtime\n",
"CODEFLY__SERVICE_CONFIGURATION__MIND__MIND__RUNTIME__SESSION_ID=session-123\n",
// The document, under the key its reader derives for this environment.
resources.ConfigurationDocumentKey(resources.ConfigurationWorkspace, "fixture-composition", "local", false) + "=",
} {
if !strings.Contains(text, expected) {
t.Fatalf("service process environment is missing %q; keys=%v", expected, environmentKeys(text))
}
}
carrier := carrierValue(t, text, resources.ConfigurationDocumentKey(resources.ConfigurationWorkspace, "fixture-composition", "local", false))
content, err := resources.DecodeConfigurationDocument(carrier, resources.ConfigurationWorkspace, "fixture-composition", "local", false)
if err != nil {
t.Fatalf("the written document does not read back for environment local: %v", err)
}
if !strings.Contains(string(content), "codefly.dev/authority/fixture-composition/v1") {
t.Fatalf("the document lost its content: %s", content)
}
if strings.Contains(text, "REDIS") || strings.Contains(text, "redis://container") {
t.Fatalf("container-only dependency leaked into native environment; keys=%v", environmentKeys(text))
}
Expand Down Expand Up @@ -192,13 +210,13 @@ func TestServiceProcessOutputEnvWritesDependencyConfigsOncePerRun(t *testing.T)

// Init phase: own + runtime only, NO dependency configurations.
if err := AppendServiceProcessConfigurationsToFile(
context.Background(), path, native, own, nil, nil, []*basev0.Configuration{runtime},
context.Background(), path, localRunEnvironment(), native, own, nil, nil, []*basev0.Configuration{runtime},
); err != nil {
t.Fatal(err)
}
// Start phase: refreshed dependency configurations only.
if err := AppendServiceProcessConfigurationsToFile(
context.Background(), path, native, nil, nil, []*basev0.Configuration{dependency}, nil,
context.Background(), path, localRunEnvironment(), native, nil, nil, []*basev0.Configuration{dependency}, nil,
); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -400,3 +418,35 @@ func TestAppendRuntimeEnvironmentToFileOmitsForbiddenDependencyEndpoint(t *testi
t.Fatalf("private endpoint reached the output environment:\n%s", text)
}
}

// localRunEnvironment is the run environment the writers name their carriers
// for, as the runner reads it off the world.
func localRunEnvironment() *basev0.Environment { return &basev0.Environment{Name: "local"} }

// carrierValue is the value written for key in a KEY=value file.
func carrierValue(t *testing.T, text, key string) string {
t.Helper()
for _, line := range strings.Split(text, "\n") {
if value, ok := strings.CutPrefix(line, key+"="); ok {
return value
}
}
t.Fatalf("%s is not in the file; keys=%v", key, environmentKeys(text))
return ""
}

// A writer that names no run environment refuses before it writes anything,
// by name, rather than failing on the first document with core's contract
// error — which is what platform-core's composition gate saw on 0.1.219: the
// composition root's merged fixture-composition reached every service's
// `--output-env` and the whole run refused at the first Init.
func TestServiceProcessOutputEnvRequiresTheRunEnvironment(t *testing.T) {
path := filepath.Join(t.TempDir(), "runtime.env")
err := AppendServiceProcessConfigurationsToFile(context.Background(), path, nil, resources.NewRuntimeContextNative(), nil, nil, nil, nil)
if err == nil || !strings.Contains(err.Error(), "names no run environment") {
t.Fatalf("got %v, want the writer to refuse without an environment", err)
}
if _, statErr := os.Stat(path); !os.IsNotExist(statErr) {
t.Fatalf("a refused write left a file: %v", statErr)
}
}
Loading