Skip to content

Instantly share code, notes, and snippets.

@bwplotka
Last active April 16, 2025 11:27
Show Gist options
  • Save bwplotka/baf5344837d98719dd126ed6bd143015 to your computer and use it in GitHub Desktop.
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.
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.
}
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