Last active
April 16, 2025 11:27
-
-
Save bwplotka/baf5344837d98719dd126ed6bd143015 to your computer and use it in GitHub Desktop.
# 47 // 1. Dynamic provider fields VS per provider sections. // 2. Field with the dynamic "inline OR providers" type VS separate fields for inline, file and providers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package yolo | |
import ( | |
"context" | |
"testing" | |
"github.com/google/go-cmp/cmp" | |
// Ecosystem is moving away from "gopkg.in/yaml.v2", so let's use what will be used long term | |
// (does not matter much). | |
"github.com/goccy/go-yaml" | |
) | |
// Two core questions: | |
// 1. Dynamic provider fields VS per provider sections. | |
// 2. Field with the dynamic "inline OR providers" type VS separate fields for inline, file and providers. | |
type KubernetesSP struct { | |
Namespace string `yaml:"namespace"` | |
Name string `yaml:"name"` | |
Key string `yaml:"key"` | |
} | |
type FileSP struct { | |
Path string `yaml:"path"` | |
} | |
type InlineSP struct { | |
Password string `yaml:"path"` // TODO: Add redaction capability. | |
} | |
// To answer (2): | |
// A) Current proposal. | |
const ( | |
aExample = ` | |
password: | |
kubernetes: | |
namespace: "<ns>" | |
name: "<secret name>" | |
key: "<data's key for secret name>" | |
` | |
aExampleInline = ` | |
password: "<inlined secret>" | |
` | |
) | |
type ElementA struct { | |
Password SecretA `yaml:"password"` | |
} | |
type SecretA struct { | |
Kubernetes KubernetesSP `yaml:"kubernetes"` | |
File FileSP `yaml:"file"` | |
Inline InlineSP `yaml:"inline"` // Could be also just string. | |
} | |
func (s *SecretA) UnmarshalYAML(_ context.Context, unmarshalFn func(any) error) error { | |
// Try inlined form first, for compatibility and ease of use. | |
var inlinedForm string | |
if err := unmarshalFn(&inlinedForm); err == nil { | |
s.Inline = InlineSP{Password: inlinedForm} | |
return nil | |
} | |
// Fallback to complex struct. | |
// NOTE: "plain" casting is needed to avoid recursive call to UnmarshalYAML. | |
// This is what the current Prometheus code is doing too. | |
type plain SecretA | |
return unmarshalFn((*plain)(s)) | |
} | |
func TestA(t *testing.T) { | |
t.Run("kube", func(t *testing.T) { | |
var got ElementA | |
if err := yaml.Unmarshal([]byte(aExample), &got); err != nil { | |
t.Fatal(err) | |
} | |
expected := ElementA{ | |
Password: SecretA{ | |
Kubernetes: KubernetesSP{ | |
Namespace: "<ns>", | |
Name: "<secret name>", | |
Key: "<data's key for secret name>", | |
}, | |
}, | |
} | |
if diff := cmp.Diff(expected, got); diff != "" { | |
t.Fatal(diff) | |
} | |
}) | |
t.Run("inline", func(t *testing.T) { | |
var got ElementA | |
if err := yaml.Unmarshal([]byte(aExampleInline), &got); err != nil { | |
t.Fatal(err) | |
} | |
expected := ElementA{ | |
Password: SecretA{ | |
Inline: InlineSP{ | |
Password: "<inlined secret>", | |
}, | |
}, | |
} | |
if diff := cmp.Diff(expected, got); diff != "" { | |
t.Fatal(diff) | |
} | |
}) | |
} | |
// B) More explicit mode | |
const ( | |
bExample = ` | |
password_complex: | |
kubernetes: | |
namespace: "<ns>" | |
name: "<secret name>" | |
key: "<data's key for secret name>" | |
` | |
bExampleInline = ` | |
password: "<inlined secret>" | |
` | |
) | |
// Just types for demo, trivial to implement. | |
type ElementB struct { | |
Password string `yaml:"password"` | |
PasswordComplex SecretB `yaml:"password_complex"` | |
} | |
type SecretB struct { | |
Kubernetes KubernetesSP `yaml:"kubernetes"` | |
File FileSP `yaml:"file"` | |
Inline InlineSP `yaml:"inline"` // Could be also just string. | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package yolo | |
import ( | |
"context" | |
"fmt" | |
"testing" | |
// Ecosystem is moving away from "gopkg.in/yaml.v2", so let's use what will be used long term | |
// (does not matter much). | |
"github.com/goccy/go-yaml" | |
"github.com/google/go-cmp/cmp" | |
) | |
// Two core questions: | |
// 1. Dynamic provider fields VS per provider sections. | |
// 2. Field with the dynamic "inline OR providers" type VS separate fields for inline, file and providers. | |
type KubernetesSP struct { | |
Namespace string `yaml:"namespace"` | |
Name string `yaml:"name"` | |
Key string `yaml:"key"` | |
} | |
type FileSP struct { | |
Path string `yaml:"path"` | |
} | |
// To answer (1): | |
const ( | |
// A) Current proposal as of 16.04.2025 (https://github.com/prometheus/proposals/pull/47). | |
aExample = ` | |
password: | |
provider: kubernetes | |
namespace: "<ns>" | |
name: "<secret name>" | |
key: "<data's key for secret name>" | |
` | |
aExampleFile = ` | |
password: | |
provider: file | |
path: "<path to secret file>" | |
` | |
) | |
type ElementA struct { | |
Password SecretA `yaml:"password"` | |
} | |
type SecretA struct { | |
Provider string `yaml:"provider"` | |
// Custom unmarshal is used for those. | |
Kubernetes KubernetesSP `yaml:"-"` | |
File FileSP `yaml:"-"` | |
} | |
func (s *SecretA) UnmarshalYAML(_ context.Context, unmarshalFn func(any) error) error { | |
provider := struct { | |
Provider string `yaml:"provider"` | |
}{} | |
if err := unmarshalFn(&provider); err != nil { | |
return err | |
} | |
switch provider.Provider { | |
case "kubernetes": | |
s.Provider = provider.Provider | |
return unmarshalFn(&s.Kubernetes) | |
case "file": | |
s.Provider = provider.Provider | |
return unmarshalFn(&s.File) | |
default: | |
return fmt.Errorf("unknown provider %q", provider.Provider) | |
} | |
} | |
func TestA(t *testing.T) { | |
t.Run("kube", func(t *testing.T) { | |
var got ElementA | |
if err := yaml.Unmarshal([]byte(aExample), &got); err != nil { | |
t.Fatal(err) | |
} | |
expected := ElementA{ | |
Password: SecretA{ | |
Provider: "kubernetes", | |
Kubernetes: KubernetesSP{ | |
Namespace: "<ns>", | |
Name: "<secret name>", | |
Key: "<data's key for secret name>", | |
}, | |
}, | |
} | |
if diff := cmp.Diff(expected, got); diff != "" { | |
t.Fatal(diff) | |
} | |
}) | |
t.Run("file", func(t *testing.T) { | |
var got ElementA | |
if err := yaml.Unmarshal([]byte(aExampleFile), &got); err != nil { | |
t.Fatal(err) | |
} | |
expected := ElementA{ | |
Password: SecretA{ | |
Provider: "file", | |
File: FileSP{ | |
Path: "<path to secret file>", | |
}, | |
}, | |
} | |
if diff := cmp.Diff(expected, got); diff != "" { | |
t.Fatal(diff) | |
} | |
}) | |
} | |
const ( | |
// B) @dgl proposal. | |
bExample = ` | |
password: | |
provider: kubernetes | |
kubernetes: | |
namespace: "<ns>" | |
name: "<secret name>" | |
key: "<data's key for secret name>" | |
` | |
bExampleFile = ` | |
password: | |
provider: file | |
file: | |
path: "<path to secret file>" | |
` | |
) | |
type ElementB struct { | |
Password SecretB `yaml:"password"` | |
} | |
type SecretB struct { | |
// TODO(bwplotka): Requires validation ofc, but trivial. See C example, I don't think we need this field. | |
Provider string `yaml:"provider"` | |
Kubernetes KubernetesSP `yaml:"kubernetes"` | |
File FileSP `yaml:"file"` | |
} | |
func TestB(t *testing.T) { | |
t.Run("kube", func(t *testing.T) { | |
var got ElementB | |
if err := yaml.Unmarshal([]byte(bExample), &got); err != nil { | |
t.Fatal(err) | |
} | |
expected := ElementB{ | |
Password: SecretB{ | |
Provider: "kubernetes", | |
Kubernetes: KubernetesSP{ | |
Namespace: "<ns>", | |
Name: "<secret name>", | |
Key: "<data's key for secret name>", | |
}, | |
}, | |
} | |
if diff := cmp.Diff(expected, got); diff != "" { | |
t.Fatal(diff) | |
} | |
}) | |
t.Run("file", func(t *testing.T) { | |
var got ElementB | |
if err := yaml.Unmarshal([]byte(bExampleFile), &got); err != nil { | |
t.Fatal(err) | |
} | |
expected := ElementB{ | |
Password: SecretB{ | |
Provider: "file", | |
File: FileSP{ | |
Path: "<path to secret file>", | |
}, | |
}, | |
} | |
if diff := cmp.Diff(expected, got); diff != "" { | |
t.Fatal(diff) | |
} | |
}) | |
} | |
type ElementC struct { | |
Password SecretC `yaml:"password"` | |
} | |
type SecretC struct { | |
Kubernetes KubernetesSP `yaml:"kubernetes"` | |
File FileSP `yaml:"file"` | |
} | |
const ( | |
// C) What we could make it truly like Scrape Config's SD? (field tells what provider you use). | |
cExample = ` | |
password: | |
kubernetes: | |
namespace: "<ns>" | |
name: "<secret name>" | |
key: "<data's key for secret name>" | |
` | |
cExampleFile = ` | |
password: | |
file: | |
path: "<path to secret file>" | |
` | |
) | |
func TestC(t *testing.T) { | |
t.Run("kube", func(t *testing.T) { | |
var got ElementC | |
if err := yaml.Unmarshal([]byte(cExample), &got); err != nil { | |
t.Fatal(err) | |
} | |
expected := ElementC{ | |
Password: SecretC{ | |
Kubernetes: KubernetesSP{ | |
Namespace: "<ns>", | |
Name: "<secret name>", | |
Key: "<data's key for secret name>", | |
}, | |
}, | |
} | |
if diff := cmp.Diff(expected, got); diff != "" { | |
t.Fatal(diff) | |
} | |
}) | |
t.Run("file", func(t *testing.T) { | |
var got ElementC | |
if err := yaml.Unmarshal([]byte(cExampleFile), &got); err != nil { | |
t.Fatal(err) | |
} | |
expected := ElementC{ | |
Password: SecretC{ | |
File: FileSP{ | |
Path: "<path to secret file>", | |
}, | |
}, | |
} | |
if diff := cmp.Diff(expected, got); diff != "" { | |
t.Fatal(diff) | |
} | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment