diff --git a/runtime/parser/parse_connector.go b/runtime/parser/parse_connector.go index ec74ddd2478..d73898e8384 100644 --- a/runtime/parser/parse_connector.go +++ b/runtime/parser/parse_connector.go @@ -193,7 +193,9 @@ func parseMCPConnector(tmp *ConnectorYAML) ([]*runtimev1.SecurityRule, []string, return rules, sec.Agents, nil } -// analyzeTemplatedProperties returns a slice of map keys that have a value which contains templating tags. +// analyzeTemplatedProperties returns a sorted slice of map keys that have a value which contains templating tags. +// The keys are sorted because map iteration order is random, +// and the project parser treats a spec as changed (and re-reconciles everything downstream) if the order differs between parses. func analyzeTemplatedProperties(m map[string]any) ([]string, error) { var res []string for k, v := range m { @@ -205,6 +207,7 @@ func analyzeTemplatedProperties(m map[string]any) ([]string, error) { res = append(res, k) } } + slices.Sort(res) return res, nil } diff --git a/runtime/parser/parser_test.go b/runtime/parser/parser_test.go index 98278317e43..0770c849024 100644 --- a/runtime/parser/parser_test.go +++ b/runtime/parser/parser_test.go @@ -925,6 +925,32 @@ driver: duckdb require.NotContains(t, m1Updated.Refs, ResourceName{Kind: ResourceKindConnector, Name: "duckdb"}, "model should not ref deleted connector") } +func TestConnectorTemplatedPropertiesOrder(t *testing.T) { + // The project parser compares specs with proto.Equal to detect changes, + // so re-parsing an unchanged connector must return its templated properties in the same order. + ctx := context.Background() + repo := makeRepo(t, map[string]string{ + `rill.yaml`: ``, + `connectors/clickhouse.yaml`: ` +type: connector +driver: clickhouse +mode: readwrite +dsn: "{{ .env.dsn }}" +write_dsn: "{{ .env.write_dsn }}" +password: "{{ .env.password }}" +`, + }) + + // Parse several times since an unstable order only shows up on some runs + for range 20 { + p, err := Parse(ctx, repo, "", "", "duckdb", true) + require.NoError(t, err) + r := p.Resources[ResourceName{Kind: ResourceKindConnector, Name: "clickhouse"}] + require.NotNil(t, r) + require.Equal(t, []string{"dsn", "password", "write_dsn"}, r.ConnectorSpec.TemplatedProperties) + } +} + func BenchmarkReparse(b *testing.B) { ctx := context.Background() files := map[string]string{