mirror of
https://github.com/enix/x509-certificate-exporter.git
synced 2026-08-19 03:56:21 +00:00
refactor(pkg): promote fileglob and source/* from internal
This commit is contained in:
@@ -5,7 +5,9 @@
|
||||
- `cmd/x509-certificate-exporter/` — binary entrypoint
|
||||
- `pkg/cert/` — public API for certificate parsing (`pem`, `pkcs12` subpackages)
|
||||
- `pkg/registry/` — public Prometheus collector + label registry
|
||||
- `internal/` — everything else (config, log, source/{file,k8s}, server, fileglob, product)
|
||||
- `pkg/fileglob/` — public glob/walk engine (EXPERIMENTAL — promoted in v4 RC)
|
||||
- `pkg/source/{file,k8s,kubeconfig}/` — public Source implementations (EXPERIMENTAL)
|
||||
- `internal/` — wiring & process-lifecycle: config, log, server, product
|
||||
- `chart/` — Helm chart
|
||||
|
||||
The v3 code was deleted; v4 is at the repo root. The Go module path uses `/v4`
|
||||
|
||||
+10
-7
@@ -311,15 +311,16 @@ direnv.
|
||||
├── cmd/x509-certificate-exporter/ Binary entrypoint (main.go and CLI flags)
|
||||
├── pkg/ Public Go API
|
||||
│ ├── cert/ Certificate parsing (PEM, PKCS#12)
|
||||
│ └── registry/ Prometheus collector + label registry
|
||||
├── internal/ Implementation details
|
||||
│ ├── registry/ Prometheus collector + label registry
|
||||
│ ├── fileglob/ Glob expansion helpers (EXPERIMENTAL)
|
||||
│ └── source/ Source implementations (EXPERIMENTAL)
|
||||
│ ├── file/ File / directory globbing
|
||||
│ ├── k8s/ Kubernetes Secret/ConfigMap LIST+WATCH
|
||||
│ └── kubeconfig/ kubeconfig embedded-cert extraction
|
||||
├── internal/ Wiring & process lifecycle
|
||||
│ ├── config/ YAML config loader
|
||||
│ ├── log/ Structured logging setup
|
||||
│ ├── source/ The "source" abstraction
|
||||
│ │ ├── file/ File / directory globbing
|
||||
│ │ └── k8s/ Kubernetes Secret/ConfigMap LIST+WATCH
|
||||
│ ├── server/ HTTP server, metrics endpoint
|
||||
│ ├── fileglob/ Glob expansion helpers
|
||||
│ └── product/ Build-time injected version metadata
|
||||
├── chart/ Helm chart (THE primary deploy method)
|
||||
├── dev/
|
||||
@@ -829,7 +830,9 @@ into implementation.
|
||||
### Code style
|
||||
|
||||
- **Public API in `pkg/`, internals in `internal/`.** Don't promote
|
||||
without a real consumer.
|
||||
without a real consumer. Packages flagged EXPERIMENTAL in their
|
||||
godoc were promoted ahead of a settled API contract — pin a
|
||||
specific version if you depend on them.
|
||||
- **Tests colocated with code** as `*_test.go`.
|
||||
- **Table-driven tests** for parsers, registries, anything with many
|
||||
similar cases.
|
||||
|
||||
@@ -13,7 +13,7 @@ filesystem operation** — `open`, `Stat`, `Lstat`, `ReadDir`, `Readlink`.
|
||||
Today (post-symlink-mapping change) the chart writes **in-pod** paths
|
||||
into the configmap (`/mnt/watch/file-<sha1>/var/lib/kubelet/pki/...`),
|
||||
and `PathMapping` is consulted only when an absolute symlink target is
|
||||
read back — i.e. only inside `internal/fileglob.handleSymlink`. The
|
||||
read back — i.e. only inside `pkg/fileglob.handleSymlink`. The
|
||||
proposal is to flip the polarity: have the chart write raw host paths,
|
||||
and let the runtime translate transparently for every FS op.
|
||||
|
||||
@@ -27,7 +27,7 @@ and let the runtime translate transparently for every FS op.
|
||||
the runtime knows the host path natively.
|
||||
|
||||
2. **One translation point, not two.** Right now
|
||||
`internal/fileglob.handleSymlink` has bespoke translation +
|
||||
`pkg/fileglob.handleSymlink` has bespoke translation +
|
||||
containment logic. With a universal translator, the same logic
|
||||
covers every code path that touches the FS, so any new FS-using
|
||||
feature is covered for free.
|
||||
@@ -39,7 +39,7 @@ and let the runtime translate transparently for every FS op.
|
||||
### Why this is *not* done as part of the symlink-mapping change
|
||||
|
||||
- **Blast radius.** It touches every FS op in `fileglob`, not just
|
||||
symlink resolution. The cache key in `internal/source/file/file.go`
|
||||
symlink resolution. The cache key in `pkg/source/file/file.go`
|
||||
becomes the host path (today it's the walker's `Path` = in-pod),
|
||||
affecting cache invariants and the `SkipUnchanged` machinery.
|
||||
|
||||
@@ -70,12 +70,12 @@ and let the runtime translate transparently for every FS op.
|
||||
|
||||
### Sketch of the change
|
||||
|
||||
- `internal/fileglob/walkfs.go` (new): a small `WalkFS` decorator that
|
||||
- `pkg/fileglob/walkfs.go` (new): a small `WalkFS` decorator that
|
||||
takes `[]PathMapping` and a base `WalkFS`, applies the
|
||||
longest-prefix `From → To` rewrite to every method's `name`
|
||||
argument, and forwards. The walker becomes oblivious to the
|
||||
translation — it sees host paths everywhere.
|
||||
- `internal/source/file/file.go`: drop the special-case
|
||||
- `pkg/source/file/file.go`: drop the special-case
|
||||
`if e.LinkTo != "" { readPath = e.LinkTo }` (no longer needed —
|
||||
`Reader` would also be wrapped to apply the translation).
|
||||
- `chart/templates/configmap.yaml`: write host paths in
|
||||
|
||||
+1
-1
@@ -218,7 +218,7 @@ tasks:
|
||||
cmds:
|
||||
- go test -run=^$ -fuzz=FuzzParse -fuzztime=5s ./pkg/cert/pem/
|
||||
- go test -run=^$ -fuzz=FuzzParse -fuzztime=5s ./pkg/cert/pkcs12/
|
||||
- go test -run=^$ -fuzz=FuzzCompile -fuzztime=5s ./internal/fileglob/
|
||||
- go test -run=^$ -fuzz=FuzzCompile -fuzztime=5s ./pkg/fileglob/
|
||||
|
||||
test:helm-examples:
|
||||
desc: "Assert every docs/examples/**/*.values.yaml is accepted by the chart"
|
||||
|
||||
@@ -26,17 +26,17 @@ import (
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
|
||||
"github.com/enix/x509-certificate-exporter/v4/internal/config"
|
||||
"github.com/enix/x509-certificate-exporter/v4/internal/fileglob"
|
||||
xlog "github.com/enix/x509-certificate-exporter/v4/internal/log"
|
||||
"github.com/enix/x509-certificate-exporter/v4/internal/product"
|
||||
"github.com/enix/x509-certificate-exporter/v4/internal/server"
|
||||
filesource "github.com/enix/x509-certificate-exporter/v4/internal/source/file"
|
||||
k8ssource "github.com/enix/x509-certificate-exporter/v4/internal/source/k8s"
|
||||
kcsource "github.com/enix/x509-certificate-exporter/v4/internal/source/kubeconfig"
|
||||
"github.com/enix/x509-certificate-exporter/v4/pkg/cert"
|
||||
pemparser "github.com/enix/x509-certificate-exporter/v4/pkg/cert/pem"
|
||||
pkcs12parser "github.com/enix/x509-certificate-exporter/v4/pkg/cert/pkcs12"
|
||||
"github.com/enix/x509-certificate-exporter/v4/pkg/fileglob"
|
||||
"github.com/enix/x509-certificate-exporter/v4/pkg/registry"
|
||||
filesource "github.com/enix/x509-certificate-exporter/v4/pkg/source/file"
|
||||
k8ssource "github.com/enix/x509-certificate-exporter/v4/pkg/source/k8s"
|
||||
kcsource "github.com/enix/x509-certificate-exporter/v4/pkg/source/kubeconfig"
|
||||
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/enix/x509-certificate-exporter/v4/internal/fileglob"
|
||||
"github.com/enix/x509-certificate-exporter/v4/pkg/cert"
|
||||
"github.com/enix/x509-certificate-exporter/v4/pkg/fileglob"
|
||||
)
|
||||
|
||||
// Canonical values for Source.Kind in the YAML config. These strings are
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Package fileglob implements a small, dedicated glob/walk engine for
|
||||
// certificate discovery. It is NOT a general-purpose glob library.
|
||||
//
|
||||
// EXPERIMENTAL: this package was promoted from internal/ to enable external
|
||||
// reuse but its API surface may still change without notice in v5.
|
||||
//
|
||||
// Supported pattern tokens:
|
||||
//
|
||||
// - match within a single segment, never across "/"
|
||||
@@ -515,11 +518,11 @@ func (w *walker) descend(ctx context.Context, dir string, depth int, seenInodes
|
||||
entries, err := w.fsys.ReadDir(dir)
|
||||
if err != nil {
|
||||
// Report the dir-level error and stop descending here.
|
||||
reason := "walk_error"
|
||||
reason := cert.ReasonWalkError
|
||||
if os.IsPermission(err) {
|
||||
reason = "permission_denied"
|
||||
reason = cert.ReasonPermissionDenied
|
||||
} else if os.IsNotExist(err) {
|
||||
reason = "not_found"
|
||||
reason = cert.ReasonNotFound
|
||||
}
|
||||
w.emit(Result{Err: &Error{Path: dir, Reason: reason, Err: err}})
|
||||
return
|
||||
@@ -542,7 +545,7 @@ func (w *walker) descend(ctx context.Context, dir string, depth int, seenInodes
|
||||
}
|
||||
info, err := w.fsys.Lstat(full)
|
||||
if err != nil {
|
||||
w.emit(Result{Err: &Error{Path: full, Reason: "walk_error", Err: err}})
|
||||
w.emit(Result{Err: &Error{Path: full, Reason: cert.ReasonWalkError, Err: err}})
|
||||
continue
|
||||
}
|
||||
// Determine whether this entry can match any include (file or dir).
|
||||
@@ -585,7 +588,7 @@ func (w *walker) descend(ctx context.Context, dir string, depth int, seenInodes
|
||||
func (w *walker) handleSymlink(ctx context.Context, full string, info fs.FileInfo, fileMatched *Pattern, canDescend bool, depth int, seen map[uint64]struct{}) {
|
||||
target, err := w.fsys.Readlink(full)
|
||||
if err != nil {
|
||||
w.emit(Result{Err: &Error{Path: full, Reason: "broken_symlink", Err: err}})
|
||||
w.emit(Result{Err: &Error{Path: full, Reason: cert.ReasonBrokenSymlink, Err: err}})
|
||||
return
|
||||
}
|
||||
resolved := target
|
||||
@@ -610,7 +613,7 @@ func (w *walker) handleSymlink(ctx context.Context, full string, info fs.FileInf
|
||||
|
||||
tinfo, err := w.fsys.Stat(resolved)
|
||||
if err != nil {
|
||||
w.emit(Result{Err: &Error{Path: full, Reason: "broken_symlink", Err: err}})
|
||||
w.emit(Result{Err: &Error{Path: full, Reason: cert.ReasonBrokenSymlink, Err: err}})
|
||||
return
|
||||
}
|
||||
if tinfo.IsDir() {
|
||||
@@ -1,5 +1,8 @@
|
||||
// Package file implements a Source that scans local files using the
|
||||
// custom fileglob engine and caches parse results between walks.
|
||||
//
|
||||
// EXPERIMENTAL: this package was promoted from internal/ to enable external
|
||||
// reuse but its API surface may still change without notice in v5.
|
||||
package file
|
||||
|
||||
import (
|
||||
@@ -12,8 +15,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/enix/x509-certificate-exporter/v4/internal/fileglob"
|
||||
"github.com/enix/x509-certificate-exporter/v4/pkg/cert"
|
||||
"github.com/enix/x509-certificate-exporter/v4/pkg/fileglob"
|
||||
)
|
||||
|
||||
// Reader fetches the bytes of one path. The default implementation reads
|
||||
@@ -160,7 +163,7 @@ func (s *Source) runOnce(ctx context.Context, sink cert.Sink, isFirst bool) {
|
||||
// Emit a synthetic bundle so the registry can count error reasons.
|
||||
sink.Upsert(cert.Bundle{
|
||||
Source: cert.SourceRef{
|
||||
Kind: cert.KindFile, Format: "pem",
|
||||
Kind: cert.KindFile, Format: cert.FormatPEM,
|
||||
Location: r.Err.Path, SourceName: s.opts.Name,
|
||||
},
|
||||
Errors: []cert.ItemError{{Index: -1, Reason: r.Err.Reason, Err: r.Err.Err}},
|
||||
@@ -186,7 +189,7 @@ func (s *Source) runOnce(ctx context.Context, sink cert.Sink, isFirst bool) {
|
||||
s.mu.Unlock()
|
||||
for _, p := range stale {
|
||||
ref := cert.SourceRef{
|
||||
Kind: cert.KindFile, Location: p, SourceName: s.opts.Name, Format: "pem",
|
||||
Kind: cert.KindFile, Location: p, SourceName: s.opts.Name, Format: cert.FormatPEM,
|
||||
}
|
||||
sink.Delete(ref)
|
||||
s.log.Debug("file disappeared", "path", p)
|
||||
@@ -246,7 +249,7 @@ func (s *Source) processEntry(ctx context.Context, sink cert.Sink, e fileglob.En
|
||||
s.log.Warn("read error", "path", path, "error", err)
|
||||
sink.Upsert(cert.Bundle{
|
||||
Source: cert.SourceRef{
|
||||
Kind: cert.KindFile, Format: "pem",
|
||||
Kind: cert.KindFile, Format: cert.FormatPEM,
|
||||
Location: path, SourceName: s.opts.Name,
|
||||
},
|
||||
Errors: []cert.ItemError{{Index: -1, Reason: reason, Err: err}},
|
||||
@@ -19,9 +19,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/enix/x509-certificate-exporter/v4/internal/fileglob"
|
||||
"github.com/enix/x509-certificate-exporter/v4/pkg/cert"
|
||||
"github.com/enix/x509-certificate-exporter/v4/pkg/cert/pem"
|
||||
"github.com/enix/x509-certificate-exporter/v4/pkg/fileglob"
|
||||
)
|
||||
|
||||
func nopLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) }
|
||||
@@ -1,6 +1,9 @@
|
||||
// Package k8s implements a Source that watches Kubernetes Secrets (and
|
||||
// optionally ConfigMaps).
|
||||
//
|
||||
// EXPERIMENTAL: this package was promoted from internal/ to enable external
|
||||
// reuse but its API surface may still change without notice in v5.
|
||||
//
|
||||
// Architecture: Secrets and ConfigMaps are observed via a direct paginated
|
||||
// LIST + WATCH loop, not a client-go SharedInformer. The reason is that
|
||||
// client-go's pager.List accumulates every page into a single in-memory
|
||||
@@ -2,6 +2,9 @@
|
||||
// embedded in kubeconfig files (or referenced via certificate-authority/
|
||||
// client-certificate file paths). Mirrors the existing exporter's
|
||||
// behaviour: only the four canonical JSONPath-like locations are read.
|
||||
//
|
||||
// EXPERIMENTAL: this package was promoted from internal/ to enable external
|
||||
// reuse but its API surface may still change without notice in v5.
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
@@ -147,7 +150,7 @@ func (s *Source) scan(path string, sink cert.Sink, seen map[string]struct{}, all
|
||||
if err != nil {
|
||||
*allOK = false
|
||||
s.log.Warn("read kubeconfig", "path", path, "error", err)
|
||||
ref := cert.SourceRef{Kind: cert.KindKubeconfig, Location: path, SourceName: s.opts.Name, Format: "pem"}
|
||||
ref := cert.SourceRef{Kind: cert.KindKubeconfig, Location: path, SourceName: s.opts.Name, Format: cert.FormatPEM}
|
||||
sink.Upsert(cert.Bundle{
|
||||
Source: ref,
|
||||
Errors: []cert.ItemError{{Index: -1, Reason: cert.ReasonReadFailed, Err: err}},
|
||||
@@ -159,7 +162,7 @@ func (s *Source) scan(path string, sink cert.Sink, seen map[string]struct{}, all
|
||||
var doc kubeconfigDoc
|
||||
if err := yaml.Unmarshal(data, &doc); err != nil {
|
||||
*allOK = false
|
||||
ref := cert.SourceRef{Kind: cert.KindKubeconfig, Location: path, SourceName: s.opts.Name, Format: "pem"}
|
||||
ref := cert.SourceRef{Kind: cert.KindKubeconfig, Location: path, SourceName: s.opts.Name, Format: cert.FormatPEM}
|
||||
sink.Upsert(cert.Bundle{
|
||||
Source: ref,
|
||||
Errors: []cert.ItemError{{Index: -1, Reason: cert.ReasonDecodeFailed, Err: err}},
|
||||
@@ -186,7 +189,7 @@ func (s *Source) emit(path, kind, key, b64Data, refPath string, sink cert.Sink,
|
||||
Source: cert.SourceRef{
|
||||
Kind: cert.KindKubeconfig, Location: path,
|
||||
Key: fmt.Sprintf("%s/%s", kind, key),
|
||||
Format: "pem", SourceName: s.opts.Name,
|
||||
Format: cert.FormatPEM, SourceName: s.opts.Name,
|
||||
Attributes: map[string]string{"embedded_kind": kind, "embedded_key": key},
|
||||
},
|
||||
Errors: []cert.ItemError{{Index: -1, Reason: cert.ReasonDecodeFailed, Err: err}},
|
||||
@@ -208,7 +211,7 @@ func (s *Source) emit(path, kind, key, b64Data, refPath string, sink cert.Sink,
|
||||
Source: cert.SourceRef{
|
||||
Kind: cert.KindKubeconfig, Location: path,
|
||||
Key: fmt.Sprintf("%s/%s", kind, key),
|
||||
Format: "pem", SourceName: s.opts.Name,
|
||||
Format: cert.FormatPEM, SourceName: s.opts.Name,
|
||||
Attributes: map[string]string{"embedded_kind": kind, "embedded_key": key},
|
||||
},
|
||||
Errors: []cert.ItemError{{Index: -1, Reason: cert.ReasonReadFailed, Err: err}},
|
||||
@@ -225,7 +228,7 @@ func (s *Source) emit(path, kind, key, b64Data, refPath string, sink cert.Sink,
|
||||
parsed := s.parser.Parse(data, cert.SourceRef{
|
||||
Kind: cert.KindKubeconfig, Location: path,
|
||||
Key: fmt.Sprintf("%s/%s", kind, key),
|
||||
Format: "pem", SourceName: s.opts.Name,
|
||||
Format: cert.FormatPEM, SourceName: s.opts.Name,
|
||||
Attributes: map[string]string{"embedded_kind": kind, "embedded_key": key},
|
||||
}, cert.ParseOptions{})
|
||||
sink.Upsert(parsed)
|
||||
@@ -264,7 +267,7 @@ func decodeKey(k, sourceName string) cert.SourceRef {
|
||||
_ = i
|
||||
r := cert.SourceRef{
|
||||
Kind: cert.KindKubeconfig, Location: path,
|
||||
Format: "pem", SourceName: sourceName,
|
||||
Format: cert.FormatPEM, SourceName: sourceName,
|
||||
}
|
||||
if kind != "" || key != "" {
|
||||
r.Key = fmt.Sprintf("%s/%s", kind, key)
|
||||
Reference in New Issue
Block a user