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
27 changes: 26 additions & 1 deletion cmd/doctor_workspace_hop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,34 @@ func TestDoctorWorkspaceRecognizesGeneratedHopRoutes(t *testing.T) {
}
}

// A composition that names no module for the hop is READY: the declaring
// service receives hop-routes with an empty ROUTES, which the doctor reads
// from the same resolved inventory the run resolves (checkConfigurationSources
// takes generated groups from there, "never from a name exemption"), so
// nothing reports the group missing.
//
// This replaces a baselined refusal: `{"no selected modules", …,
// codeConfigurationMissing, "hop-routes"}` in the table below asserted that
// this composition fails the doctor. That refusal was the symptom of the
// delivery gap, not a rule — a host that stands a hop only on some
// compositions (saas's gateway on a cell) declares the group always and must
// start where it stands no hop — so the case is replaced by this one rather
// than kept green.
func TestDoctorWorkspaceAcceptsAHopThatSelectsNoModule(t *testing.T) {
dir := doctorHopWorkspace(t)
require.NoError(t, os.Remove(filepath.Join(dir, "configurations/local/hop.env")))
before := snapshotTree(t, dir)
for _, opts := range []workspaceReadinessOptions{{dir: dir}, {dir: dir, service: "authority/hop"}} {
report := runReadiness(t, opts)
require.Equal(t, readinessStatusReady, report.Status, reportJSON(t, report))
requireNoCode(t, report, codeConfigurationMissing)
requireNoCode(t, report, codeConfigurationDirMissing)
}
require.Equal(t, before, snapshotTree(t, dir), "doctor must not materialize an empty route table")
}

func TestDoctorWorkspaceStillRefusesInvalidHopInputs(t *testing.T) {
for _, tc := range []struct{ name, path, content, code, reason string }{
{"no selected modules", "configurations/local/hop.env", "", codeConfigurationMissing, "hop-routes"},
{"missing catalog", "modules/records/" + composition.APIContractCatalogFileName, "", codeConfigurationInvalid, "not checked"},
{"bad principal", "modules/records/module.contract.codefly.yaml", "schema: codefly/module-contract/v1\nprincipal: other\n", codeConfigurationInvalid, "principal"},
{"authored routes", "configurations/local/hop-routes.env", "ROUTES={}\n", codeConfigurationInvalid, "remove its authored configuration"},
Expand Down
1 change: 1 addition & 0 deletions cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func init() {
GenerateCmd.AddCommand(generate.PermissionsCmd)
GenerateCmd.AddCommand(generate.RunnablesCmd)
GenerateCmd.AddCommand(generate.RunnableBindingsCmd)
GenerateCmd.AddCommand(generate.FixtureCompositionCmd)
GenerateCmd.AddCommand(generate.ClientCmd)
GenerateCmd.AddCommand(generate.TenantOverlaysCmd)
}
75 changes: 75 additions & 0 deletions cmd/generate/fixture_composition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package generate

import (
"github.com/codefly-dev/cli/cmd/common"
"github.com/codefly-dev/cli/pkg/cli"
"github.com/codefly-dev/cli/pkg/environments"
"github.com/codefly-dev/cli/pkg/fixturefragments"
"github.com/spf13/cobra"
)

var fixtureCompositionEnv string

// FixtureCompositionCmd writes the local authority's fixture-composition
// document for one environment, as `codefly run` does before the authority
// boots.
var FixtureCompositionCmd = &cobra.Command{
Use: "fixture-composition",
Short: "Merge every composed module's fixture-composition fragment into the workspace's document",
Long: `Merge the fixture-composition fragment every module of the workspace ships
(configurations/<profile>/` + fixturefragments.Group + `.yaml in the module's own directory)
into the workspace's configurations/<profile>/` + fixturefragments.Group + `.yaml, the document
the local authority reads.

Each module states, once, what the local authority's standard fixture roles may
do at its kinds; the composition's own module states only what is its own (a
collection its seed writes). Every grant of a role on one kind, every
installation and every stand-in module has exactly one source: two fragments
stating the same one are refused, naming both. The rules are the authority's
(codefly-dev/authority libraries/fixture-composition).

` + "`codefly run`" + ` writes the same document before the local authority boots, so a
developer never runs this first. The written file says it is generated in its
first line; a hand-written document is never overwritten, and this command
refuses it by name.

Examples:
codefly generate fixture-composition
codefly generate fixture-composition --env local`,
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
ctx, done := common.NewContext()
defer done()
// Fragments ship inside the composed modules, so a pinned module is
// materialized first, as a run does.
workspace, err := common.LoadWorkspaceWithPinnedModules(ctx)
if err != nil {
return err
}
env, err := environments.Select(workspace, fixtureCompositionEnv)
if err != nil {
return err
}
prepared, err := fixturefragments.Prepare(ctx, workspace, env)
if err != nil {
return err
}
changed, err := fixturefragments.Write(prepared)
if err != nil {
return err
}
verb := "is current"
if changed {
verb = "written"
}
cli.Header(1, "%s %s, from %d fragment(s)", prepared.Target, verb, len(prepared.Sources))
for _, source := range prepared.Sources {
cli.Info(" %s", source)
}
return nil
},
}

func init() {
FixtureCompositionCmd.Flags().StringVar(&fixtureCompositionEnv, "env", "local", "environment whose configuration profile the document is written for")
}
44 changes: 44 additions & 0 deletions cmd/run/fixture_composition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package run

import (
"context"
"errors"
"fmt"
"strings"

"github.com/codefly-dev/core/resources"

"github.com/codefly-dev/cli/pkg/cli"
"github.com/codefly-dev/cli/pkg/environments"
"github.com/codefly-dev/cli/pkg/fixturefragments"
)

// provisionFixtureComposition writes, before a run loads its configuration,
// the local authority's fixture-composition document: the merge of the
// fragment every module of the workspace ships (fixturefragments), which
// `codefly generate fixture-composition` writes on its own. Nothing is written
// unless a service declares the group. A conflict between two fragments
// refuses the run naming both. A hand-written document is never overwritten:
// the run reads it as it stands and says that no module's fragment is applied.
func provisionFixtureComposition(ctx context.Context, workspace *resources.Workspace, env *environments.Environment) error {
declared, err := fixturefragments.Declared(ctx, workspace)
if err != nil || !declared {
return err
}
prepared, err := fixturefragments.Prepare(ctx, workspace, env)
if err != nil {
return fmt.Errorf("%s: %w", fixturefragments.Group, err)
}
changed, err := fixturefragments.Write(prepared)
if errors.Is(err, fixturefragments.ErrHandWritten) {
cli.Warning("%v. This run reads it as it stands: no module's fragment is applied", err)
return nil
}
if err != nil {
return err
}
if changed {
cli.Info("merged %s from %d fragment(s): %s", prepared.Target, len(prepared.Sources), strings.Join(prepared.Sources, ", "))
}
return nil
}
58 changes: 58 additions & 0 deletions cmd/run/fixture_composition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package run

import (
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/codefly-dev/core/resources"
"github.com/stretchr/testify/require"

"github.com/codefly-dev/cli/pkg/environments"
"github.com/codefly-dev/cli/pkg/fixturefragments"
)

// A run writes the merged fixture-composition document before anything reads
// the configuration, and leaves a hand-written one as it stands.
func TestARunMergesTheFixtureCompositionAndKeepsAHandWrittenOne(t *testing.T) {
dir := t.TempDir()
head := "schema: codefly.dev/authority/fixture-composition/v1\n"
for name, content := range map[string]string{
"workspace.codefly.yaml": "name: playground\nlayout: modules\nmodules:\n - name: authority\n - name: store\nenvironments:\n - name: local\n",
"modules/authority/module.codefly.yaml": "kind: module\nname: authority\nservices:\n - name: local\n",
"modules/authority/services/local/service.codefly.yaml": "name: local\nversion: 0.1.0\nworkspace-configuration-dependencies:\n - " + fixturefragments.Group + "\n",
"modules/store/module.codefly.yaml": "kind: module\nname: store\n",
"modules/store/configurations/local/" + fixturefragments.Group + ".yaml": head + "roles: [{organization: acme, name: viewer, scopes: [{kind: documents, actions: [read]}]}]\n",
} {
path := filepath.Join(dir, name)
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
}
cmd := exec.Command("git", "init", "-q")
cmd.Dir = dir
out, err := cmd.CombinedOutput()
require.NoErrorf(t, err, "git init: %s", out)
ctx := context.Background()
workspace, err := resources.LoadWorkspaceFromDir(ctx, dir)
require.NoError(t, err)
env := &environments.Environment{Name: "local"}
target := filepath.Join(dir, "configurations", "local", fixturefragments.Group+".yaml")

handWritten := head + "# the playground's own copy\n"
require.NoError(t, os.MkdirAll(filepath.Dir(target), 0o755))
require.NoError(t, os.WriteFile(target, []byte(handWritten), 0o600))
require.NoError(t, provisionFixtureComposition(ctx, workspace, env))
kept, err := os.ReadFile(target)
require.NoError(t, err)
require.Equal(t, handWritten, string(kept))

require.NoError(t, os.Remove(target))
require.NoError(t, provisionFixtureComposition(ctx, workspace, env))
written, err := os.ReadFile(target)
require.NoError(t, err)
require.True(t, strings.HasPrefix(string(written), "# Generated by codefly"), string(written))
require.Contains(t, string(written), "kind: documents")
}
6 changes: 6 additions & 0 deletions cmd/run/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,12 @@ func newRunFlow(ctx context.Context, workspace *resources.Workspace, module *res
return nil, w.Wrap(err)
}

// The local authority's fixture-composition document is merged from the
// composed modules' fragments before anything reads the configuration.
if fixtureErr := provisionFixtureComposition(ctx, workspace, env); fixtureErr != nil {
return nil, w.Wrap(fixtureErr)
}

// Parse remote services
remoteServices, err := parseRemote(workspace, remotes)
if err != nil {
Expand Down
38 changes: 38 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2731,6 +2731,7 @@ Subcommands:

- [`codefly generate client`](#codefly-generate-client)
- [`codefly generate contracts`](#codefly-generate-contracts)
- [`codefly generate fixture-composition`](#codefly-generate-fixture-composition)
- [`codefly generate permissions`](#codefly-generate-permissions)
- [`codefly generate proto`](#codefly-generate-proto)
- [`codefly generate runnable-bindings`](#codefly-generate-runnable-bindings)
Expand Down Expand Up @@ -2816,6 +2817,43 @@ Flags:
--permissions also generate typed permission constants; with --check, verify their freshness
```

## `codefly generate fixture-composition`

Merge every composed module's fixture-composition fragment into the workspace's document

```
Merge the fixture-composition fragment every module of the workspace ships
(configurations/<profile>/fixture-composition.yaml in the module's own directory)
into the workspace's configurations/<profile>/fixture-composition.yaml, the document
the local authority reads.

Each module states, once, what the local authority's standard fixture roles may
do at its kinds; the composition's own module states only what is its own (a
collection its seed writes). Every grant of a role on one kind, every
installation and every stand-in module has exactly one source: two fragments
stating the same one are refused, naming both. The rules are the authority's
(codefly-dev/authority libraries/fixture-composition).

`codefly run` writes the same document before the local authority boots, so a
developer never runs this first. The written file says it is generated in its
first line; a hand-written document is never overwritten, and this command
refuses it by name.

Examples:
codefly generate fixture-composition
codefly generate fixture-composition --env local
```

```
codefly generate fixture-composition [flags]
```

Flags:

```
--env string environment whose configuration profile the document is written for (default "local")
```

## `codefly generate permissions`

Generate typed Go and TypeScript permissions from published contracts
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261026143932-67b85b14a942
github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-20261026143932-67b85b14a942
github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261026023425-000d44495d93
github.com/codefly-dev/authority/libraries/fixture-composition/go v0.0.0-20261027171336-5178f01696e3
github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261026175541-3afad7224855
github.com/codefly-dev/core v0.19.7-0.20261026194550-a8599bd3991a
github.com/codefly-dev/golor v0.1.3
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-202610261439
github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-20261026143932-67b85b14a942/go.mod h1:n+JVNRyv2dp7BIwUoKYn3TcjN7YpZmxMhBrWp14wsKQ=
github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261026023425-000d44495d93 h1:PnhgA9wkib67bJnc5hkhkc80J2ch5uYiq+9/NiD/XFU=
github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261026023425-000d44495d93/go.mod h1:XsOFnO2MQdr37vM3ULa0Uwi8ZWptyVx7hRSUyIdaZ1Q=
github.com/codefly-dev/authority/libraries/fixture-composition/go v0.0.0-20261027171336-5178f01696e3 h1:UGR5uQAPBO9YtTFCDWEfXeTgKBYU8vwSFaqza3gT9+U=
github.com/codefly-dev/authority/libraries/fixture-composition/go v0.0.0-20261027171336-5178f01696e3/go.mod h1:RxoEMLDVDjAdTwHA7cDVQ9CwYGEuLjTMsf8HdPaHrho=
github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261026175541-3afad7224855 h1:XmPltOHirB97RopD8WIJe9bwt1wkHAzngKoj8k2XI8U=
github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261026175541-3afad7224855/go.mod h1:rfn41uiORQ36RrOeFEjXHPLC9NUi0j14AoYJtcovsn4=
github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261026023425-000d44495d93 h1:4EygH7/J0E6aM7hUPdbQGfit1rsLUNCtWJ8kMh2b9PE=
Expand Down
Loading
Loading