Skip to content

Accept optional strings as number arguments #3766

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 17, 2025
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Main (unreleased)
### Enhancements

- Add `hash_string_id` argument to `foreach` block to hash the string representation of the pipeline id instead of using the string itself. (@wildum)

- Update `async-profiler` binaries for `pyroscope.java` to 4.0-87b7b42 (@github-hamza-bouqal)

- (_Experimental_) `prometheus.write.queue` add support for exemplars. (@dehaansa)
Expand All @@ -30,13 +31,17 @@ Main (unreleased)
### Bugfixes

- Fix the `validate` command not understanding the `livedebugging` block. (@dehaansa)

- Fix invalid class names in python profiles obtained with `pyroscope.ebpf`. (@korniltsev)

- For CRD-based components (`prometheus.operator.*`), retry initializing informers if the apiserver request fails. This rectifies issues where the apiserver is not reachable immediately after node restart. (@dehaansa)

- Fixed a bug which prevented non-secret optional secrets to be passed in as `number` arguments. (@ptodev)

### Other changes

- Mark `pyroscope.receive_http` and `pyroscope.relabel` components as GA. (@marcsanmi)

- Upgrade `otelcol` components from OpenTelemetry v0.126.0 to v0.127.0 (@korniltsev)

v1.9.1
Expand Down
12 changes: 12 additions & 0 deletions syntax/internal/value/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,18 @@ func convertValue(val Value, toType Type) (Value, error) {
return Uint(parsed), nil
}
}

case TypeCapsule:
// Some capsules, such as optional secrects, may be convertible to a string.
// Try to convert them to a string and then rerun convertValue.
into := reflect.New(reflect.TypeOf(string(""))).Elem()
ok, err := tryCapsuleConvert(val, into, TypeString)
if ok && err == nil {
val, err := convertValue(Value{into, TypeString}, toType)
if err == nil {
return val, nil
}
}
}

return Null, TypeError{Value: val, Expected: toType}
Expand Down
11 changes: 10 additions & 1 deletion syntax/vm/vm_stdlib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ func TestVM_Stdlib_Errors(t *testing.T) {
func TestStdlibCoalesce(t *testing.T) {
t.Setenv("TEST_VAR2", "Hello!")

scope := vm.NewScope(map[string]any{
"optionalSecretStr": alloytypes.OptionalSecret{Value: "bar"},
"optionalSecretInt": alloytypes.OptionalSecret{Value: "123", IsSecret: false},
})

tt := []struct {
name string
input string
Expand All @@ -221,6 +226,10 @@ func TestStdlibCoalesce(t *testing.T) {
{"coalesce(object, true) and return true", `coalesce(encoding.from_json("{}"), true)`, true},
{"coalesce(object, false) and return false", `coalesce(encoding.from_json("{}"), false)`, false},
{"coalesce(list, nil)", `coalesce([],null)`, value.Null},
{"optional secret str first in coalesce", `coalesce(optionalSecretStr, 1)`, string("bar")},
{"optional secret str second in coalesce", `coalesce("foo", optionalSecretStr)`, string("foo")},
{"optional secret int first in coalesce", `coalesce(optionalSecretInt, 1)`, int(123)},
{"optional secret int second in coalesce", `coalesce(1, optionalSecretInt)`, int(1)},
}

for _, tc := range tt {
Expand All @@ -231,7 +240,7 @@ func TestStdlibCoalesce(t *testing.T) {
eval := vm.New(expr)

rv := reflect.New(reflect.TypeOf(tc.expect))
require.NoError(t, eval.Evaluate(nil, rv.Interface()))
require.NoError(t, eval.Evaluate(scope, rv.Interface()))
require.Equal(t, tc.expect, rv.Elem().Interface())
})
}
Expand Down
44 changes: 44 additions & 0 deletions syntax/vm/vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"
"unicode"

"github.com/grafana/alloy/syntax/alloytypes"
"github.com/grafana/alloy/syntax/parser"
"github.com/grafana/alloy/syntax/scanner"
"github.com/grafana/alloy/syntax/token"
Expand Down Expand Up @@ -61,6 +62,49 @@ func TestVM_Evaluate_Literals(t *testing.T) {
}
}

func TestVM_Evaluate_Secrets(t *testing.T) {
scope := vm.NewScope(map[string]any{
"secretSecret": alloytypes.Secret("foo"),
"optionalSecretStr": alloytypes.OptionalSecret{Value: "bar"},
"optionalSecretInt": alloytypes.OptionalSecret{Value: "123", IsSecret: false},
"optionalSecretNegative": alloytypes.OptionalSecret{Value: "-123", IsSecret: false},
"optionalSecretFloat": alloytypes.OptionalSecret{Value: "23.5", IsSecret: false},
})

tt := map[string]struct {
input string
expect interface{}
errMsg string
}{
"secret": {`secretSecret`, string("bar"), "secrets may not be converted into strings"},
"optional secret str": {`optionalSecretStr`, string("bar"), ""},
"optional secret int": {`optionalSecretInt`, int(123), ""},
"optional secret negative int": {`optionalSecretNegative`, int(-123), ""},
"optional secret float": {`optionalSecretFloat`, float64(23.5), ""},
}

for name, tc := range tt {
t.Run(name, func(t *testing.T) {
expr, err := parser.ParseExpression(tc.input)
require.NoError(t, err)

eval := vm.New(expr)

vPtr := reflect.New(reflect.TypeOf(tc.expect)).Interface()

err = eval.Evaluate(scope, vPtr)
if tc.errMsg == "" {
require.NoError(t, err)

actual := reflect.ValueOf(vPtr).Elem().Interface()
require.Equal(t, tc.expect, actual)
} else {
require.ErrorContains(t, err, tc.errMsg)
}
})
}
}

func TestVM_Evaluate(t *testing.T) {
// Shared scope across all tests below
scope := vm.NewScope(map[string]interface{}{
Expand Down