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
40 changes: 40 additions & 0 deletions runtime_environment_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"io"
"os"
"strings"

"github.com/codefly-dev/core/resources"
)

const (
Expand Down Expand Up @@ -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':
Expand All @@ -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
}
67 changes: 67 additions & 0 deletions runtime_environment_file_test.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand Down Expand Up @@ -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,
Expand Down
Loading