Repository: skupperproject/skupper (go)

Issue:
V2 move nonkube platform file to internal path
**Is your feature request related to a problem? Please describe.**
No

**Describe the solution you'd like**
Non-kube sites create a platform.yaml file under `runtime/`, as this is currently
just an internal detail, move it to ìnternal/` directory.

**Additional context**
At this point the platform is just used internally, so it is safer to keep it as an internal
implementation detail for now.


Pull request description:
Moved platform file to internal path
* The platform.yaml file has been moved from runtime/ to internal/
* Former PlatformInfo type used in V1 by the switch command has been removed

Fixes #1879

Patch:
diff --git a/cmd/bootstrap/remove.sh b/cmd/bootstrap/remove.sh
index f40e9ef7..3a4599ee 100755
--- a/cmd/bootstrap/remove.sh
+++ b/cmd/bootstrap/remove.sh
@@ -26,7 +26,7 @@ usage() {
 }
 
 remove_definition() {
-    platform_file="${namespaces_path}/${namespace}/runtime/platform.yaml"
+    platform_file="${namespaces_path}/${namespace}/internal/platform.yaml"
     if [ -f "${platform_file}" ]; then
         SKUPPER_PLATFORM=$(grep '^platform: ' "${platform_file}" | sed -e 's/.*: //g')
         if [ "${SKUPPER_PLATFORM}" = "podman" ] || [ "${SKUPPER_PLATFORM}" = "docker" ]; then
diff --git a/internal/config/platform.go b/internal/config/platform.go
index aa005015..bfd1e0e6 100644
--- a/internal/config/platform.go
+++ b/internal/config/platform.go
@@ -1,133 +1,12 @@
 package config
 
 import (
-	"bytes"
-	"fmt"
-	"io"
 	"os"
-	"path"
-	"path/filepath"
 	"slices"
 	"strings"
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/utils"
-	"gopkg.in/yaml.v3"
-)
-
-type PlatformInfo struct {
-	Current  types.Platform `yaml:"current"`
-	Previous types.Platform `yaml:"previous"`
-}
-
-func (p *PlatformInfo) Update(platform types.Platform) error {
-	if err := p.Load(); err != nil {
-		return err
-	}
-	if p.Current == "" {
-		p.Current = platform
-	}
-	p.Previous = p.Current
-	p.Current = platform
-
-	// Creating the base dir
-	baseDir := filepath.Dir(PlatformConfigFile)
-	if _, err := os.Stat(baseDir); err != nil {
-		if err = os.MkdirAll(baseDir, 0755); err != nil {
-			return fmt.Errorf("unable to create base directory %s - %q", baseDir, err)
-		}
-	}
-	f, err := os.Create(PlatformConfigFile)
-	if err != nil {
-		return fmt.Errorf("error creating file %s: %v", PlatformConfigFile, err)
-	}
-	defer f.Close()
-	e := yaml.NewEncoder(f)
-	if err = e.Encode(p); err != nil {
-		return fmt.Errorf("error saving file: %s: %v", PlatformConfigFile, err)
-	}
-	return nil
-}
-
-func (p *PlatformInfo) Load() error {
-	data, err := os.ReadFile(PlatformConfigFile)
-	if err != nil && !os.IsNotExist(err) {
-		return fmt.Errorf("error loading %s: %v", PlatformConfigFile, err)
-	}
-	if data != nil {
-		decoder := yaml.NewDecoder(bytes.NewReader(data))
-		if err = decoder.Decode(p); err != nil && err != io.EOF {
-			return fmt.Errorf("error decoding %s: %v", PlatformConfigFile, err)
-		}
-	}
-	return nil
-}
-
-type ConfigFileHandler interface {
-	GetFilename() string
-	SetFileName(name string)
-	Save() error
-	Load() error
-	GetData() interface{}
-	SetData(data interface{})
-}
-
-type ConfigFileHandlerCommon struct {
-	filename string
-	data     interface{}
-}
-
-func (l *ConfigFileHandlerCommon) GetFilename() string {
-	return l.filename
-}
-
-func (l *ConfigFileHandlerCommon) SetFileName(name string) {
-	l.filename = name
-}
-
-func (l *ConfigFileHandlerCommon) Save() error {
-	baseDir := filepath.Dir(l.GetFilename())
-	if _, err := os.Stat(baseDir); err != nil {
-		if err = os.MkdirAll(baseDir, 0755); err != nil {
-			return fmt.Errorf("unable to create base directory %s - %q", baseDir, err)
-		}
-	}
-	f, err := os.Create(l.GetFilename())
-	if err != nil {
-		return fmt.Errorf("error creating file %s: %v", l.GetFilename(), err)
-	}
-	defer f.Close()
-	e := yaml.NewEncoder(f)
-	if err = e.Encode(l.GetData()); err != nil {
-		return fmt.Errorf("error saving file: %s: %v", l.GetFilename(), err)
-	}
-	return nil
-}
-
-func (l *ConfigFileHandlerCommon) Load() error {
-	data, err := os.ReadFile(l.GetFilename())
-	if err != nil && !os.IsNotExist(err) {
-		return fmt.Errorf("error loading %s: %v", l.GetFilename(), err)
-	}
-	if data != nil {
-		decoder := yaml.NewDecoder(bytes.NewReader(data))
-		if err = decoder.Decode(l.data); err != nil && err != io.EOF {
-			return fmt.Errorf("error decoding %s: %v", l.GetFilename(), err)
-		}
-	}
-	return nil
-}
-
-func (l *ConfigFileHandlerCommon) GetData() interface{} {
-	return l.data
-}
-
-func (l *ConfigFileHandlerCommon) SetData(data interface{}) {
-	l.data = data
-}
-
-var (
-	PlatformConfigFile = path.Join(getDataHome(), "platform.yaml")
 )
 
 var (
@@ -143,9 +22,7 @@ var (
 // In case the defined platform is invalid, "kubernetes"
 // will be returned.
 func GetPlatform() types.Platform {
-	p := &PlatformInfo{}
 	var platform types.Platform
-	_ = p.Load()
 	for i, arg := range os.Args {
 		if slices.Contains([]string{"--platform", "-p"}, arg) && i+1 < len(os.Args) {
 			platformArg := os.Args[i+1]
@@ -160,7 +37,6 @@ func GetPlatform() types.Platform {
 	if platform == "" {
 		platform = types.Platform(utils.DefaultStr(Platform,
 			os.Getenv(types.ENV_PLATFORM),
-			string(p.Current),
 			string(types.PlatformKubernetes)))
 	}
 	switch platform {
@@ -174,15 +50,3 @@ func GetPlatform() types.Platform {
 		return types.PlatformKubernetes
 	}
 }
-
-func getDataHome() string {
-	if os.Getuid() == 0 {
-		return "/var/lib/skupper"
-	}
-	dataHome, ok := os.LookupEnv("XDG_DATA_HOME")
-	if !ok {
-		homeDir, _ := os.UserHomeDir()
-		dataHome = homeDir + "/.local/share"
-	}
-	return path.Join(dataHome, "skupper")
-}
diff --git a/internal/nonkube/bundle/install.sh.template b/internal/nonkube/bundle/install.sh.template
index ca36e1de..6f495fe1 100644
--- a/internal/nonkube/bundle/install.sh.template
+++ b/internal/nonkube/bundle/install.sh.template
@@ -38,7 +38,7 @@ if [ "${UID}" -eq 0 ]; then
     export USERNS="''"
 fi
 export NAMESPACES_PATH="${SKUPPER_OUTPUT_PATH}/namespaces"
-export PLATFORM_FILE="${NAMESPACES_PATH}/${NAMESPACE}/runtime/platform.yaml"
+export PLATFORM_FILE="${NAMESPACES_PATH}/${NAMESPACE}/internal/platform.yaml"
 export USER="${USER:-$(id -un)}"
 SITE_ID="$(hostname -s)-${USER}-$(date +%s)"
 export SITE_ID
@@ -79,7 +79,7 @@ parse_opts() {
                 if [ -n "${OPTARG}" ]; then
                     export NAMESPACE="${OPTARG}"
                 fi
-                export PLATFORM_FILE="${NAMESPACES_PATH}/${NAMESPACE}/runtime/platform.yaml"
+                export PLATFORM_FILE="${NAMESPACES_PATH}/${NAMESPACE}/internal/platform.yaml"
                 ;;
             d)
                 export DUMP_TOKENS=true
diff --git a/pkg/nonkube/api/environment.go b/pkg/nonkube/api/environment.go
index 0b1fa705..790f80c1 100644
--- a/pkg/nonkube/api/environment.go
+++ b/pkg/nonkube/api/environment.go
@@ -21,6 +21,7 @@ const (
 	RuntimePath           InternalPath = "runtime"
 	RuntimeSiteStatePath  InternalPath = "runtime/resources"
 	RuntimeTokenPath      InternalPath = "runtime/links"
+	InternalBasePath      InternalPath = "internal"
 	LoadedSiteStatePath   InternalPath = "internal/snapshot"
 	ScriptsPath           InternalPath = "internal/scripts"
 )
diff --git a/pkg/nonkube/common/fs_config_renderer.go b/pkg/nonkube/common/fs_config_renderer.go
index 2163ae62..3990cd22 100644
--- a/pkg/nonkube/common/fs_config_renderer.go
+++ b/pkg/nonkube/common/fs_config_renderer.go
@@ -121,7 +121,7 @@ func (c *FileSystemConfigurationRenderer) Render(siteState *api.SiteState) error
 	// Saving runtime platform
 	if !c.Bundle {
 		content := fmt.Sprintf("platform: %s\n", c.Platform)
-		platformPath := path.Join(outputPath, string(api.RuntimePath), "platform.yaml")
+		platformPath := path.Join(outputPath, string(api.InternalBasePath), "platform.yaml")
 		logger.Debug("writing platform", slog.String("platform", c.Platform), slog.String("path", platformPath))
 		err = os.WriteFile(platformPath, []byte(content), 0644)
 		if err != nil {
diff --git a/pkg/nonkube/common/platform.go b/pkg/nonkube/common/platform.go
index 778f40b4..a4ba6a86 100644
--- a/pkg/nonkube/common/platform.go
+++ b/pkg/nonkube/common/platform.go
@@ -26,8 +26,8 @@ func (s *NamespacePlatformLoader) Load(namespace string) (string, error) {
 		namespace = "default"
 	}
 	pathProvider := s.GetPathProvider()
-	runtimePath := pathProvider(namespace, api.RuntimePath)
-	platformFile, err := os.ReadFile(path.Join(runtimePath, "platform.yaml"))
+	internalPath := pathProvider(namespace, api.InternalBasePath)
+	platformFile, err := os.ReadFile(path.Join(internalPath, "platform.yaml"))
 	if err != nil {
 		return "", fmt.Errorf("failed to read platform config file for namespace %s: %w", namespace, err)
 	}


Test patch:
diff --git a/pkg/nonkube/api/environment_test.go b/pkg/nonkube/api/environment_test.go
index 544ed250..9e8c3efb 100644
--- a/pkg/nonkube/api/environment_test.go
+++ b/pkg/nonkube/api/environment_test.go
@@ -1,17 +1,13 @@
 package api
 
 import (
-	"bytes"
-	"fmt"
 	"os"
 	"path"
-	"reflect"
 	"testing"
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/internal/config"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gopkg.in/yaml.v3"
 	"gotest.tools/v3/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
@@ -110,84 +106,51 @@ func TestGetPlatform(t *testing.T) {
 		want   types.Platform
 		cliVar string
 		envVar string
-		cfgVal types.Platform
 	}{
 		{
 			name:   "default-as-nothing-set",
 			want:   "kubernetes",
 			cliVar: "",
 			envVar: "",
-			cfgVal: "",
 		},
 		{
 			name:   "podman-as-cli-flag-set",
 			want:   "podman",
 			cliVar: "podman",
 			envVar: "",
-			cfgVal: "",
 		},
 		{
 			name:   "podman-as-cli-flag-set-highest-precedence",
 			want:   "podman",
 			cliVar: "podman",
 			envVar: "kubernetes",
-			cfgVal: types.PlatformKubernetes,
 		},
 		{
 			name:   "podman-as-envvar-set",
 			want:   "podman",
 			cliVar: "",
 			envVar: "podman",
-			cfgVal: "",
 		},
 		{
 			name:   "podman-as-envvar-set-over-config",
 			want:   "podman",
 			cliVar: "",
 			envVar: "podman",
-			cfgVal: types.PlatformKubernetes,
-		},
-		{
-			name:   "podman-as-config-set",
-			want:   "podman",
-			cliVar: "",
-			envVar: "",
-			cfgVal: types.PlatformPodman,
 		},
 	}
 	// Saving original values
 	cliOrig := config.Platform
 	envOrig := os.Getenv(types.ENV_PLATFORM)
-	cfgFileOrig := config.PlatformConfigFile
 	// Restore original values
 	defer func() {
 		config.Platform = cliOrig
 		t.Setenv(types.ENV_PLATFORM, envOrig)
-		config.PlatformConfigFile = cfgFileOrig
 	}()
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			config.Platform = tt.cliVar
 			t.Setenv(types.ENV_PLATFORM, tt.envVar)
-
-			// creating a temporary platform file
-			f, err := os.CreateTemp(os.TempDir(), "platform-*.yaml")
-			_ = f.Close()
-			assert.Assert(t, err, "unable to create temporary platform file")
-			config.PlatformConfigFile = f.Name()
-			if tt.cfgVal == "" {
-				_ = os.Remove(f.Name())
-			} else {
-				info := &config.PlatformInfo{}
-				assert.Assert(t, info.Update(tt.cfgVal), "error setting platform in %s", f.Name())
-			}
-
 			got := config.GetPlatform()
-
-			// removing temporary file
-			if tt.cfgVal != "" {
-				_ = os.Remove(f.Name())
-			}
 			if got != tt.want {
 				t.Errorf("GetPlatform() = %v, want %v", got, tt.want)
 			}
@@ -195,188 +158,6 @@ func TestGetPlatform(t *testing.T) {
 	}
 }
 
-func TestPlatformInfo_Load(t *testing.T) {
-	tests := []struct {
-		name          string
-		existing      *config.PlatformInfo
-		want          *config.PlatformInfo
-		wantReadErr   bool
-		wantDecodeErr bool
-	}{
-		{
-			name:     "default-no-config-file",
-			existing: nil,
-			want:     &config.PlatformInfo{},
-		},
-		{
-			name:        "error-no-permission",
-			existing:    nil,
-			want:        &config.PlatformInfo{},
-			wantReadErr: true,
-		},
-		{
-			name:          "error-invalid-content",
-			existing:      &config.PlatformInfo{},
-			want:          &config.PlatformInfo{},
-			wantDecodeErr: true,
-		},
-		{
-			name: "valid-config-file",
-			existing: &config.PlatformInfo{
-				Current:  types.PlatformPodman,
-				Previous: types.PlatformKubernetes,
-			},
-			want: &config.PlatformInfo{
-				Current:  types.PlatformPodman,
-				Previous: types.PlatformKubernetes,
-			},
-		},
-	}
-	// Saving original values
-	cfgFileOrig := config.PlatformConfigFile
-	// Restore original values
-	defer func() {
-		config.PlatformConfigFile = cfgFileOrig
-	}()
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			f, err := os.CreateTemp(os.TempDir(), "platform-*.yaml")
-			assert.Assert(t, err, "error creating temporary platform config file")
-			tempFile := f.Name()
-			defer func(name string) {
-				_ = os.Remove(name)
-			}(tempFile)
-			config.PlatformConfigFile = tempFile
-			if !tt.wantReadErr {
-				_ = os.Remove(tempFile)
-			} else {
-				_ = os.Chmod(tempFile, 0)
-			}
-			if tt.existing == nil {
-				tt.existing = &config.PlatformInfo{}
-			} else if !tt.wantReadErr {
-				// populating temp file
-				info := tt.existing
-				var data string
-				if !tt.wantDecodeErr {
-					data = fmt.Sprintf("current: %s\nprevious: %s\n", info.Current, info.Previous)
-				} else {
-					data = `invalid content`
-				}
-				assert.Assert(t, os.WriteFile(tempFile, []byte(data), 0644), "error writing to temporary file")
-			}
-			wantErr := tt.wantReadErr || tt.wantDecodeErr
-			if err := tt.existing.Load(); (err != nil) != wantErr {
-				t.Errorf("Load() error = %v, wantReadErr %v", err, tt.wantReadErr)
-			}
-			assert.Assert(t, reflect.DeepEqual(tt.existing, tt.want), "want = %v, got = %v", tt.want, tt.existing)
-		})
-	}
-}
-
-func TestPlatformInfo_Update(t *testing.T) {
-	tests := []struct {
-		name         string
-		existing     *config.PlatformInfo
-		platform     types.Platform
-		want         *config.PlatformInfo
-		wantWriteErr bool
-		wantReadErr  bool
-	}{
-		{
-			name:     "new-config-file",
-			existing: nil,
-			platform: types.PlatformPodman,
-			want: &config.PlatformInfo{
-				Current:  types.PlatformPodman,
-				Previous: types.PlatformPodman,
-			},
-		},
-		{
-			name: "config-file-changed",
-			existing: &config.PlatformInfo{
-				Current:  types.PlatformKubernetes,
-				Previous: types.PlatformKubernetes,
-			},
-			platform: types.PlatformPodman,
-			want: &config.PlatformInfo{
-				Current:  types.PlatformPodman,
-				Previous: types.PlatformKubernetes,
-			},
-		},
-		{
-			name: "error-no-write-permission",
-			existing: &config.PlatformInfo{
-				Current:  types.PlatformKubernetes,
-				Previous: types.PlatformKubernetes,
-			},
-			platform: types.PlatformPodman,
-			want: &config.PlatformInfo{
-				Current:  types.PlatformKubernetes,
-				Previous: types.PlatformKubernetes,
-			},
-			wantWriteErr: true,
-		},
-		{
-			name: "error-no-read-permission",
-			existing: &config.PlatformInfo{
-				Current:  types.PlatformKubernetes,
-				Previous: types.PlatformKubernetes,
-			},
-			platform:    types.PlatformPodman,
-			want:        &config.PlatformInfo{},
-			wantReadErr: true,
-		},
-	}
-	// Saving original values
-	cfgFileOrig := config.PlatformConfigFile
-	// Restore original values
-	defer func() {
-		config.PlatformConfigFile = cfgFileOrig
-	}()
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			tempDir := os.TempDir()
-			f, err := os.CreateTemp(tempDir, "platform-*.yaml")
-			assert.Assert(t, err, "error creating temporary platform config file")
-			_ = f.Close()
-			tempFile := f.Name()
-			defer func(name string) {
-				_ = os.Remove(name)
-			}(tempFile)
-			config.PlatformConfigFile = tempFile
-			if tt.existing == nil {
-				_ = os.Remove(tempFile)
-				tt.existing = &config.PlatformInfo{}
-			} else {
-				data := fmt.Sprintf("current: %s\nprevious: %s\n", tt.existing.Current, tt.existing.Previous)
-				assert.Assert(t, os.WriteFile(tempFile, []byte(data), 0644), "error preparing platform config file")
-			}
-
-			if tt.wantWriteErr {
-				_ = os.Chmod(tempFile, 0444)
-			} else if tt.wantReadErr {
-				_ = os.Chmod(tempFile, 0)
-			}
-
-			wantErr := tt.wantWriteErr || tt.wantReadErr
-			if err := tt.existing.Update(tt.platform); (err != nil) != wantErr {
-				t.Errorf("Update() error = %v, wantErr %v", err, wantErr)
-			}
-
-			// loading file
-			if tt.want != nil {
-				data, _ := os.ReadFile(tempFile)
-				decoder := yaml.NewDecoder(bytes.NewReader(data))
-				current := &config.PlatformInfo{}
-				_ = decoder.Decode(current)
-				assert.Assert(t, reflect.DeepEqual(tt.want, current), "want = %v, got = %v", tt.want, current)
-			}
-		})
-	}
-}
-
 func TestGetHostSiteHome(t *testing.T) {
 	fakeSite := &v2alpha1.Site{
 		ObjectMeta: metav1.ObjectMeta{
diff --git a/pkg/nonkube/common/fs_config_renderer_test.go b/pkg/nonkube/common/fs_config_renderer_test.go
index 62ab7906..3379e2a6 100644
--- a/pkg/nonkube/common/fs_config_renderer_test.go
+++ b/pkg/nonkube/common/fs_config_renderer_test.go
@@ -73,7 +73,7 @@ func testFileSystemConfigurationRendererRender(t *testing.T, addInputCertificate
 		string(api.CertificatesPath) + "/link-one-profile/ca.crt",
 		string(api.CertificatesPath) + "/link-one-profile/tls.crt",
 		string(api.CertificatesPath) + "/link-one-profile/tls.key",
-		string(api.RuntimePath) + "/platform.yaml",
+		string(api.InternalBasePath) + "/platform.yaml",
 		string(api.RuntimeTokenPath) + "/link-link-access-one-127.0.0.1.yaml",
 	}
 	if !addInputCertificates {
diff --git a/pkg/nonkube/common/platform_test.go b/pkg/nonkube/common/platform_test.go
new file mode 100644
index 00000000..0a056c9f
--- /dev/null
+++ b/pkg/nonkube/common/platform_test.go
@@ -0,0 +1,84 @@
+package common
+
+import (
+	"fmt"
+	"os"
+	"path"
+	"testing"
+
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"gotest.tools/v3/assert"
+)
+
+func TestNamespacePlatformLoader_GetPathProvider(t *testing.T) {
+	nsPlatformLoader := &NamespacePlatformLoader{}
+	defaultPathProvider := nsPlatformLoader.GetPathProvider()
+	assert.Equal(t, defaultPathProvider("", api.RuntimePath), api.GetInternalOutputPath("", api.RuntimePath))
+	customProvider := createCustomPathProvider(t)
+	nsPlatformLoader.PathProvider = customProvider
+	assert.Equal(t, nsPlatformLoader.GetPathProvider()("", api.RuntimePath), customProvider("", api.RuntimePath))
+}
+
+func TestNamespacePlatformLoader_Load(t *testing.T) {
+	for _, tc := range []struct {
+		name                string
+		namespace           string
+		noPlatformFile      bool
+		invalidPlatformFile bool
+		errorPrefix         string
+		expectedPlatform    string
+	}{
+		{
+			name:             "empty-namespace-default-platform",
+			expectedPlatform: "kubernetes",
+		},
+		{
+			name:             "sample-namespace-podman-platform",
+			namespace:        "sample",
+			expectedPlatform: "podman",
+		},
+		{
+			name:           "empty-namespace-no-platform-file",
+			noPlatformFile: true,
+			errorPrefix:    "failed to read platform config file for namespace default: ",
+		},
+		{
+			name:                "empty-namespace-invalid-platform-file",
+			invalidPlatformFile: true,
+			errorPrefix:         "failed to unmarshal platform config file for namespace default: ",
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			pathProvider := createCustomPathProvider(t)
+			basePath := pathProvider(tc.namespace, api.InternalBasePath)
+			assert.Assert(t, os.MkdirAll(basePath, 0755))
+			nsPlatformLoader := &NamespacePlatformLoader{
+				PathProvider: pathProvider,
+			}
+			if !tc.noPlatformFile {
+				data := fmt.Sprintf("platform: %s", tc.expectedPlatform)
+				if tc.invalidPlatformFile {
+					data = "bad-data"
+				}
+				assert.Assert(t, os.WriteFile(path.Join(basePath, "platform.yaml"), []byte(data), 0644), "error creating fake platform.yaml")
+			}
+			platform, err := nsPlatformLoader.Load(tc.namespace)
+			if tc.noPlatformFile || tc.invalidPlatformFile {
+				assert.ErrorContains(t, err, tc.errorPrefix)
+			} else {
+				assert.NilError(t, err)
+				assert.Equal(t, platform, tc.expectedPlatform)
+			}
+		})
+	}
+}
+
+func createCustomPathProvider(t *testing.T) api.InternalPathProvider {
+	baseDir := t.TempDir()
+	return func(namespace string, internalPath api.InternalPath) string {
+		if namespace == "" {
+			namespace = "default"
+		}
+		return path.Join(baseDir, namespace, string(internalPath))
+	}
+}