Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
44 changes: 43 additions & 1 deletion vals.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,10 +339,52 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) {
return valStr, nil
}

uri, err := url.Parse(key)
// Handle ARN-based URIs which contain colons that would be misinterpreted as port separators
// ARN format: arn:aws:service:region:account:resource
// We need to detect and transform the ARN to avoid URL parsing issues with colons
processedKey := key
arnValue := ""

// Check if this looks like an ARN-based URI (contains "://arn:aws:" or "://arn:aws-")
if strings.Contains(key, "://arn:aws:") || strings.Contains(key, "://arn:aws-") {
// Find the scheme end and extract the ARN
schemeEnd := strings.Index(key, "://")
if schemeEnd != -1 {
prefix := key[:schemeEnd+3] // includes "://"
remainder := key[schemeEnd+3:]

// Find where the ARN ends (at ? for query params, # for fragment, or end of string)
arnEnd := len(remainder)
if idx := strings.IndexAny(remainder, "?#"); idx != -1 {
arnEnd = idx
}

arnValue = remainder[:arnEnd]
suffix := remainder[arnEnd:]

// Transform to use triple-slash format (empty host, ARN in path)
// This prevents the colon in ARN from being interpreted as a port separator
processedKey = prefix + "/" + arnValue + suffix
}
}

uri, err := url.Parse(processedKey)
if err != nil {
return "", err
}

// If we processed an ARN, extract it from the path and restore to host
if arnValue != "" {
// Remove the leading slash we added
uri.Host = strings.TrimPrefix(uri.Path, "/")
// Find the actual path after the ARN
arnLen := len(arnValue)
if len(uri.Path) > arnLen+1 {
uri.Path = uri.Path[arnLen+1:]
} else {
uri.Path = ""
}
Comment on lines +379 to +385
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "Find the actual path after the ARN" on line 380 is potentially misleading. In the current implementation and test cases, the entire ARN (including any path-like components such as /myteam/mydoc) is treated as the resource identifier and placed in uri.Host. The logic here ensures uri.Path is empty when the entire remainder was the ARN, which is the correct behavior. Consider updating this comment to clarify that this handles the edge case where there might be additional path components after the ARN, though in practice for AWS ARNs, the entire identifier including any slashes is part of the ARN itself and should go to the host.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

}

hash := uriToProviderHash(uri)

Expand Down
96 changes: 96 additions & 0 deletions vals_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"sort"
"strings"
"testing"
"crypto/md5"
"fmt"
Comment on lines +10 to +11
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import statements should be organized with standard library imports grouped together before third-party imports. The crypto/md5 and fmt imports should be moved up to join the other standard library imports (bytes, os, path/filepath, sort, strings, testing) rather than being placed after testing.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback


"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -187,3 +189,97 @@ datetime_offset: "2025-01-01T12:34:56+01:00"

require.Equal(t, expected, buf.String())
}

// mockProvider is a simple test double implementing api.Provider
type mockProvider struct {
getStringFunc func(string) (string, error)
getStringMapFunc func(string) (map[string]interface{}, error)
}

func (m *mockProvider) GetString(key string) (string, error) {
if m.getStringFunc != nil {
return m.getStringFunc(key)
}
return "", nil
}

func (m *mockProvider) GetStringMap(key string) (map[string]interface{}, error) {
if m.getStringMapFunc != nil {
return m.getStringMapFunc(key)
}
return nil, nil
}

func TestARNFragmentExtractionWithMockProvider(t *testing.T) {
r, err := New(Options{})
require.NoError(t, err)

// compute provider hash used by Runtime (scheme + query.Encode())
hash := fmt.Sprintf("%x", md5.Sum([]byte("echo")))
Comment on lines +217 to +218
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hash calculation on line 218 only includes the scheme ("echo") but not the query parameters. According to the uriToProviderHash function in vals.go (lines 163-169), the hash should include both uri.Scheme and uri.Query().Encode(). Since this test uses "ref+echo://..." with no query parameters, the query would be empty, so the current calculation happens to be correct. However, the comment is misleading - it says "scheme + query.Encode()" but the code only uses the scheme. Consider updating the comment to clarify that this works because there are no query parameters, or better yet, use the actual uriToProviderHash logic to compute the hash to make this test more robust and maintainable.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback


arn := "arn:aws:secretsmanager:us-east-1:123456789012:secret:/myteam/mydoc"

// mock provider returns a nested map for the ARN key
mock := &mockProvider{
getStringMapFunc: func(key string) (map[string]interface{}, error) {
if key != arn {
t.Fatalf("unexpected key passed to provider.GetStringMap: %q", key)
}
return map[string]interface{}{
"myteam": map[string]interface{}{
"mydoc": "mydoc",
},
}, nil
},
}

r.providers[hash] = mock

res, err := r.Get("ref+echo://" + arn + "#/myteam/mydoc")
require.NoError(t, err)
require.Equal(t, "mydoc", res)
}

func TestARNBasedURIParsing(t *testing.T) {
// Test that ARN-based URIs with colons are parsed correctly
// This tests the fix for issue #909
testCases := []struct {
name string
input string
expected string
checkResult bool
}{
{
name: "Simple echo ARN format",
input: "ref+echo://arn:aws:secretsmanager:us-east-1:123456789012:secret:/demo/app/database",
expected: "arn:aws:secretsmanager:us-east-1:123456789012:secret:/demo/app/database",
checkResult: true,
},
{
name: "ARN with query params",
input: "ref+echo://arn:aws:secretsmanager:us-east-1:123456789012:secret:/demo/app/database?region=us-east-1",
expected: "arn:aws:secretsmanager:us-east-1:123456789012:secret:/demo/app/database",
checkResult: true,
},
{
name: "ARN with fragment - parse only",
input: "ref+echo://arn:aws:secretsmanager:us-east-1:123456789012:secret:/myteam/mydoc#/myteam/mydoc",
checkResult: false,
},
{
name: "ARN with both query and fragment - parse only",
input: "ref+echo://arn:aws:secretsmanager:us-east-1:123456789012:secret:/demo/app/database?region=us-east-1#/demo/app/database",
checkResult: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := Get(tc.input, Options{})
require.NoError(t, err, "Failed to parse ARN-based URI: %s", tc.input)
if tc.checkResult {
require.Equal(t, tc.expected, result)
}
})
}
}