diff --git a/collector.go b/collector.go index 2827487..95d65af 100644 --- a/collector.go +++ b/collector.go @@ -44,7 +44,19 @@ func (g *collector) collectSecretFields(v reflect.Value, path string) { case reflect.Map: for _, key := range v.MapKeys() { item := v.MapIndex(key) - g.collectSecretFields(item, fmt.Sprintf("%v[%v]", path, key)) + + if item.Kind() == reflect.Struct { + // If the value is a struct, create a pointer to the map value and modify via pointer + ptr := reflect.New(item.Type()) + ptr.Elem().Set(item) + + g.collectSecretFields(ptr, fmt.Sprintf("%v[%v]", path, key)) + + // Set the modified struct back into the map + v.SetMapIndex(key, ptr.Elem()) + } else { + g.collectSecretFields(item, fmt.Sprintf("%v[%v]", path, key)) + } } case reflect.String: diff --git a/collector_test.go b/collector_test.go index afb41d6..fab6f56 100644 --- a/collector_test.go +++ b/collector_test.go @@ -3,29 +3,10 @@ package cloudsecrets import ( "fmt" "reflect" + "sort" "testing" ) -type config1 struct { - DB dbConfig - JWTSecrets []jwtSecret - Providers map[string]*providerConfig - DoublePtr **providerConfig - unexported dbConfig -} - -type dbConfig struct { - User string - Password string -} - -type providerConfig struct { - Name string - Secret string -} - -type jwtSecret string - func TestCollectFields(t *testing.T) { tt := []struct { Name string @@ -34,18 +15,26 @@ func TestCollectFields(t *testing.T) { Error bool }{ { - Name: "Basic DB config with no creds", - Input: &config1{ + Name: "DB_config_with_no_creds", + Input: &cfg{ DB: dbConfig{ User: "db-user", Password: "db-password", }, + DBPtr: &dbConfig{ + User: "db-user", + Password: "db-password", + }, + DBDoublePtr: ptr(&dbConfig{ + User: "db-user", + Password: "db-password", + }), }, Out: []string{}, }, { - Name: "Basic DB config with creds", - Input: &config1{ + Name: "DB_config_with_creds", + Input: &cfg{ DB: dbConfig{ User: "db-user", Password: "$SECRET:db-password", @@ -54,8 +43,28 @@ func TestCollectFields(t *testing.T) { Out: []string{"db-password"}, }, { - Name: "Slice of secrets", - Input: &config1{ + Name: "DB config ptr with creds", + Input: &cfg{ + DBPtr: &dbConfig{ + User: "db-user", + Password: "$SECRET:db-password", + }, + }, + Out: []string{"db-password"}, + }, + { + Name: "DB_config_double_ptr_with_creds", + Input: &cfg{ + DBDoublePtr: ptr(&dbConfig{ + User: "db-user", + Password: "$SECRET:db-password", + }), + }, + Out: []string{"db-password"}, + }, + { + Name: "Slice_of_secret_values", + Input: &cfg{ DB: dbConfig{ User: "db-user", Password: "$SECRET:secretName", @@ -65,9 +74,20 @@ func TestCollectFields(t *testing.T) { Out: []string{"secretName", "jwtSecret1", "jwtSecret2"}, }, { - Name: "Map with secrets", - Input: &config1{ - Providers: map[string]*providerConfig{ + Name: "Slice_of_secret_pointer_values", + Input: &cfg{ + DB: dbConfig{ + User: "db-user", + Password: "$SECRET:secretName", + }, + JWTSecretsPtr: []*jwtSecret{ptr(jwtSecret("$SECRET:jwtSecret1")), ptr(jwtSecret("$SECRET:jwtSecret2")), ptr(jwtSecret("nope"))}, + }, + Out: []string{"secretName", "jwtSecret1", "jwtSecret2"}, + }, + { + Name: "Map_with_values", + Input: &cfg{ + Providers: map[string]providerConfig{ "provider1": {Name: "provider1", Secret: "$SECRET:secretProvider1"}, "provider2": {Name: "provider2", Secret: "$SECRET:secretProvider2"}, "provider3": {Name: "provider3", Secret: "$SECRET:secretProvider3"}, @@ -76,15 +96,19 @@ func TestCollectFields(t *testing.T) { Out: []string{"secretProvider1", "secretProvider2", "secretProvider3"}, }, { - Name: "Double pointer", - Input: &config1{ - DoublePtr: ptr(&providerConfig{Name: "double-pointer", Secret: "$SECRET:double-pointer-secret"}), + Name: "Map_with_ptr_values", + Input: &cfg{ + ProvidersPtr: map[string]*providerConfig{ + "provider1": {Name: "provider1", Secret: "$SECRET:secretProvider1"}, + "provider2": {Name: "provider2", Secret: "$SECRET:secretProvider2"}, + "provider3": {Name: "provider3", Secret: "$SECRET:secretProvider3"}, + }, }, - Out: []string{"double-pointer-secret"}, + Out: []string{"secretProvider1", "secretProvider2", "secretProvider3"}, }, { - Name: "Unexported field should fail to hydrate", - Input: &config1{ + Name: "Unexported_field_should_fail_to_hydrate", + Input: &cfg{ unexported: dbConfig{ // unexported fields can't be updated via reflect pkg User: "db-user", Password: "$SECRET:secretName", // match inside unexported field @@ -97,7 +121,7 @@ func TestCollectFields(t *testing.T) { for i, tc := range tt { i, tc := i, tc - t.Run(fmt.Sprintf("tt[%v]: %v", i, tc.Name), func(t *testing.T) { + t.Run(tc.Name, func(t *testing.T) { v := reflect.ValueOf(tc.Input) c := &collector{} @@ -106,23 +130,54 @@ func TestCollectFields(t *testing.T) { if tc.Error { if c.err == nil { t.Error("expected error, got nil") + return } } else { if c.err != nil { t.Errorf("unexpected error: %v", c.err) + return } } if len(c.fields) != len(tc.Out) { t.Errorf("expected %v secrets, got %v", len(tc.Out), len(c.fields)) } - for i := 0; i < len(c.fields); i++ { - if c.fields[i].secretName != tc.Out[i] { - t.Errorf("collected field[%v].secretName=%v doesn't match tc.Out[%v]=%v", i, c.fields[i].secretName, i, tc.Out[i]) + + fields := c.fields + sort.Slice(fields, func(i, j int) bool { + return fields[i].fieldPath <= fields[j].fieldPath + }) + + for i := 0; i < len(fields); i++ { + if fields[i].secretName != tc.Out[i] { + t.Errorf("collected field[%v].secretName=%v doesn't match tc.Out[%v]=%v", i, fields[i].secretName, i, tc.Out[i]) } } }) } } +type cfg struct { + DB dbConfig + DBPtr *dbConfig + DBDoublePtr **dbConfig + JWTSecrets []jwtSecret + JWTSecretsPtr []*jwtSecret + Providers map[string]providerConfig + ProvidersPtr map[string]*providerConfig + unexported dbConfig +} + +type dbConfig struct { + User string + Password string +} + +type providerConfig struct { + Name string + Secret string +} + +type jwtSecret string + func ptr[T any](v T) *T { return &v }