Seena Fallah 736a2d58ae
feat!: generalize PTR record support from rfc2136 to all providers (#6232)
* feat(metrics): add source wrapper metrics for invalid and deduplicated endpoints

Add GaugeVecMetric.Reset() to clear stale label combinations between cycles.

Introduce invalidEndpoints and deduplicatedEndpoints gauge vectors in the
source wrappers package, partitioned by record_type and source_type. The
dedup source wrapper now tracks rejected (invalid) and de-duplicated
endpoints per collection cycle.

Update the metrics documentation and bump the known metrics count.

Signed-off-by: Seena Fallah <seenafallah@gmail.com>

* feat(source): add PTR source wrapper for automatic reverse DNS

Implement ptrSource, a source wrapper that generates PTR endpoints from
A/AAAA records. The wrapper supports:

- Global default via WithCreatePTR (maps to --create-ptr flag)
- Per-endpoint override via record-type provider-specific property
- Grouping multiple hostnames sharing an IP into a single PTR endpoint
- Skipping wildcard DNS names

Add WithPTRSupported and WithCreatePTR options to the wrapper Config
and wire the PTR wrapper into the WrapSources chain when PTR is in
managed-record-types.

Signed-off-by: Seena Fallah <seenafallah@gmail.com>

* feat(config): add --create-ptr flag and deprecate --rfc2136-create-ptr

Add the generic --create-ptr boolean flag to Config, enabling automatic
PTR record creation for any provider. Add IsPTRSupported() helper that
checks whether PTR is included in --managed-record-types.

Add validation: --create-ptr (or legacy --rfc2136-create-ptr) now
requires PTR in --managed-record-types, preventing misconfiguration.

Mark --rfc2136-create-ptr as deprecated in the flag description.

Signed-off-by: Seena Fallah <seenafallah@gmail.com>

* refactor(rfc2136): remove inline PTR logic in favor of PTR source wrapper

Remove the createPTR field, AddReverseRecord, RemoveReverseRecord, and
GenerateReverseRecord methods from the rfc2136 provider. PTR record
generation is now handled generically by the PTR source wrapper before
records reach the provider.

Update the PTR creation test to supply pre-generated PTR endpoints
(simulating what the source wrapper produces) instead of relying on
the provider to create them internally.

Signed-off-by: Seena Fallah <seenafallah@gmail.com>

* feat(controller): wire PTR source wrapper into buildSource

Pass the top-level Config to buildSource so it can read IsPTRSupported()
and the CreatePTR / RFC2136CreatePTR flags. When PTR is in
managed-record-types, the PTR source wrapper is installed in the
wrapper chain with the combined create-ptr default.

Signed-off-by: Seena Fallah <seenafallah@gmail.com>

* chore(pdns): remove stale comment and fix whitespace

Remove an outdated comment about a single-target-per-tuple assumption
that no longer applies.

Signed-off-by: Seena Fallah <seenafallah@gmail.com>

* docs: add PTR records documentation and update existing guides

Add docs/advanced/ptr-records.md covering the --create-ptr flag,
per-resource annotation overrides, prerequisites, and usage examples.

Update:
- annotations.md: document record-type annotation
- flags.md: add --create-ptr, mark --rfc2136-create-ptr as deprecated
- tutorials/rfc2136.md: point to generic --create-ptr flag
- contributing/source-wrappers.md: add PTR wrapper to the chain
- mkdocs.yml: add PTR Records navigation entry

Signed-off-by: Seena Fallah <seenafallah@gmail.com>

* feat(rfc2136)!: remove rfc2136-create-ptr in favor of create-ptr

Signed-off-by: Seena Fallah <seenafallah@gmail.com>

---------

Signed-off-by: Seena Fallah <seenafallah@gmail.com>
2026-03-30 13:36:16 +05:30

244 lines
6.2 KiB
Go

/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package metrics
import (
"fmt"
"maps"
"slices"
"strings"
"github.com/prometheus/client_golang/prometheus"
)
type MetricRegistry struct {
Registerer prometheus.Registerer
Metrics []*Metric
mName map[string]bool
}
type Metric struct {
Type string
Namespace string
Subsystem string
Name string
Help string
FQDN string
Labels []string
}
type IMetric interface {
Get() *Metric
}
type GaugeMetric struct {
Metric
Gauge prometheus.Gauge
}
func (g GaugeMetric) Get() *Metric {
return &g.Metric
}
type CounterMetric struct {
Metric
Counter prometheus.Counter
}
func (g CounterMetric) Get() *Metric {
return &g.Metric
}
type CounterVecMetric struct {
Metric
CounterVec *prometheus.CounterVec
}
func (g CounterVecMetric) Get() *Metric {
return &g.Metric
}
type GaugeVecMetric struct {
Metric
Gauge prometheus.GaugeVec
}
func (g GaugeVecMetric) Get() *Metric {
return &g.Metric
}
// SetWithLabels sets the value of the Gauge metric for the specified label values.
// All label values are converted to lowercase before being applied.
func (g GaugeVecMetric) SetWithLabels(value float64, lvs ...string) {
g.Gauge.WithLabelValues(toLower(lvs)...).Set(value)
}
// AddWithLabels adds the value to the Gauge metric for the specified label values.
// All label values are converted to lowercase before being applied.
//
// Without Reset(), values accumulate and reset only on process restart.
// Use Reset() + AddWithLabels() pattern for per-cycle counts.
func (g GaugeVecMetric) AddWithLabels(value float64, lvs ...string) {
g.Gauge.WithLabelValues(toLower(lvs)...).Add(value)
}
// Reset removes all label combinations from the gauge vector.
// Use Reset() at the start of each collection cycle followed by
// AddWithLabels() to prevent stale label combinations from persisting.
func (g GaugeVecMetric) Reset() {
g.Gauge.Reset()
}
func NewGaugeWithOpts(opts prometheus.GaugeOpts) GaugeMetric {
opts.Namespace = Namespace
return GaugeMetric{
Metric: Metric{
Type: "gauge",
Name: opts.Name,
FQDN: fmt.Sprintf("%s_%s", opts.Subsystem, opts.Name),
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Help: opts.Help,
Labels: slices.Sorted(maps.Keys(opts.ConstLabels)),
},
Gauge: prometheus.NewGauge(opts),
}
}
// NewGaugedVectorOpts creates a new GaugeVec based on the provided GaugeOpts and
// partitioned by the given label names.
func NewGaugedVectorOpts(opts prometheus.GaugeOpts, labelNames []string) GaugeVecMetric {
opts.Namespace = Namespace
return GaugeVecMetric{
Metric: Metric{
Type: "gauge",
Name: opts.Name,
FQDN: fmt.Sprintf("%s_%s", opts.Subsystem, opts.Name),
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Help: opts.Help,
Labels: append(slices.Sorted(maps.Keys(opts.ConstLabels)), labelNames...),
},
Gauge: *prometheus.NewGaugeVec(opts, labelNames),
}
}
func NewCounterWithOpts(opts prometheus.CounterOpts) CounterMetric {
opts.Namespace = Namespace
return CounterMetric{
Metric: Metric{
Type: "counter",
Name: opts.Name,
FQDN: fmt.Sprintf("%s_%s", opts.Subsystem, opts.Name),
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Help: opts.Help,
Labels: slices.Sorted(maps.Keys(opts.ConstLabels)),
},
Counter: prometheus.NewCounter(opts),
}
}
func NewCounterVecWithOpts(opts prometheus.CounterOpts, labelNames []string) CounterVecMetric {
opts.Namespace = Namespace
return CounterVecMetric{
Metric: Metric{
Type: "counter",
Name: opts.Name,
FQDN: fmt.Sprintf("%s_%s", opts.Subsystem, opts.Name),
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Help: opts.Help,
Labels: append(slices.Sorted(maps.Keys(opts.ConstLabels)), labelNames...),
},
CounterVec: prometheus.NewCounterVec(opts, labelNames),
}
}
type GaugeFuncMetric struct {
Metric
GaugeFunc prometheus.GaugeFunc
}
func (g GaugeFuncMetric) Get() *Metric {
return &g.Metric
}
func NewGaugeFuncMetric(opts prometheus.GaugeOpts) GaugeFuncMetric {
return GaugeFuncMetric{
Metric: Metric{
Type: "gauge",
Name: opts.Name,
FQDN: func() string {
if opts.Subsystem != "" {
return fmt.Sprintf("%s_%s", opts.Subsystem, opts.Name)
}
return opts.Name
}(),
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Help: opts.Help,
Labels: slices.Sorted(maps.Keys(opts.ConstLabels)),
},
GaugeFunc: prometheus.NewGaugeFunc(opts, func() float64 { return 1 }),
}
}
type SummaryVecMetric struct {
Metric
SummaryVec prometheus.SummaryVec
}
func (s SummaryVecMetric) Get() *Metric {
return &s.Metric
}
func (s SummaryVecMetric) SetWithLabels(value float64, labels prometheus.Labels) {
s.SummaryVec.With(labels).Observe(value)
}
func NewSummaryVecWithOpts(opts prometheus.SummaryOpts, labels []string) SummaryVecMetric {
opts.Namespace = Namespace
return SummaryVecMetric{
Metric: Metric{
Type: "summaryVec",
Name: opts.Name,
FQDN: fmt.Sprintf("%s_%s", opts.Subsystem, opts.Name),
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Help: opts.Help,
Labels: append(slices.Sorted(maps.Keys(opts.ConstLabels)), labels...),
},
SummaryVec: *prometheus.NewSummaryVec(opts, labels),
}
}
func PathProcessor(path string) string {
parts := strings.Split(path, "/")
return parts[len(parts)-1]
}
// toLower converts all label values to lowercase.
// The Prometheus maintainers have intentionally avoided magic transformations to keep label handling explicit and predictable.
// We expect consistent casing, normalizing at ingestion is the standard practice.
func toLower(lvs []string) []string {
for i := range lvs {
lvs[i] = strings.ToLower(lvs[i])
}
return lvs
}