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..2a81795 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,68 @@ +# This is an example goreleaser.yaml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com + +before: + hooks: + make ensure-prometheus + +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 + - .cache/prometheus + +- 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/get-prometheus.sh b/get-prometheus.sh new file mode 100755 index 0000000..753643e --- /dev/null +++ b/get-prometheus.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +PROMETHEUS_VERSION="2.40.7" +PROMETHEUS_DIST="`go env GOOS`" +PROMETHEUS_ARCH="`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" + +CACHE_DIR="${1:-.cache}" + +mkdir -p "${CACHE_DIR}" +curl -fsSLo "${CACHE_DIR}"/prometheus.tar.gz ${PROMETHEUS_DOWNLOAD_LINK} +tar -xzf "${CACHE_DIR}"/prometheus.tar.gz -C .cache +mv "${CACHE_DIR}"/prometheus-${PROMETHEUS_VERSION}.${PROMETHEUS_DIST}-${PROMETHEUS_ARCH} .cache/prometheus +rm -rf "${CACHE_DIR}"/*.tar.gz 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 +}