diff --git a/go.mod b/go.mod index 3903e2f74..66050b7c5 100644 --- a/go.mod +++ b/go.mod @@ -12,12 +12,13 @@ require ( github.com/hairyhenderson/toml v0.4.2-0.20210923231440-40456b8e66cf github.com/itchyny/gojq v0.12.13 github.com/pkg/errors v0.9.1 + github.com/robertkrimen/otto v0.2.1 github.com/stretchr/testify v1.8.4 github.com/ugorji/go/codec v1.2.11 golang.org/x/text v0.11.0 golang.org/x/tools v0.7.0 gotest.tools/v3 v3.4.0 - k8s.io/apimachinery v0.27.3 + k8s.io/apimachinery v0.26.4 sigs.k8s.io/yaml v1.3.0 ) @@ -36,7 +37,6 @@ require ( github.com/gosimple/unidecode v1.0.1 // indirect github.com/itchyny/timefmt-go v0.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kr/pretty v0.3.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -50,9 +50,10 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.27.3 // indirect + k8s.io/api v0.26.4 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/utils v0.0.0-20230711102312-30195339c3c7 // indirect layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf // indirect diff --git a/go.sum b/go.sum index 972cfb1ad..6f248a36f 100644 --- a/go.sum +++ b/go.sum @@ -4,7 +4,6 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0 github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -44,22 +43,19 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0= +github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -122,9 +118,11 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= +gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -133,10 +131,10 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -k8s.io/api v0.27.3 h1:yR6oQXXnUEBWEWcvPWS0jQL575KoAboQPfJAuKNrw5Y= -k8s.io/api v0.27.3/go.mod h1:C4BNvZnQOF7JA/0Xed2S+aUyJSfTGkGFxLXz9MnpIpg= -k8s.io/apimachinery v0.27.3 h1:Ubye8oBufD04l9QnNtW05idcOe9Z3GQN8+7PqmuVcUM= -k8s.io/apimachinery v0.27.3/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= +k8s.io/api v0.26.4 h1:qSG2PmtcD23BkYiWfoYAcak870eF/hE7NNYBYavTT94= +k8s.io/api v0.26.4/go.mod h1:WwKEXU3R1rgCZ77AYa7DFksd9/BAIKyOmRlbVxgvjCk= +k8s.io/apimachinery v0.26.4 h1:rZccKdBLg9vP6J09JD+z8Yr99Ce8gk3Lbi9TCx05Jzs= +k8s.io/apimachinery v0.26.4/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/utils v0.0.0-20230711102312-30195339c3c7 h1:ZgnF1KZsYxWIifwSNZFZgNtWE89WI5yiP5WwlfDoIyc= diff --git a/js/js.go b/js/js.go new file mode 100644 index 000000000..dda599866 --- /dev/null +++ b/js/js.go @@ -0,0 +1,18 @@ +package js + +import ( + _ "embed" + + "github.com/robertkrimen/otto/registry" +) + +//go:embed k8s.js +var k8s string + +//go:embed shared.js +var shared string + +func init() { + _ = registry.Register(func() string { return k8s }) + _ = registry.Register(func() string { return shared }) +} diff --git a/js/k8s.js b/js/k8s.js new file mode 100644 index 000000000..40a6b6486 --- /dev/null +++ b/js/k8s.js @@ -0,0 +1,363 @@ +k8s = { + conditions: { + getMessage: function(v) { + return v.healthStatus.message.trim(); + }, + getError: function(v) { + active = [] + if (v.status == null) { + return "No status found" + } + status = v.status + if (status.conditions == null) { + return "no conditions found" + } + status.conditions.forEach(function(state) { + if (state.status == "False") { + active.push(state) + } + }) + active.sort(function(a, b) { a.lastTransitionTime > b.lastTransitionTime && 1 || -1 }) + errorMessage = "" + active.forEach(function(state) { + if (errorMessage != "") { + errorMessage += ', ' + } + errorMessage += state.lastTransitionTime + ': ' + state.type + ' is ' + state.reason + if (state.message != null) { + errorMessage += ' with ' + state.message + } + }) + return errorMessage + }, + isReady: function(v) { + return v.healthStatus.status.toLowerCase() === "healthy" ? true : false; + }, + }, + getAlertName: function(v) { + name = v.alertname + if (startsWith(v.alertname, "KubeDeployment")) { + return name + "/" + v.deployment + } + if (startsWith(v.alertname, "KubePod") || startsWith(v.alertname, "ExcessivePod")) { + return name + "/" + v.pod + } + if (startsWith(v.alertname, "KubeDaemonSet")) { + return name + "/" + v.daemonset + } + if (v.alertname == "CertManagerInvalidCertificate") { + return name + "/" + v.name + } + if (startsWith(v.alertname, "KubeStatefulSet")) { + return name + "/" + v.statefulset + } + if (startsWith(v.alertname, "Node") || startsWith(v.alertname, "KubeNode")) { + return name + "/" + v.node + } + }, + getAlertLabels: function(v) { + function ignoreLabel(k) { + return k == "severity" || k == "job" || k == "alertname" || k == "alertstate" || k == "__name__" || k == "value" || k == "namespace" + } + function parseLabels(v) { + results = {} + v.namespace = v.namespace || v.exported_namespace + v.instance = v.exported_instance + delete (v.exported_namespace) + delete (v.exported_instance) + for (k in v) { + newKey = k.replace("label_", "") + newKey = newKey.replace("apps_kubernetes_io_", "apps/kubernetes.io/") + results[newKey] = v[k] + delete v[k] + } + return results + } + v = parseLabels(v) + if (v.alertname == "CertManagerInvalidCertificate") { + delete (v.condition) + delete (v.container) + delete (v.endpoint) + delete (v.instance) + delete (v.service) + delete (v.pod) + } + return v + }, + getAlerts: function(results) { + function ignoreLabel(k) { + return k == "severity" || k == "job" || k == "alertname" || k == "alertstate" || k == "__name__" || k == "value" || k == "namespace" + } + function getLabels(v) { + s = "" + for (k in v) { + if (ignoreLabel(k)) { + continue + } + if (s != "") { + s += " " + } + s += k + "=" + v[k] + } + return s + } + function getLabelMap(v) { + out = {} + for (k in v) { + if (ignoreLabel(k)) { + continue + } + out[k] = v[k] + "" + } + return out + } + var out = _.map(results, function(v) { + v = k8s.getAlertLabels(v) + return { + pass: v.severity == "none", + namespace: v.namespace, + labels: getLabelMap(v), + message: getLabels(v), + name: k8s.getAlertName(v) + } + }) + JSON.stringify(out) + }, + getNodeMetrics: function(results) { + components = [] + for (i in results) { + node = results[i].Object + components.push({ + name: k8s.getNodeName(node.metadata.name), + properties: [ + { + name: "cpu", + value: fromMillicores(node.usage.cpu) + }, + { + name: "memory", + value: fromSI(node.usage.memory) + } + ] + }) + } + return components + }, + + getPodMetrics: function(results) { + components = [] + for (i in results) { + node = results[i].Object + cpu = 0 + mem = 0 + for (j in node.containers) { + cpu += fromMillicores(node.containers[j].usage.cpu) + mem += fromSI(node.containers[j].usage.memory) + } + components.push({ + name: node.metadata.name, + properties: [ + { + name: "cpu", + value: cpu + }, + { + name: "memory", + value: mem + } + ] + }) + } + return components + }, + + filterLabels: function(labels) { + var filtered = {} + for (label in labels) { + if (endsWith(label, "-hash")) { + continue + } + filtered[label] = labels[label] + } + return filtered + }, + getNodeName: function(name) { + return name.replace(".compute.internal", "") + }, + getPodTopology: function(results) { + var pods = [] + for (i in results) { + pod = results[i].Object + labels = k8s.filterLabels(pod.metadata.labels) + labels.namespace = pod.metadata.namespace + pod_mem_limit = 0 + pod_cpu_limit = 0 + if (pod.spec.containers[0].resources.limits) { + pod_mem_limit = fromSI(pod.spec.containers[0].resources.limits.memory || 0) + pod_cpu_limit = fromMillicores(pod.spec.containers[0].resources.limits.cpu || 0) + } + if (pod_mem_limit === 0) { + pod_mem_limit = null + } + if (pod_cpu_limit === 0) { + pod_cpu_limit = null + } + + _pod = { + name: pod.metadata.name, + namespace: pod.metadata.namespace, + type: "KubernetesPod", + labels: labels, + logs: [ + { name: "Kubernetes", type: "KubernetesPod" }, + ], + external_id: pod.metadata.namespace + "/" + pod.metadata.name, + configs: [ + { + name: pod.metadata.name, + type: "Kubernetes::Pod", + } + ], + properties: [ + { + name: "version", + text: pod.spec.containers[0].image.split(':')[1], + headline: true + }, + { + name: "cpu", + headline: true, + unit: "millicores", + max: pod_cpu_limit, + }, + { + name: "memory", + headline: true, + unit: "bytes", + max: pod_mem_limit, + }, + { + name: "node", + text: pod.spec.nodeName + }, + { + name: "created", + text: pod.metadata.creationTimestamp, + }, + { + name: "ip", + text: pod.status.IPs != null && pod.status.IPs.length > 0 ? pod.status.IPs[0].ip : "" + } + ] + } + + if (k8s.conditions.isReady(pod)) { + _pod.status = "healthy" + } else { + _pod.status = "unhealthy" + _pod.status_reason = k8s.conditions.getMessage(pod) + } + + pods.push(_pod) + } + return pods + }, + + + getNodeTopology: function(results) { + var nodes = [] + for (i in results) { + node = results[i].Object + _node = { + name: k8s.getNodeName(node.metadata.name), + type: "KubernetesNode", + external_id: node.metadata.name, + labels: k8s.filterLabels(node.metadata.labels), + selectors: [{ + name: "", + labelSelector: "", + fieldSelector: "node=" + node.metadata.name + }], + logs: [ + { name: "Kubernetes", type: "KubernetesNode" }, + ], + configs: [ + { + name: node.metadata.name, + type: "Kubernetes::Node", + } + ], + properties: [ + { + name: "cpu", + min: 0, + unit: "millicores", + headline: true, + max: fromMillicores(node.status.allocatable.cpu) + }, + { + name: "memory", + unit: "bytes", + headline: true, + max: fromSI(node.status.allocatable.memory) + }, + { + name: "ephemeral-storage", + unit: "bytes", + max: fromSI(node.status.allocatable["ephemeral-storage"]) + }, + { + name: "instance-type", + text: node.metadata.labels["beta.kubernetes.io/instance-type"] + }, + { + name: "zone", + text: node.metadata.labels["topology.kubernetes.io/zone"] + }, + { + name: "ami", + text: node.metadata.labels["eks.amazonaws.com/nodegroup-image"] + } + ] + } + internalIP = _.find(node.status.addresses, function(a) { a.type == "InternalIP" }) + if (internalIP != null) { + _node.properties.push({ + name: "ip", + text: internalIP.address + }) + } + externalIP = _.find(node.status.addresses, function(a) { a.type == "ExternalIP" }) + if (externalIP != null) { + _node.properties.push({ + name: "externalIp", + text: externalIP.address + }) + } + _node.properties.push({ + name: "os", + text: node.status.nodeInfo.osImage + "(" + node.status.nodeInfo.architecture + ")" + }) + for (k in node.status.nodeInfo) { + if (k == "bootID" || k == "machineID" || k == "systemUUID" || k == "architecture" || k == "operatingSystem" || k == "osImage") { + continue + } + v = node.status.nodeInfo[k] + _node.properties.push({ + name: k.replace("Version", ""), + text: v + }) + } + + if (k8s.conditions.isReady(node)) { + _node.status = "healthy" + } else { + _node.status = "unhealthy" + _node.status_reason = k8s.conditions.getMessage(node) + } + + nodes.push(_node) + } + return nodes + } +} diff --git a/js/shared.js b/js/shared.js new file mode 100644 index 000000000..a13882d41 --- /dev/null +++ b/js/shared.js @@ -0,0 +1,41 @@ +function fromMillicores(mc) { + if (typeof (mc) == Number) { + return mc * 1000 + } + mc = mc.toString() + if (mc.substring(mc.length - 1, mc.length) == "m") { + return Number(mc.substring(0, mc.length - 1)) + } + return Number(mc) +} + +function fromSI(si) { + if (typeof (si) == Number) { + return si + } + si = si.toString() + unit = si.substring(si.length - 2, si.length) + if (unit == "Ki") { + return Number(si.substring(0, si.length - 2)) * 1024 + } else if (unit === "Mi") { + return Number(si.substring(0, si.length - 2)) * 1024 * 1024 + } else if (unit === "Gi") { + return Number(si.substring(0, si.length - 2)) * 1024 * 1024 * 1024 + } + return Number(si) +} + +function startsWith(s, search, rawPos) { + if (s == null) { + return false; + } + var pos = rawPos > 0 ? rawPos | 0 : 0; + return s.substring(pos, pos + search.length) === search; +} + +function endsWith(s, search) { + if (s == null) { + return false; + } + return s.indexOf(search) === s.length - search.length; +} diff --git a/template.go b/template.go index 726d8a712..cde20b790 100644 --- a/template.go +++ b/template.go @@ -5,11 +5,16 @@ import ( "context" "encoding/json" "fmt" + "os" "strings" gotemplate "text/template" "github.com/flanksource/gomplate/v3/funcs" + _ "github.com/flanksource/gomplate/v3/js" "github.com/google/cel-go/cel" + "github.com/robertkrimen/otto" + "github.com/robertkrimen/otto/registry" + _ "github.com/robertkrimen/otto/underscore" ) var funcMap gotemplate.FuncMap @@ -19,9 +24,9 @@ func init() { } type Template struct { - Template string `yaml:"template,omitempty" json:"template,omitempty"` + Template string `yaml:"template,omitempty" json:"template,omitempty"` // Go template JSONPath string `yaml:"jsonPath,omitempty" json:"jsonPath,omitempty"` - Expression string `yaml:"expr,omitempty" json:"expr,omitempty"` + Expression string `yaml:"expr,omitempty" json:"expr,omitempty"` // A cel-go expression Javascript string `yaml:"javascript,omitempty" json:"javascript,omitempty"` } @@ -29,31 +34,26 @@ func (t Template) IsEmpty() bool { return t.Template == "" && t.JSONPath == "" && t.Expression == "" && t.Javascript == "" } -func RunTemplate(environment map[string]interface{}, template Template) (string, error) { +func RunTemplate(environment map[string]any, template Template) (string, error) { // javascript if template.Javascript != "" { - // // FIXME: whitelist allowed files - // vm := otto.New() - // for k, v := range environment { - // if err := vm.Set(k, v); err != nil { - // return "", errors.Wrapf(err, "error setting %s", k) - // } - // } - - // if err != nil { - // return "", errors.Wrapf(err, "error setting findConfigItem function") - // } - - // out, err := vm.Run(template.Javascript) - // if err != nil { - // return "", errors.Wrapf(err, "failed to run javascript") - // } - - // if s, err := out.ToString(); err != nil { - // return "", errors.Wrapf(err, "failed to cast output to string") - // } else { - // return s, nil - // } + vm := otto.New() + for k, v := range environment { + if err := vm.Set(k, v); err != nil { + return "", fmt.Errorf("error setting %s", k) + } + } + + out, err := vm.Run(template.Javascript) + if err != nil { + return "", fmt.Errorf("failed to run javascript: %v", err) + } + + if s, err := out.ToString(); err != nil { + return "", fmt.Errorf("failed to cast output to string: %v", err) + } else { + return s, nil + } } // gotemplate @@ -64,9 +64,9 @@ func RunTemplate(environment map[string]interface{}, template Template) (string, return "", err } - // marshal data from interface{} to map[string]interface{} + // marshal data from any to map[string]any data, _ := json.Marshal(environment) - unstructured := make(map[string]interface{}) + unstructured := make(map[string]any) if err := json.Unmarshal(data, &unstructured); err != nil { return "", err } @@ -108,3 +108,16 @@ func RunTemplate(environment map[string]interface{}, template Template) (string, return "", nil } + +// LoadSharedLibrary loads a shared library for Otto +func LoadSharedLibrary(source string) error { + source = strings.TrimSpace(source) + data, err := os.ReadFile(source) + if err != nil { + return fmt.Errorf("failed to read shared library %s: %s", source, err) + } + + fmt.Printf("Loaded %s: \n%s\n", source, string(data)) + registry.Register(func() string { return string(data) }) + return nil +} diff --git a/template_test.go b/template_test.go index bea119baf..0c9a677c7 100644 --- a/template_test.go +++ b/template_test.go @@ -8,6 +8,36 @@ import ( "github.com/stretchr/testify/assert" ) +func TestJavascript(t *testing.T) { + tests := []struct { + env map[string]interface{} + js string + out string + }{ + {map[string]interface{}{"x": 5, "y": 3}, "x + y", "8"}, + {map[string]interface{}{"str": "Hello, World!"}, "str", "Hello, World!"}, + {map[string]interface{}{"numbers": []int{1, 2, 3, 4, 5}}, "_.reduce(numbers, function(memo, num){ return memo + num; }, 0)", "15"}, + {map[string]interface{}{"numbers": []int{4, 2, 55}}, `_.max(numbers)`, "55"}, + {map[string]interface{}{"arr": []int{1, 2, 1, 4, 1, 2}}, "_.uniq(arr)", "1,2,4"}, + {map[string]interface{}{"numbers": []int{4, 2, 55}}, `_.max(numbers)`, "55"}, + {map[string]interface{}{"arr": []int{1, 2, 1, 4, 1, 2}}, "_.uniq(arr)", "1,2,4"}, + {map[string]interface{}{"x": "1Ki"}, "fromSI(x)", "1024"}, + {map[string]interface{}{"x": "2m"}, "fromMillicores(x)", "2"}, + {map[string]interface{}{"name": "mission.compute.internal"}, "k8s.getNodeName(name)", "mission"}, + {map[string]interface{}{"msg": map[string]any{"healthStatus": map[string]string{"status": "HEALTHY"}}}, "k8s.conditions.isReady(msg)", "true"}, + } + + for _, tc := range tests { + t.Run(tc.js, func(t *testing.T) { + out, err := RunTemplate(tc.env, Template{ + Javascript: tc.js, + }) + assert.ErrorIs(t, err, nil) + assert.Equal(t, tc.out, out) + }) + } +} + func TestGomplate(t *testing.T) { tests := []struct { env map[string]interface{}