From 7f20487cae7ed2a4eed5c431e802b93f2518ae56 Mon Sep 17 00:00:00 2001 From: Aline Abler Date: Fri, 23 Feb 2024 16:38:54 +0100 Subject: [PATCH] Initial implementation --- .github/ISSUE_TEMPLATE/bug_report.yml | 49 +++++++ .github/ISSUE_TEMPLATE/config.yml | 6 + .github/ISSUE_TEMPLATE/feature_request.yml | 67 +++++++++ .github/PULL_REQUEST_TEMPLATE.md | 19 +++ .github/changelog-configuration.json | 42 ++++++ .github/common.libsonnet | 33 +++++ .github/workflows/build.yml | 32 ++++ .github/workflows/lint.yml | 27 ++++ .github/workflows/release.yml | 63 ++++++++ .github/workflows/test.yml | 32 ++++ .gitignore | 18 +++ .goreleaser.yml | 63 ++++++++ CODEOWNERS | 1 + Dockerfile | 20 +++ LICENSE | 29 ++++ Makefile | 75 ++++++++++ Makefile.vars.mk | 21 +++ README.md | 19 +++ go.mod | 20 +++ go.sum | 33 +++++ jsonnet/promtest.libsonnet | 33 +++++ main.go | 161 +++++++++++++++++++++ pkg/promjsonnet/querycheck.go | 56 +++++++ 23 files changed, 919 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/changelog-configuration.json create mode 100644 .github/common.libsonnet create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .goreleaser.yml create mode 100644 CODEOWNERS create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 Makefile.vars.mk create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 jsonnet/promtest.libsonnet create mode 100644 main.go create mode 100644 pkg/promjsonnet/querycheck.go diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..e866d86 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,49 @@ +name: 🐛 Bug report +description: Create a report to help us improve 🎉 +labels: + - bug + +body: + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional Context + description: Add any other context about the problem here. + validations: + required: false + - type: textarea + id: logs + attributes: + label: Logs + description: If applicable, add logs to help explain the bug. + render: shell + validations: + required: false + - type: textarea + id: expected_behavior + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + - type: textarea + id: reproduction_steps + attributes: + label: Steps To Reproduce + description: Describe steps to reproduce the behavior + validations: + required: false + - type: textarea + id: version + attributes: + label: Versions + placeholder: v1.2.3 [, Kubernetes 1.21] + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..d224e95 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +blank_issues_enabled: false + +contact_links: + - name: ❓ Question + url: https://github.com/appuio/promtool-jsonnet/discussions + about: Ask or discuss with us, we're happy to help 🙋 diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..401c7aa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,67 @@ +name: 🚀 Feature request +description: Suggest an idea for this project 💡 +labels: + - enhancement + +body: + - type: textarea + id: summary + attributes: + label: Summary + value: | + **As** role name\ + **I want** a feature or functionality\ + **So that** I get certain business value + description: This user story helps us to quickly understand what this idea is about. + validations: + required: true + - type: textarea + id: context + attributes: + label: Context + description: Add more information here. You are completely free regarding form and length. + validations: + required: true + - type: textarea + id: out_of_scope + attributes: + label: Out of Scope + description: List aspects that are explicitly not part of this feature + placeholder: | + - ... + - ... + - ... + validations: + required: false + - type: textarea + id: links + attributes: + label: Further links + description: URLs of relevant Git repositories, PRs, Issues, etc. + placeholder: | + - #567 + - https://kubernetes.io/docs/reference/ + validations: + required: false + - type: textarea + id: acceptance_criteria + attributes: + label: Acceptance Criteria + description: If you already have ideas what the detailed requirements are, please list them below in given-when-then expressions. + placeholder: | + - Given a precondition, when an action happens, then expect a result + + ```gherkin + Given a precondition + When an action happens + Then expect a result + ``` + validations: + required: false + - type: textarea + id: implementation_idea + attributes: + label: Implementation Ideas + description: If applicable, shortly list possible implementation ideas + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..c6d8d79 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ +## Summary + +* Short summary of what's included in the PR +* Give special note to breaking changes + +## Checklist + +- [ ] Categorize the PR by setting a good title and adding one of the labels: + `bug`, `enhancement`, `documentation`, `change`, `breaking`, `dependency` + as they show up in the changelog +- [ ] Update tests. +- [ ] Link this PR to related issues. + + diff --git a/.github/changelog-configuration.json b/.github/changelog-configuration.json new file mode 100644 index 0000000..8c93e7b --- /dev/null +++ b/.github/changelog-configuration.json @@ -0,0 +1,42 @@ +{ + "pr_template": "- ${{TITLE}} (#${{NUMBER}})", + "categories": [ + { + "title": "## 🚀 Features", + "labels": [ + "enhancement" + ] + }, + { + "title": "## 🛠️ Minor Changes", + "labels": [ + "change" + ] + }, + { + "title": "## 🔎 Breaking Changes", + "labels": [ + "breaking" + ] + }, + { + "title": "## 🐛 Fixes", + "labels": [ + "bug" + ] + }, + { + "title": "## 📄 Documentation", + "labels": [ + "documentation" + ] + }, + { + "title": "## 🔗 Dependency Updates", + "labels": [ + "dependency" + ] + } + ], + "template": "${{CATEGORIZED_COUNT}} changes since ${{FROM_TAG}}\n\n${{CHANGELOG}}" +} diff --git a/.github/common.libsonnet b/.github/common.libsonnet new file mode 100644 index 0000000..a6a2664 --- /dev/null +++ b/.github/common.libsonnet @@ -0,0 +1,33 @@ +local formatLabels = function(labels) + local lf = std.join(', ', std.map(function(l) '%s="%s"' % [l, labels[l]], std.objectFields(labels))); + '{%s}' % [lf]; + +// returns a series object with correctly formatted labels. +// labels can be modified post creation using `_labels`. +local series = function(name, labels, values) { + _name:: name, + _labels:: labels, + series: self._name + formatLabels(self._labels), + values: values, +}; + +// returns a test object with the given series and samples. Sample interval is 30s +// the evaluation time is set one hour in the future since all our queries operate on a 1h window +local test = function(name, series, query, samples, interval='30s', eval_time='1h') { + name: name, + interval: interval, + input_series: if std.isArray(series) then series else std.objectValues(series), + promql_expr_test: [ + { + expr: query, + eval_time: eval_time, + exp_samples: if std.isArray(samples) then samples else [samples], + }, + ], +}; + +{ + series: series, + formatLabels: formatLabels, + test: test, +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..87a5b09 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,32 @@ +name: Build + +on: + pull_request: + branches: + - master + push: + branches: + - master + +jobs: + go: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Determine Go version from go.mod + run: echo "GO_VERSION=$(grep "go 1." go.mod | cut -d " " -f 2)" >> $GITHUB_ENV + + - uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Run build + run: make build diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..64bb875 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: Lint + +on: + pull_request: {} + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Determine Go version from go.mod + run: echo "GO_VERSION=$(grep "go 1." go.mod | cut -d " " -f 2)" >> $GITHUB_ENV + + - uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Run linters + run: make lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ad09554 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,63 @@ +name: Release + +on: + push: + tags: + - "*" + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Determine Go version from go.mod + run: echo "GO_VERSION=$(grep "go 1." go.mod | cut -d " " -f 2)" >> $GITHUB_ENV + + - uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build changelog from PRs with labels + id: build_changelog + uses: mikepenz/release-changelog-builder-action@v3 + with: + configuration: ".github/changelog-configuration.json" + # PreReleases still get a changelog, but the next full release gets a diff since the last full release, + # combining possible changelogs of all previous PreReleases in between. + # PreReleases show a partial changelog since last PreRelease. + ignorePreReleases: "${{ !contains(github.ref, '-rc') }}" + outputFile: .github/release-notes.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish releases + uses: goreleaser/goreleaser-action@v4 + with: + args: release --release-notes .github/release-notes.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..03543b7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +name: Test + +on: + pull_request: + branches: + - master + push: + branches: + - master + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Determine Go version from go.mod + run: echo "GO_VERSION=$(grep "go 1." go.mod | cut -d " " -f 2)" >> $GITHUB_ENV + + - uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Run tests + run: make test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..301a54e --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Goreleaser +dist/ +.github/release-notes.md + +# Build +promtool-jsonnet +*.out + +# Docs +.cache/ +.public/ +node_modules/ + +# IDEs +.vscode/ + +# Generated Manifests +manifests/grafana/grafana.yaml diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..fe5f770 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,63 @@ +# This is an example goreleaser.yaml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com + +builds: +- env: + - CGO_ENABLED=0 # this is needed otherwise the Docker image build is faulty + goarch: + - amd64 + - arm64 + goos: + - linux + - darwin + goarm: + - 8 + +archives: +- format: binary + name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" + files: + - jsonnet + +checksum: + name_template: "checksums.txt" + +snapshot: + name_template: "{{ incpatch .Version }}-snapshot" + +dockers: +- goarch: amd64 + use: buildx + build_flag_templates: + - "--platform=linux/amd64" + image_templates: + - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:v{{ .Version }}-amd64" + extra_files: + - jsonnet + +- goarch: arm64 + use: buildx + build_flag_templates: + - "--platform=linux/arm64/v8" + image_templates: + - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:v{{ .Version }}-arm64" + extra_files: + - jsonnet + - .cache/prometheus + +docker_manifests: + ## ghcr.io + # For prereleases, updating `latest` does not make sense. + # Only the image for the exact version should be pushed. + - name_template: "{{ if not .Prerelease }}{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:latest{{ end }}" + image_templates: + - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:v{{ .Version }}-amd64" + - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:v{{ .Version }}-arm64" + + - name_template: "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:v{{ .Version }}" + image_templates: + - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:v{{ .Version }}-amd64" + - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:v{{ .Version }}-arm64" + +release: + prerelease: auto diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..4aecf21 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @appuio/aldebaran-tech diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..918dfd2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM docker.io/library/alpine:3.19 as runtime + +RUN \ + apk add --update --no-cache \ + bash \ + coreutils \ + curl \ + ca-certificates \ + tzdata + +ENTRYPOINT ["promtool-jsonnet"] +COPY promtool-jsonnet /usr/bin/ + +COPY .cache/prometheus /usr/lib/prometheus +ENV PJ_PROMTOOL_PATH="/usr/lib/prometheus/promtool" + +COPY jsonnet /usr/lib/jsonnet +ENV PJ_JSONNET_PATH="/usr/lib/jsonnet" + +USER 65536:0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..30bfa85 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022, VSHN AG +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3ffd255 --- /dev/null +++ b/Makefile @@ -0,0 +1,75 @@ +# Set Shell to bash, otherwise some targets fail with dash/zsh etc. +SHELL := /bin/bash + +# Disable built-in rules +MAKEFLAGS += --no-builtin-rules +MAKEFLAGS += --no-builtin-variables +.SUFFIXES: +.SECONDARY: +.DEFAULT_GOAL := help + +# General variables +include Makefile.vars.mk + +# Following includes do not print warnings or error if files aren't found +# Optional Documentation module. +-include docs/antora-preview.mk docs/antora-build.mk +-include Makefile.compose.mk + +.PHONY: help +help: ## Show this help + @grep -E -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +.PHONY: build +build: build-bin build-docker ## All-in-one build + +.PHONY: build-bin +build-bin: export CGO_ENABLED = 0 +build-bin: fmt vet ## Build binary + @go build -o $(BIN_FILENAME) github.com/appuio/promtool-jsonnet + +.PHONY: build-docker +build-docker: build-bin ensure-prometheus ## Build docker image + $(DOCKER_CMD) build -t $(CONTAINER_IMG) . + $(DOCKER_CMD) build -t $(CONTAINER_IMG_TEST) . + +.PHONY: ensure-prometheus +ensure-prometheus: .cache/prometheus ## Ensures that Prometheus is installed in the project dir. Downloads it if necessary. + +.PHONY: test +test: ensure-prometheus + go test ./... -tags integration -coverprofile cover.out -covermode atomic + +.PHONY: fmt +fmt: ## Run 'go fmt' and `jsonnetfmt` against code + go fmt ./... + find . \( -name '*.jsonnet' -o -name '*.libsonnet' \) -exec jsonnetfmt -i -- {} \; + +.PHONY: vet +vet: ## Run 'go vet' against code + go vet ./... + +.PHONY: lint +lint: fmt vet generate ## All-in-one linting + @echo 'Check for uncommitted changes ...' + git diff --exit-code + +.PHONY: generate +generate: ## Generate additional code and artifacts + @go generate ./... + +.PHONY: clean +clean: + rm -rf docs/node_modules $(docs_out_dir) dist .cache + +.cache/prometheus: + mkdir -p .cache + curl -fsSLo .cache/prometheus.tar.gz $(PROMETHEUS_DOWNLOAD_LINK) + tar -xzf .cache/prometheus.tar.gz -C .cache + mv .cache/prometheus-$(PROMETHEUS_VERSION).$(PROMETHEUS_DIST)-$(PROMETHEUS_ARCH) .cache/prometheus + rm -rf .cache/*.tar.gz + +# current date in UTC in ISO 8601 format (RFC 3339) with Z as timezone that works on both linux and macos +.PHONY: current-date +current-date: + date -u +"%Y-%m-%dT%H:%M:%SZ" diff --git a/Makefile.vars.mk b/Makefile.vars.mk new file mode 100644 index 0000000..6a1fd7f --- /dev/null +++ b/Makefile.vars.mk @@ -0,0 +1,21 @@ +## These are some common variables for Make + +PROJECT_ROOT_DIR = . +PROJECT_NAME ?= promtool-jsonnet +PROJECT_OWNER ?= appuio + +## BUILD:go +BIN_FILENAME ?= $(PROJECT_NAME) + +## BUILD:docker +DOCKER_CMD ?= docker + +IMG_TAG ?= latest +# Image URL to use all building/pushing image targets +CONTAINER_IMG ?= local.dev/$(PROJECT_OWNER)/$(PROJECT_NAME):$(IMG_TAG) +CONTAINER_IMG_TEST ?= local.dev/$(PROJECT_OWNER)/$(PROJECT_NAME)-test:$(IMG_TAG) + +PROMETHEUS_VERSION ?= 2.40.7 +PROMETHEUS_DIST ?= $(shell go env GOOS) +PROMETHEUS_ARCH ?= $(shell go env GOARCH) +PROMETHEUS_DOWNLOAD_LINK ?= https://github.com/prometheus/prometheus/releases/download/v$(PROMETHEUS_VERSION)/prometheus-$(PROMETHEUS_VERSION).$(PROMETHEUS_DIST)-$(PROMETHEUS_ARCH).tar.gz diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8dd076 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# promtool-jsonnet + +[![Build](https://img.shields.io/github/workflow/status/appuio/promtool-jsonnet/Test)][build] +![Go version](https://img.shields.io/github/go-mod/go-version/appuio/promtool-jsonnet) +[![Version](https://img.shields.io/github/v/release/appuio/promtool-jsonnet)][releases] +[![GitHub downloads](https://img.shields.io/github/downloads/appuio/promtool-jsonnet/total)][releases] + +[build]: https://github.com/appuio/promtool-jsonnet/actions?query=workflow%3ATest +[releases]: https://github.com/appuio/promtool-jsonnet/releases + +## Usage + +### Run tests + +```sh +export PJ_JSONNET_PATH=~"`pwd`/jsonnet/" +make ensure-prometheus +go run . --test-file ~/path/to/your/promtool/tests.jsonnet --add-yaml-file ~/path/to/supplemental/yaml/file.yml +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c289bf1 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module github.com/appuio/promtool-jsonnet + +go 1.21.5 + +require ( + github.com/google/go-jsonnet v0.20.0 + github.com/urfave/cli/v2 v2.25.7 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + sigs.k8s.io/yaml v1.1.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3e87534 --- /dev/null +++ b/go.sum @@ -0,0 +1,33 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g= +github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +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-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/jsonnet/promtest.libsonnet b/jsonnet/promtest.libsonnet new file mode 100644 index 0000000..a6a2664 --- /dev/null +++ b/jsonnet/promtest.libsonnet @@ -0,0 +1,33 @@ +local formatLabels = function(labels) + local lf = std.join(', ', std.map(function(l) '%s="%s"' % [l, labels[l]], std.objectFields(labels))); + '{%s}' % [lf]; + +// returns a series object with correctly formatted labels. +// labels can be modified post creation using `_labels`. +local series = function(name, labels, values) { + _name:: name, + _labels:: labels, + series: self._name + formatLabels(self._labels), + values: values, +}; + +// returns a test object with the given series and samples. Sample interval is 30s +// the evaluation time is set one hour in the future since all our queries operate on a 1h window +local test = function(name, series, query, samples, interval='30s', eval_time='1h') { + name: name, + interval: interval, + input_series: if std.isArray(series) then series else std.objectValues(series), + promql_expr_test: [ + { + expr: query, + eval_time: eval_time, + exp_samples: if std.isArray(samples) then samples else [samples], + }, + ], +}; + +{ + series: series, + formatLabels: formatLabels, + test: test, +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..3cc5bf0 --- /dev/null +++ b/main.go @@ -0,0 +1,161 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "github.com/appuio/promtool-jsonnet/pkg/promjsonnet" + "github.com/urfave/cli/v2" + "gopkg.in/yaml.v3" +) + +var ( + // these variables are populated by Goreleaser when releasing + version = "unknown" + commit = "-dirty-" + date = time.Now().Format("2006-01-02") + + appName = "promtool-jsonnet" + appLongName = "Run Jsonnet files against promtool test" + + // envPrefix is the global prefix to use for the keys in environment variables + envPrefix = "PJ" +) + +type commandArgs struct { + testFilePath string + promtoolPath string + additionalYamlFiles cli.StringSlice + jsonnetPath cli.StringSlice +} + +func main() { + ctx, stop, app := newApp() + defer stop() + err := app.RunContext(ctx, os.Args) + // If required flags aren't set, it will exit before we could set up logging + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} + +func newApp() (context.Context, context.CancelFunc, *cli.App) { + command := &commandArgs{} + app := &cli.App{ + Name: appName, + Usage: appLongName, + Version: fmt.Sprintf("%s, revision=%s, date=%s", version, commit, date), + Compiled: compilationDate(), + + EnableBashCompletion: true, + + Action: command.execute, + Flags: []cli.Flag{ + &cli.StringFlag{Name: "test-file", Usage: "Path of the jsonnet test file from which to test queries", + EnvVars: envVars("TEST_FILE"), Destination: &command.testFilePath, Value: "./test.jsonnet"}, + &cli.StringFlag{Name: "promtool-path", Usage: "Path of the promtool binary", + EnvVars: envVars("PROMTOOL_PATH"), Destination: &command.promtoolPath, Value: "./.cache/prometheus/promtool"}, + &cli.StringSliceFlag{Name: "add-yaml-file", Usage: "Additional yaml files to include into the test (available to jsonnet as extVar)", + EnvVars: envVars("ADD_YAML_FILE"), Destination: &command.additionalYamlFiles}, + &cli.StringSliceFlag{Name: "jsonnet-path", Usage: "Additional library paths for jsonnet", + EnvVars: envVars("JSONNET_PATH"), Destination: &command.jsonnetPath}, + }, + ExitErrHandler: func(context *cli.Context, err error) { + if err == nil { + return + } + // Don't show stack trace if the error is expected (someone called cli.Exit()) + var exitErr cli.ExitCoder + if errors.As(err, &exitErr) { + cli.HandleExitCoder(err) + return + } + cli.OsExiter(1) + }, + } + // There is logr.NewContext(...) which returns a context that carries the logger instance. + // However, since we are configuring and replacing this logger after starting up and parsing the flags, + // we'll store a thread-safe atomic reference. + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + return ctx, stop, app +} + +// env combines envPrefix with given suffix delimited by underscore. +func env(suffix string) string { + if envPrefix == "" { + return suffix + } + return envPrefix + "_" + suffix +} + +// envVars combines envPrefix with each given suffix delimited by underscore. +func envVars(suffixes ...string) []string { + arr := make([]string, len(suffixes)) + for i := range suffixes { + arr[i] = env(suffixes[i]) + } + return arr +} + +func compilationDate() time.Time { + compiled, err := time.Parse(time.RFC3339, date) + if err != nil { + // an empty Time{} causes cli.App to guess it from binary's file timestamp. + return time.Time{} + } + return compiled +} + +func (cmd *commandArgs) execute(cliCtx *cli.Context) error { + log.Print("Running test for file ", cmd.testFilePath) + extVars, err := cmd.buildExtVars() + if err != nil { + log.Print(err, "Query test setup failed") + } + err = promjsonnet.RunTestQueries(cmd.testFilePath, cmd.promtoolPath, extVars, cmd.jsonnetPath.Value()) + + if err != nil { + log.Print(err, "Query test failed") + } + log.Print("Done") + return err +} + +func (cmd *commandArgs) buildExtVars() (*map[string]string, error) { + extVars := make(map[string]string) + for file := range cmd.additionalYamlFiles.Value() { + path := cmd.additionalYamlFiles.Value()[file] + name := filepath.Base(path) + fileHandle, err := os.Open(path) + if err != nil { + return nil, err + } + defer fileHandle.Close() + fileContent, err := io.ReadAll(fileHandle) + if err != nil { + return nil, err + } + + var parsed map[string]interface{} + err = yaml.Unmarshal(fileContent, &parsed) + if err != nil { + return nil, err + } + jsonStr, err := json.Marshal(parsed) + if err != nil { + return nil, err + } + extVars[name] = string(jsonStr) + } + return &extVars, nil +} diff --git a/pkg/promjsonnet/querycheck.go b/pkg/promjsonnet/querycheck.go new file mode 100644 index 0000000..722ffed --- /dev/null +++ b/pkg/promjsonnet/querycheck.go @@ -0,0 +1,56 @@ +package promjsonnet + +import ( + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + + "github.com/google/go-jsonnet" +) + +func RunTestQueries(filepath string, promtoolPath string, extcodes *map[string]string, jpaths []string) error { + tmp, err := renderJsonnet(filepath, extcodes, &jpaths) + if err != nil { + return err + } + return runPromtool(tmp, promtoolPath) +} + +func runPromtool(tmp string, promtoolPath string) error { + cmd := exec.Command(promtoolPath, "test", "rules", tmp) + var stderr, stdout strings.Builder + cmd.Stderr = &stderr + cmd.Stdout = &stdout + err := cmd.Run() + // Not using t.Log to keep formatting sane + fmt.Println("STDOUT") + fmt.Println(stdout.String()) + fmt.Println("STDERR") + fmt.Println(stderr.String()) + return err +} + +func renderJsonnet(tFile string, extcodes *map[string]string, jpaths *[]string) (string, error) { + vm := jsonnet.MakeVM() + vm.Importer(&jsonnet.FileImporter{ + JPaths: *jpaths, + }) + + for key := range *extcodes { + vm.ExtCode(key, (*extcodes)[key]) + } + + ev, err := vm.EvaluateFile(tFile) + if err != nil { + return "", err + } + + filename := filepath.Base(tFile) + + tmp := path.Join("/tmp", fmt.Sprintf("%s.json", filename)) + err = os.WriteFile(tmp, []byte(ev), 0644) + return tmp, err +}