mirror of
https://github.com/outbackdingo/talos-cloud-controller-manager.git
synced 2026-01-27 10:20:27 +00:00
feat: transformer functions
Add functions to template executer. Signed-off-by: Serge Logvinov <serge.logvinov@sinextra.dev>
This commit is contained in:
@@ -74,3 +74,23 @@ Example output:
|
||||
```txt
|
||||
talosccm_csr_approval_count{status="approve"} 2
|
||||
```
|
||||
|
||||
### Transformer rules calls
|
||||
|
||||
|Metric name|Metric type|Labels/tags|
|
||||
|-----------|-----------|-----------|
|
||||
|talosccm_transformer_duration_seconds|Histogram|`type`=<type_transformation>|
|
||||
|talosccm_transformer_errors_total|Counter|`type`=<type_transformation>|
|
||||
|
||||
Example output:
|
||||
|
||||
```txt
|
||||
talosccm_transformer_duration_seconds_bucket{type="metadata",le="0.001"} 16
|
||||
talosccm_transformer_duration_seconds_bucket{type="metadata",le="0.01"} 16
|
||||
talosccm_transformer_duration_seconds_bucket{type="metadata",le="0.05"} 16
|
||||
talosccm_transformer_duration_seconds_bucket{type="metadata",le="0.1"} 16
|
||||
talosccm_transformer_duration_seconds_bucket{type="metadata",le="+Inf"} 16
|
||||
talosccm_transformer_duration_seconds_sum{type="metadata"} 0.0012434149999999999
|
||||
talosccm_transformer_duration_seconds_count{type="metadata"} 16
|
||||
talosccm_transformer_errors_total{type="metadata"} 6
|
||||
```
|
||||
|
||||
51
pkg/metrics/metrics_trans.go
Normal file
51
pkg/metrics/metrics_trans.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"k8s.io/component-base/metrics"
|
||||
"k8s.io/component-base/metrics/legacyregistry"
|
||||
)
|
||||
|
||||
// TransformerMetrics contains the metrics for transformer.
|
||||
type TransformerMetrics struct {
|
||||
Duration *metrics.HistogramVec
|
||||
Errors *metrics.CounterVec
|
||||
}
|
||||
|
||||
var transformerMetrics = registerTransformerMetrics()
|
||||
|
||||
// ObserveTransformer records the transformer latency and counts the errors.
|
||||
func (mc *MetricContext) ObserveTransformer(err error) error {
|
||||
transformerMetrics.Duration.WithLabelValues(mc.attributes...).Observe(
|
||||
time.Since(mc.start).Seconds())
|
||||
|
||||
if err != nil {
|
||||
transformerMetrics.Errors.WithLabelValues(mc.attributes...).Inc()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func registerTransformerMetrics() *TransformerMetrics {
|
||||
metrics := &TransformerMetrics{
|
||||
Duration: metrics.NewHistogramVec(
|
||||
&metrics.HistogramOpts{
|
||||
Name: "talosccm_transformer_duration_seconds",
|
||||
Help: "Latency of an Transformer call",
|
||||
Buckets: []float64{.001, .01, .05, .1},
|
||||
}, []string{"type"}),
|
||||
Errors: metrics.NewCounterVec(
|
||||
&metrics.CounterOpts{
|
||||
Name: "talosccm_transformer_errors_total",
|
||||
Help: "Total number of errors for an Transformer call",
|
||||
}, []string{"type"}),
|
||||
}
|
||||
|
||||
legacyregistry.MustRegister(
|
||||
metrics.Duration,
|
||||
metrics.Errors,
|
||||
)
|
||||
|
||||
return metrics
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/siderolabs/talos-cloud-controller-manager/pkg/metrics"
|
||||
"github.com/siderolabs/talos-cloud-controller-manager/pkg/transformer"
|
||||
utilsnet "github.com/siderolabs/talos-cloud-controller-manager/pkg/utils/net"
|
||||
"github.com/siderolabs/talos/pkg/machinery/constants"
|
||||
"github.com/siderolabs/talos/pkg/machinery/resources/network"
|
||||
"github.com/siderolabs/talos/pkg/machinery/resources/runtime"
|
||||
|
||||
@@ -23,9 +24,12 @@ import (
|
||||
"k8s.io/utils/strings/slices"
|
||||
)
|
||||
|
||||
func ipDescovery(nodeIPs []string, ifaces []network.AddressStatusSpec) (publicIPv4s, publicIPv6s []string) {
|
||||
func ipDiscovery(nodeIPs []string, ifaces []network.AddressStatusSpec) (publicIPv4s, publicIPv6s []string) {
|
||||
for _, iface := range ifaces {
|
||||
if iface.LinkName == "kubespan" || iface.LinkName == "lo" {
|
||||
if iface.LinkName == constants.KubeSpanLinkName ||
|
||||
iface.LinkName == constants.SideroLinkName ||
|
||||
iface.LinkName == "lo" ||
|
||||
strings.HasPrefix(iface.LinkName, "dummy") {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -52,7 +56,7 @@ func getNodeAddresses(config *cloudConfig, platform string, features *transforme
|
||||
switch platform {
|
||||
// Those platforms don't expose public IPs information in metadata
|
||||
case "nocloud", "metal", "openstack", "oracle":
|
||||
publicIPv4s, publicIPv6s = ipDescovery(nodeIPs, ifaces)
|
||||
publicIPv4s, publicIPv6s = ipDiscovery(nodeIPs, ifaces)
|
||||
default:
|
||||
for _, iface := range ifaces {
|
||||
if iface.LinkName == "external" {
|
||||
@@ -72,7 +76,7 @@ func getNodeAddresses(config *cloudConfig, platform string, features *transforme
|
||||
}
|
||||
|
||||
if features != nil && features.PublicIPDiscovery {
|
||||
ipv4, ipv6 := ipDescovery(nodeIPs, ifaces)
|
||||
ipv4, ipv6 := ipDiscovery(nodeIPs, ifaces)
|
||||
publicIPv4s = append(publicIPv4s, ipv4...)
|
||||
publicIPv6s = append(publicIPv6s, ipv6...)
|
||||
}
|
||||
|
||||
@@ -97,8 +97,10 @@ func (i *instances) InstanceMetadata(ctx context.Context, node *v1.Node) (*cloud
|
||||
}
|
||||
}
|
||||
|
||||
mct := metrics.NewMetricContext("metadata")
|
||||
|
||||
nodeSpec, err := transformer.TransformNode(i.c.config.Transformations, meta)
|
||||
if err != nil {
|
||||
if mct.ObserveTransformer(err) != nil {
|
||||
return nil, fmt.Errorf("error transforming node: %w", err)
|
||||
}
|
||||
|
||||
|
||||
85
pkg/transformer/functions.go
Normal file
85
pkg/transformer/functions.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package transformer
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var genericMap = map[string]interface{}{
|
||||
// String functions:
|
||||
"upper": strings.ToUpper,
|
||||
"lower": strings.ToLower,
|
||||
"trim": strings.TrimSpace,
|
||||
"trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) },
|
||||
"trimPrefix": func(a, b string) string { return strings.TrimPrefix(b, a) },
|
||||
|
||||
"replace": func(o, n, s string) string { return strings.ReplaceAll(s, o, n) },
|
||||
"regexFind": regexFind,
|
||||
"regexFindString": regexFindString,
|
||||
"regexReplaceAll": regexReplaceAll,
|
||||
|
||||
"contains": func(substr string, str string) bool { return strings.Contains(str, substr) },
|
||||
"hasPrefix": func(substr string, str string) bool { return strings.HasPrefix(str, substr) },
|
||||
"hasSuffix": func(substr string, str string) bool { return strings.HasSuffix(str, substr) },
|
||||
|
||||
// Encoding functions:
|
||||
"b64enc": base64encode,
|
||||
"b64dec": base64decode,
|
||||
}
|
||||
|
||||
// GenericFuncMap returns a copy of the basic function map as a map[string]interface{}.
|
||||
func GenericFuncMap() map[string]interface{} {
|
||||
gfm := make(map[string]interface{}, len(genericMap))
|
||||
for k, v := range genericMap {
|
||||
gfm[k] = v
|
||||
}
|
||||
|
||||
return gfm
|
||||
}
|
||||
|
||||
func regexFindString(regex string, s string, n int) (string, error) {
|
||||
r, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
matches := r.FindStringSubmatch(s)
|
||||
|
||||
if len(matches) < n+1 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return matches[n], nil
|
||||
}
|
||||
|
||||
func regexReplaceAll(regex string, s string, repl string) (string, error) {
|
||||
r, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return r.ReplaceAllString(s, repl), nil
|
||||
}
|
||||
|
||||
func regexFind(regex string, s string) (string, error) {
|
||||
r, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return r.FindString(s), nil
|
||||
}
|
||||
|
||||
func base64encode(v string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(v))
|
||||
}
|
||||
|
||||
func base64decode(v string) (string, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
@@ -110,8 +110,6 @@ func TransformNode(terms []NodeTerm, platformMetadata *runtime.PlatformMetadataS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return node, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +117,7 @@ func TransformNode(terms []NodeTerm, platformMetadata *runtime.PlatformMetadataS
|
||||
}
|
||||
|
||||
func executeTemplate(tmpl string, data interface{}) (string, error) {
|
||||
t, err := template.New("transformer").Parse(tmpl)
|
||||
t, err := template.New("transformer").Funcs(GenericFuncMap()).Parse(tmpl)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse template %q: %w", tmpl, err)
|
||||
}
|
||||
|
||||
@@ -137,8 +137,10 @@ func TestMatch(t *testing.T) {
|
||||
terms: []transformer.NodeTerm{
|
||||
{
|
||||
Name: "my-transformer",
|
||||
Labels: map[string]string{
|
||||
"karpenter.sh/capacity-type": "{{ if .Spot }}spot{{ else }}on-demand{{ end }}",
|
||||
},
|
||||
PlatformMetadata: map[string]string{
|
||||
"Spot": "true",
|
||||
"Zone": "us-west1",
|
||||
},
|
||||
},
|
||||
@@ -146,10 +148,13 @@ func TestMatch(t *testing.T) {
|
||||
metadata: runtime.PlatformMetadataSpec{
|
||||
Platform: "test-platform",
|
||||
Hostname: "test-hostname",
|
||||
Spot: true,
|
||||
},
|
||||
expected: &transformer.NodeSpec{
|
||||
Annotations: map[string]string{},
|
||||
Labels: map[string]string{},
|
||||
Labels: map[string]string{
|
||||
"karpenter.sh/capacity-type": "spot",
|
||||
},
|
||||
},
|
||||
expectedMeta: &runtime.PlatformMetadataSpec{
|
||||
Platform: "test-platform",
|
||||
@@ -164,10 +169,49 @@ func TestMatch(t *testing.T) {
|
||||
{
|
||||
Name: "my-transformer",
|
||||
PlatformMetadata: map[string]string{
|
||||
"Hostname": "fake-hostname",
|
||||
"spot": "true",
|
||||
"zoNe": "us-west1",
|
||||
"wrong": "value",
|
||||
"Hostname": "fake-hostname",
|
||||
"spot": "true",
|
||||
"zoNe": "us-west1",
|
||||
"wrong": "value",
|
||||
"InstanceType": `{{ regexFindString "^type-([a-z0-9]+)-(.*)$" .Hostname 1 }}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: runtime.PlatformMetadataSpec{
|
||||
Platform: "test-platform",
|
||||
Hostname: "type-c1m5-hostname",
|
||||
},
|
||||
expected: &transformer.NodeSpec{
|
||||
Annotations: map[string]string{},
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
expectedMeta: &runtime.PlatformMetadataSpec{
|
||||
Platform: "test-platform",
|
||||
Hostname: "type-c1m5-hostname",
|
||||
Spot: true,
|
||||
Zone: "us-west1",
|
||||
InstanceType: "c1m5",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Multiple transformers",
|
||||
terms: []transformer.NodeTerm{
|
||||
{
|
||||
Name: "first-rule",
|
||||
Annotations: map[string]string{
|
||||
"first-annotation": "first-value",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"karpenter.sh/capacity-type": "on-demand",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "second-rule",
|
||||
Labels: map[string]string{
|
||||
"karpenter.sh/capacity-type": "spot",
|
||||
},
|
||||
PlatformMetadata: map[string]string{
|
||||
"Zone": "us-west1",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -176,13 +220,16 @@ func TestMatch(t *testing.T) {
|
||||
Hostname: "test-hostname",
|
||||
},
|
||||
expected: &transformer.NodeSpec{
|
||||
Annotations: map[string]string{},
|
||||
Labels: map[string]string{},
|
||||
Annotations: map[string]string{
|
||||
"first-annotation": "first-value",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"karpenter.sh/capacity-type": "spot",
|
||||
},
|
||||
},
|
||||
expectedMeta: &runtime.PlatformMetadataSpec{
|
||||
Platform: "test-platform",
|
||||
Hostname: "test-hostname",
|
||||
Spot: true,
|
||||
Zone: "us-west1",
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user