feat: transformer functions

Add functions to template executer.

Signed-off-by: Serge Logvinov <serge.logvinov@sinextra.dev>
This commit is contained in:
Serge Logvinov
2024-05-08 00:03:59 +03:00
parent 0e8728c11d
commit 386958d6af
7 changed files with 224 additions and 17 deletions

View File

@@ -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
```

View 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
}

View File

@@ -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...)
}

View File

@@ -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)
}

View 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
}

View File

@@ -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)
}

View File

@@ -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",
},
},