Repository: skupperproject/skupper
Pull request #1893 (2025-01-17 19:16:46)

Fix Unspecified Vanflow SITE Attributes
Previously the vanflow SITE record was using an unspecified attributes to denote the site version and policy status. This breaking change moves the SITE Version attribute to the specified buildVersion attribute (code point 32) in the spec and removes the now-deprecated Policy attribute. Versions running the old unspecified version attribute (2.0.0-previews 1 and 2) will not observe the site version from sites with the corrected attribute and vice versa. The Policy attribute has been unused since 1.x.

c-kruse: Fixes https://github.com/skupperproject/skupper/issues/1891

This is an unfortunate breaking change, but the consequences should be pretty benign. The network-observer API will label incompatible sites as version "unknown", and skupper site status will have a blank (`-`) version for incompatible sites.

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/kube/flow/status.go b/pkg/kube/flow/status.go
index fc1a86fb..3cfb80da 100644
--- a/pkg/kube/flow/status.go
+++ b/pkg/kube/flow/status.go
@@ -514,7 +514,6 @@ func asSiteInfo(site vanflow.SiteRecord) network.SiteInfo {
 		Namespace: dref(site.Namespace),
 		Platform:  dref(site.Platform),
 		Version:   dref(site.Version),
-		Policy:    dref(site.Policy),
 	}
 }
 
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)
 	}
diff --git a/pkg/vanflow/records.go b/pkg/vanflow/records.go
index e0f33188..9f88017e 100644
--- a/pkg/vanflow/records.go
+++ b/pkg/vanflow/records.go
@@ -32,8 +32,7 @@ type SiteRecord struct {
 	Platform  *string `vflow:"11"`
 	Namespace *string `vflow:"12"`
 	Name      *string `vflow:"30"` //unspeced
-	Version   *string `vflow:"52"`
-	Policy    *string `vflow:"53"`
+	Version   *string `vflow:"32"`
 }
 
 func (r SiteRecord) GetTypeMeta() TypeMeta {


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))
+	}
+}