diff --git a/runtime_environment_file.go b/runtime_environment_file.go index c91c3bb..4f06f30 100644 --- a/runtime_environment_file.go +++ b/runtime_environment_file.go @@ -7,6 +7,8 @@ import ( "io" "os" "strings" + + "github.com/codefly-dev/core/resources" ) const ( @@ -89,6 +91,9 @@ func validRuntimeEnvironmentName(name string) bool { if !strings.HasPrefix(name, "CODEFLY__") || len(name) == len("CODEFLY__") { return false } + if isConfigurationDocumentCarrier(name) { + return true + } for _, character := range name { switch { case character >= 'A' && character <= 'Z': @@ -100,3 +105,38 @@ func validRuntimeEnvironmentName(name string) bool { } return true } + +// configurationDocumentDigestLength is the length of the digest core appends +// to a document prefix, read from core's own key rather than recomputed: this +// package hashes nothing (scripts/allowed-imports.txt). +var configurationDocumentDigestLength = len(resources.ConfigurationDocumentKey("", "", "", false)) - len(resources.ConfigurationDocumentPrefix) + +// isConfigurationDocumentCarrier reports a structured configuration document's +// carrier exactly as core mints it (resources.ConfigurationDocumentKey): one of +// the two document prefixes and a lowercase hex SHA-256, nothing else. +// +// It is the one name outside [A-Z0-9_] an artifact carries. The digest is +// lowercase because core spells it so, and this SDK finds the document by +// calling that same function and looking the result up exactly +// (configurationDocument), as does every consumer built on an earlier core. +// So the grammar admits the name core mints rather than the name changing +// under its readers, and only that shape: a lowercase letter anywhere else is +// still refused. +func isConfigurationDocumentCarrier(name string) bool { + for _, prefix := range []string{resources.ConfigurationDocumentPrefix, resources.SecretConfigurationDocumentPrefix} { + digest, found := strings.CutPrefix(name, prefix) + if !found { + continue + } + if len(digest) != configurationDocumentDigestLength { + return false + } + for _, character := range digest { + if (character < '0' || character > '9') && (character < 'a' || character > 'f') { + return false + } + } + return true + } + return false +} diff --git a/runtime_environment_file_test.go b/runtime_environment_file_test.go index 949ac37..89700e6 100644 --- a/runtime_environment_file_test.go +++ b/runtime_environment_file_test.go @@ -1,10 +1,14 @@ package codefly_test import ( + "context" "os" "path/filepath" + "strings" "testing" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/codefly-dev/core/resources" codefly "github.com/codefly-dev/sdk-go" "github.com/stretchr/testify/require" ) @@ -49,6 +53,69 @@ func TestLoadRuntimeEnvironmentFileRejectsUnsafeFile(t *testing.T) { require.Error(t, codefly.LoadRuntimeEnvironmentFile(link)) } +// TestLoadRuntimeEnvironmentFileCarriesAConfigurationDocument: core names a +// structured configuration document's carrier with a lowercase hex digest +// (resources.ConfigurationDocumentKey), and an artifact exporting one — a +// composition's workspace fixture-composition document — was refused whole. +// The document's carrier now loads, and the SDK reads the document from it. +func TestLoadRuntimeEnvironmentFileCarriesAConfigurationDocument(t *testing.T) { + t.Cleanup(func() { require.NoError(t, codefly.LoadEnvironmentVariables()) }) + t.Setenv(resources.EnvironmentPrefix, "local") + manager := resources.NewEnvironmentVariableManager() + manager.SetEnvironment(&basev0.Environment{Name: "local"}) + require.NoError(t, manager.AddConfigurations(context.Background(), &basev0.Configuration{ + Origin: resources.ConfigurationWorkspace, + Infos: []*basev0.ConfigurationInformation{{ + Name: "fixture-composition", + Data: &basev0.ConfigurationData{Kind: "json", Content: []byte(`{"roles":[]}`)}, + }}, + })) + variables, err := manager.All() + require.NoError(t, err) + key := resources.ConfigurationDocumentKey(resources.ConfigurationWorkspace, "fixture-composition", "local", false) + var body strings.Builder + for _, variable := range variables { + if variable.Key == key { + body.WriteString(variable.Key + "=" + variable.ValueAsString() + "\n") + } + } + require.NotEmpty(t, body.String(), "core delivers the document under the key it names") + t.Setenv(key, "") + + require.NoError(t, codefly.LoadRuntimeEnvironmentFile(writeRuntimeEnvironmentFile(t, body.String(), 0o600))) + require.NoError(t, codefly.LoadEnvironmentVariables()) + document, err := codefly.For(context.Background()).WorkspaceConfigurationDocument("fixture-composition") + require.NoError(t, err) + require.JSONEq(t, `{"roles":[]}`, string(document)) +} + +// TestLoadRuntimeEnvironmentFileAdmitsOnlyTheDocumentDigestInLowerCase holds +// the exception to exactly the shape core mints. +func TestLoadRuntimeEnvironmentFileAdmitsOnlyTheDocumentDigestInLowerCase(t *testing.T) { + digest := strings.Repeat("0a", 32) + for name, admitted := range map[string]bool{ + resources.ConfigurationDocumentPrefix + digest: true, + resources.SecretConfigurationDocumentPrefix + digest: true, + resources.ConfigurationDocumentPrefix + strings.ToUpper(digest): true, + resources.ConfigurationDocumentPrefix + digest[1:]: false, + resources.ConfigurationDocumentPrefix + digest + "a": false, + resources.ConfigurationDocumentPrefix + strings.Repeat("g", 64): false, + "CODEFLY__CONFIGURATION_DOCUMENT_V2__" + digest: false, + "CODEFLY__WORKSPACE_CONFIGURATION__" + digest: false, + "CODEFLY__lower": false, + "codefly__CONFIGURATION_DOCUMENT_V1__" + digest: false, + } { + path := writeRuntimeEnvironmentFile(t, name+"=value\n", 0o600) + t.Setenv(name, "") + err := codefly.LoadRuntimeEnvironmentFile(path) + if admitted { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + func writeRuntimeEnvironmentFile( t *testing.T, body string,