From 386958d6af99d526fd14d000ba3ec1d7fc0117af Mon Sep 17 00:00:00 2001 From: Serge Logvinov Date: Wed, 8 May 2024 00:03:59 +0300 Subject: [PATCH] feat: transformer functions Add functions to template executer. Signed-off-by: Serge Logvinov --- docs/metrics.md | 20 +++++++ pkg/metrics/metrics_trans.go | 51 +++++++++++++++++ pkg/talos/helper.go | 12 ++-- pkg/talos/instances.go | 4 +- pkg/transformer/functions.go | 85 +++++++++++++++++++++++++++++ pkg/transformer/transformer.go | 4 +- pkg/transformer/transformer_test.go | 63 ++++++++++++++++++--- 7 files changed, 223 insertions(+), 16 deletions(-) create mode 100644 pkg/metrics/metrics_trans.go create mode 100644 pkg/transformer/functions.go diff --git a/docs/metrics.md b/docs/metrics.md index f799704..b4aad38 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -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`=| +|talosccm_transformer_errors_total|Counter|`type`=| + +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 +``` diff --git a/pkg/metrics/metrics_trans.go b/pkg/metrics/metrics_trans.go new file mode 100644 index 0000000..ef3aa5b --- /dev/null +++ b/pkg/metrics/metrics_trans.go @@ -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 +} diff --git a/pkg/talos/helper.go b/pkg/talos/helper.go index 6a60cf4..d015189 100644 --- a/pkg/talos/helper.go +++ b/pkg/talos/helper.go @@ -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...) } diff --git a/pkg/talos/instances.go b/pkg/talos/instances.go index 51ab358..c5127f2 100644 --- a/pkg/talos/instances.go +++ b/pkg/talos/instances.go @@ -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) } diff --git a/pkg/transformer/functions.go b/pkg/transformer/functions.go new file mode 100644 index 0000000..b894957 --- /dev/null +++ b/pkg/transformer/functions.go @@ -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 +} diff --git a/pkg/transformer/transformer.go b/pkg/transformer/transformer.go index 21032a6..e564350 100644 --- a/pkg/transformer/transformer.go +++ b/pkg/transformer/transformer.go @@ -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) } diff --git a/pkg/transformer/transformer_test.go b/pkg/transformer/transformer_test.go index 8ec4d49..45f9f6d 100644 --- a/pkg/transformer/transformer_test.go +++ b/pkg/transformer/transformer_test.go @@ -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,25 +169,67 @@ 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: "test-hostname", + 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", + }, + }, + }, + metadata: runtime.PlatformMetadataSpec{ + Platform: "test-platform", + Hostname: "test-hostname", + }, + expected: &transformer.NodeSpec{ + 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", }, },