Skip to content
Open
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
51 changes: 51 additions & 0 deletions resources/out_of_repo_module_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
package resources_test

import (
"context"
"testing"

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

// A composition-root workspace references an out-of-repo module by relative
// path (a sibling checkout, not a vendored copy). Its services must load into
// the graph and wire across the reference boundary like in-repo modules.
func TestWorkspaceReferencesOutOfRepoModuleByPath(t *testing.T) {
ctx := context.Background()
workspace, err := resources.LoadWorkspaceFromDir(ctx, "testdata/out-of-repo/solution")
require.NoError(t, err)

// Portability is the whole point: the committed reference is a relative
// sibling path, not an absolute machine path. ModulePath joins it onto the
// workspace dir, so it resolves the same regardless of invocation cwd.
var saasRef *resources.ModuleReference
for _, ref := range workspace.Modules {
if ref.Name == "saas" {
saasRef = ref
}
}
require.NotNil(t, saasRef)
require.NotNil(t, saasRef.PathOverride)
require.Equal(t, "../host", *saasRef.PathOverride)

saas, err := workspace.LoadModuleFromName(ctx, "saas")
require.NoError(t, err)
gateway, err := saas.LoadServiceFromName(ctx, "gateway")
require.NoError(t, err)
require.Len(t, gateway.Endpoints, 1)
require.Equal(t, "public-api", gateway.Endpoints[0].Name)

services, err := workspace.LoadServices(ctx)
require.NoError(t, err)
var names []string
for _, svc := range services {
names = append(names, svc.Name)
}
require.Contains(t, names, "gateway")
require.Contains(t, names, "api")

// The in-repo consumer depends on the out-of-repo producer's public
// endpoint; visibility wiring resolves across the reference boundary.
require.NoError(t, workspace.ValidateServiceDependencies(ctx))
}
19 changes: 18 additions & 1 deletion resources/path_validation.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,14 +47,31 @@ func validateResourcePathOverride(kind string, override *string) error {
return validateResourceRelativePath(kind+" override", *override)
}

// validateModuleReferencePathOverride governs where a workspace module
// reference resolves. Unlike service/job overrides — which stay confined to
// their owning resource — a module reference is a composition root: it may
// point at an out-of-repo module (a sibling checkout next to the workspace) so
// a solution can boot a host it references without vendoring a copy. Absolute
// and upward-traversing relative paths are both allowed; NUL and backslash
// stay rejected as cross-platform hazards.
func validateModuleReferencePathOverride(override *string) error {
if override == nil {
return nil
}
if strings.ContainsAny(*override, "\x00\\") {
return fmt.Errorf("module path override %q must not contain NUL or backslash", *override)
}
return nil
}

func validateModuleReferencePath(ref *ModuleReference) error {
if ref == nil {
return fmt.Errorf("module reference cannot be nil")
}
if err := validateResourcePathComponent("module", ref.Name); err != nil {
return err
}
return validateResourcePathOverride("module", ref.PathOverride)
return validateModuleReferencePathOverride(ref.PathOverride)
}

func validateServiceReferencePath(ref *ServiceReference) error {
Expand Down
18 changes: 18 additions & 0 deletions resources/resource_path_boundaries_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,3 +104,21 @@ func TestAbsoluteResourceOverridesRemainSupported(t *testing.T) {
t.Fatalf("absolute override rejected: %v", err)
}
}

func TestModuleReferenceOverrideAllowsOutOfRepoPath(t *testing.T) {
up := "../host"
if err := validateModuleReferencePath(&ModuleReference{Name: "saas", PathOverride: &up}); err != nil {
t.Fatalf("out-of-repo module reference rejected: %v", err)
}
for _, bad := range []string{"../ho\x00st", "..\\host"} {
override := bad
if err := validateModuleReferencePath(&ModuleReference{Name: "saas", PathOverride: &override}); err == nil {
t.Fatalf("module reference override %q was accepted", bad)
}
}
// The out-of-repo escape hatch is for module references only; service
// overrides stay confined to their owning module.
if err := validateServiceReferencePath(&ServiceReference{Name: "gateway", PathOverride: &up}); err == nil {
t.Fatal("traversing service reference override was accepted")
}
}
4 changes: 4 additions & 0 deletions resources/testdata/out-of-repo/host/module.codefly.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
kind: module
name: saas
services:
- name: gateway
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
kind: service
name: gateway
version: 0.0.0
agent:
kind: runtime::service
name: go-grpc
version: 0.0.1
publisher: codefly.ai
endpoints:
- name: public-api
visibility: public
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
kind: module
name: platform
services:
- name: api
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
kind: service
name: api
version: 0.0.0
agent:
kind: runtime::service
name: go-grpc
version: 0.0.1
publisher: codefly.ai
service-dependencies:
- name: gateway
module: saas
endpoints:
- name: public-api
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
name: solution
layout: modules
modules:
- name: platform
- name: saas
path: ../host
Loading