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
12 changes: 10 additions & 2 deletions cmd/run/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ var WorkspaceCmd = &cobra.Command{
// The host module is the one exception: its ingress services are the roots,
// and the rest of it is reached through their dependencies, as a person would
// reach it.
//
// A service the environment withdraws (exclude-services) is no root: the
// dependency graph is built without it, so a root naming it would be a sort
// target the graph does not hold. A module the environment withdraws whole
// contributes nothing, its service-entry included — it is not composed there.
func CompositionRoots(ctx context.Context, workspace *resources.Workspace) ([]string, error) {
env, err := orchestration.SelectEnvironment(workspace, environmentName)
if err != nil {
Expand Down Expand Up @@ -114,10 +119,13 @@ func CompositionRoots(ctx context.Context, workspace *resources.Workspace) ([]st
if err != nil {
return nil, fmt.Errorf("%s: %w", ref.Name, err)
}
if mod.ServiceEntry != "" {
if env.WithdrawsModule(mod) {
continue
}
if mod.ServiceEntry != "" && !env.Excludes(mod.Name, mod.ServiceEntry) {
add(resources.ServiceUnique(mod.Name, mod.ServiceEntry))
}
for _, service := range mod.ServiceReferences {
for _, service := range env.ComposedServices(mod) {
add(resources.ServiceUnique(mod.Name, service.Name))
}
}
Expand Down
98 changes: 98 additions & 0 deletions cmd/run/workspace_environment_exclusions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package run

import (
"fmt"
"os"
"path/filepath"
"testing"

"github.com/codefly-dev/cli/pkg/environments"
"github.com/codefly-dev/cli/pkg/orchestration"
"github.com/codefly-dev/core/architecture"
"github.com/codefly-dev/core/resources"
"github.com/stretchr/testify/require"
)

// A composition root must survive the same selected-environment exclusions
// that orchestration applies when it builds the dependency graph.
func TestCompositionRootsRespectSelectedEnvironmentExclusions(t *testing.T) {
oldEnv, oldProfile, oldExcluded := environmentName, profile, excludeDependencies
t.Cleanup(func() { environmentName, profile, excludeDependencies = oldEnv, oldProfile, oldExcluded })
profile = "small"
excludeDependencies = []string{"host/admin", "worker/cli-only"}
root := t.TempDir()
workspaceYAML := `name: roots
layout: modules
modules:
- name: host
- name: worker
run-profiles:
small:
exclude-dependencies: [worker/profile-only]
environments:
- name: local
host:
coordinate: example/local/cell
component: host
domain: example
audience: host
trust_domain: cluster.example
delivery: host/web/rest
ingress:
- name: web
service: web
endpoint: rest
hosts: [web.example.test]
- name: admin
service: admin
endpoint: rest
hosts: [admin.example.test]
exclude-services:
- service: worker/witness
- name: staging
`
for module, names := range map[string][]string{
"host": {"web", "admin"},
"worker": {"api", "witness", "cli-only", "profile-only"},
} {
dir := filepath.Join(root, "modules", module)
require.NoError(t, os.MkdirAll(dir, 0o755))
doc := "kind: module\nname: " + module + "\nservices:\n"
for _, name := range names {
doc += " - name: " + name + "\n"
serviceDir := filepath.Join(dir, "services", name)
require.NoError(t, os.MkdirAll(serviceDir, 0o755))
service := fmt.Sprintf("kind: service\nname: %s\nmodule: %s\nversion: 0.0.0\nagent:\n kind: runtime::service\n name: go\n version: 0.0.1\n publisher: codefly.ai\nendpoints:\n - name: rest\n api: rest\n visibility: public\n exposure: none\n", name, module)
require.NoError(t, os.WriteFile(filepath.Join(serviceDir, "service.codefly.yaml"), []byte(service), 0o600))
}
require.NoError(t, os.WriteFile(filepath.Join(dir, "module.codefly.yaml"), []byte(doc), 0o600))
}
require.NoError(t, os.WriteFile(filepath.Join(root, "workspace.codefly.yaml"), []byte(workspaceYAML), 0o600))
workspace, err := resources.LoadWorkspaceFromDir(t.Context(), root)
require.NoError(t, err)
for _, tc := range []struct {
env string
want []string
}{
{"local", []string{"host/web", "worker/api"}},
{"staging", []string{"host/web", "worker/api", "worker/witness"}},
} {
t.Run(tc.env, func(t *testing.T) {
environmentName = tc.env
roots, err := CompositionRoots(t.Context(), workspace)
require.NoError(t, err)
env, err := orchestration.SelectEnvironment(workspace, tc.env)
require.NoError(t, err)
require.NoError(t, environments.ValidateEnvironmentExclusions(t.Context(), workspace, env))
graph, err := architecture.NewServiceDependencies(t.Context(), workspace, architecture.ExcludeServices(env.ExcludedServiceUniques()...))
require.NoError(t, err)
for _, root := range roots {
_, err = graph.OrderTo(t.Context(), root)
if err != nil {
t.Errorf("selected composition root %s cannot be ordered: %v", root, err)
}
}
require.Equal(t, tc.want, roots)
})
}
}
76 changes: 76 additions & 0 deletions cmd/run/workspace_roots_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package run

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

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

// excludingWorkspace lays down two modules and an environment that withdraws
// what exclusions says: shop (api, vault, cache) and auth (hop, local).
func excludingWorkspace(t *testing.T, exclusions string) *resources.Workspace {
t.Helper()
root := t.TempDir()
service := func(name, extra string) string {
return "kind: service\nname: " + name + "\nversion: 0.0.0\nagent:\n kind: runtime::service\n name: go-grpc\n version: 0.0.1\n publisher: codefly.ai\n" + extra
}
files := map[string]string{
resources.WorkspaceConfigurationName: `name: acme
layout: modules
modules:
- name: shop
- name: auth
environments:
- name: local
` + exclusions,
"modules/shop/module.codefly.yaml": "kind: module\nname: shop\nservice-entry: api\nservices:\n - name: api\n - name: vault\n - name: cache\n",
"modules/shop/services/api/service.codefly.yaml": service("api", "service-dependencies:\n - name: vault\n"),
"modules/shop/services/vault/service.codefly.yaml": service("vault", ""),
"modules/shop/services/cache/service.codefly.yaml": service("cache", ""),
"modules/auth/module.codefly.yaml": "kind: module\nname: auth\nservices:\n - name: hop\n - name: local\n",
"modules/auth/services/hop/service.codefly.yaml": service("hop", ""),
"modules/auth/services/local/service.codefly.yaml": service("local", ""),
}
for rel, content := range files {
full := filepath.Join(root, rel)
require.NoError(t, os.MkdirAll(filepath.Dir(full), 0o755))
require.NoError(t, os.WriteFile(full, []byte(content), 0o644))
}
workspace, err := resources.LoadWorkspaceFromDir(context.Background(), root)
require.NoError(t, err)
return workspace
}

func useEnvironmentName(t *testing.T, value string) {
t.Helper()
previous := environmentName
t.Cleanup(func() { environmentName = previous })
environmentName = value
}

// A service the environment withdraws is no root, and a module withdrawn whole
// contributes none: the graph is built without them, so a root naming one would
// be a sort target the graph does not hold ("cannot topologically sort to").
func TestCompositionRootsLeaveOutWhatTheEnvironmentWithdraws(t *testing.T) {
useEnvironmentName(t, "local")
workspace := excludingWorkspace(t, ` exclude-services:
- service: shop/vault
withdrawn-from: [shop/api]
- service: auth/hop
- service: auth/local
`)
roots, err := CompositionRoots(context.Background(), workspace)
require.NoError(t, err)
require.Equal(t, []string{"shop/api", "shop/cache"}, roots)

// Without exclusions every service of every module is a root.
useEnvironmentName(t, "local")
all := excludingWorkspace(t, "")
roots, err = CompositionRoots(context.Background(), all)
require.NoError(t, err)
require.Equal(t, []string{"shop/api", "shop/vault", "shop/cache", "auth/hop", "auth/local"}, roots)
}
5 changes: 4 additions & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ service` that do not name one root service — `--naming-scope`,
`--temporary-ports`, `--output-env`, `--runtime-context`, `--set` among them,
never `--stand-alone` or `--exclude-root`; `--exclude-dependency` and
`--exclude-workspace-configuration` leave a service or a configuration group
out. The deterministic port hash folds every endpoint into a few thousand
out. The selected environment's `exclude-services` also removes services from
the composition roots; a module withdrawn whole contributes no root, including
its service-entry. CLI and run-profile exclusions still apply on top.
The deterministic port hash folds every endpoint into a few thousand
buckets, so a `--naming-scope` can hash two endpoints to one port and abort
the run; `--temporary-ports` sidesteps the hash, under the ordering its
allocation needs — a workspace configuration reference to a service that has
Expand Down
Loading