From ccec34913ab56fad1dbc37a502c91dab1cde6cbb Mon Sep 17 00:00:00 2001 From: maxime1907 <19607336+maxime1907@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:14:36 +0200 Subject: [PATCH] feat: initial version --- .dockerignore | 29 ++++ .github/dependabot.yml | 16 ++ .github/workflows/build_release.yml | 131 ++++++++++++++++ .github/workflows/compliance.yml.disabled | 22 +++ .github/workflows/lint.yml | 33 +++++ .gitignore | 4 + Containerfile | 25 ++++ Makefile | 49 ++++++ README.md | 24 +++ cmd/ovh-exporter/main.go | 43 ++++++ cog.toml | 3 + go.mod | 28 ++++ go.sum | 74 +++++++++ pkg/cmd/version.go | 22 +++ pkg/credentials/generate.go | 62 ++++++++ pkg/network/serve.go | 140 ++++++++++++++++++ pkg/ovhsdk/api/cloud_project_flavor.go | 28 ++++ .../api/cloud_project_instance_billing.go | 40 +++++ pkg/ovhsdk/models/cloud_project_flavor.go | 18 +++ pkg/ovhsdk/models/cloud_project_instance.go | 86 +++++++++++ 20 files changed, 877 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build_release.yml create mode 100644 .github/workflows/compliance.yml.disabled create mode 100644 .github/workflows/lint.yml create mode 100644 Containerfile create mode 100644 Makefile create mode 100644 cmd/ovh-exporter/main.go create mode 100644 cog.toml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/cmd/version.go create mode 100644 pkg/credentials/generate.go create mode 100644 pkg/network/serve.go create mode 100644 pkg/ovhsdk/api/cloud_project_flavor.go create mode 100644 pkg/ovhsdk/api/cloud_project_instance_billing.go create mode 100644 pkg/ovhsdk/models/cloud_project_flavor.go create mode 100644 pkg/ovhsdk/models/cloud_project_instance.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7f0f65e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,29 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Custom part +ovh-exporter +vendor diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..45aac52 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + - "bot" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + - "bot" diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml new file mode 100644 index 0000000..3ba034d --- /dev/null +++ b/.github/workflows/build_release.yml @@ -0,0 +1,131 @@ +name: Build and release + +on: + push: + branches: + - main + tags: + - 'v*.*.*' + pull_request: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + BINARY_NAME: ovh-exporter + +jobs: + golang: + name: Golang + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-24.04 + include: + - os: ubuntu-24.04 + platform: linux + arch: amd64 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23.1' + + - name: Install packages + run: | + sudo apt-get update + sudo apt install -yq --no-install-recommends make + + - name: Build + env: + GOOS: ${{ matrix.platform }} + GOARCH: ${{ matrix.arch }} + run: make build + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ env.BINARY_NAME }}-${{ matrix.platform }}-${{ matrix.arch }} + path: ${{ env.BINARY_NAME }} + if-no-files-found: error + + container: + name: Containerfile + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-24.04 + include: + - os: ubuntu-24.04 + platform: linux + arch: amd64 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + uses: docker/metadata-action@v5 + id: metadata + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build image + uses: docker/build-push-action@v6 + with: + context: . + file: Containerfile + push: ${{ github.event_name != 'pull_request' }} + platforms: ${{ matrix.platform }}/${{ matrix.arch }} + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + + release: + runs-on: ubuntu-24.04 + needs: + - golang + - container + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + + - name: Package + run: | + for folder in ./*; do + if [ -d "$folder" ]; then + echo "Processing folder: $folder" + cd $folder + tar -czf ../$folder.tar.gz -T <(\ls -1) + cd .. + sha256sum $folder.tar.gz > $folder.tar.gz.sha256 + fi + done + + - name: Release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: '*.tar.gz*' + tag: ${{ github.ref }} + file_glob: true diff --git a/.github/workflows/compliance.yml.disabled b/.github/workflows/compliance.yml.disabled new file mode 100644 index 0000000..75ac6eb --- /dev/null +++ b/.github/workflows/compliance.yml.disabled @@ -0,0 +1,22 @@ +name: Compliance + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + commit: + name: Commit + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Conventional commit check + uses: cocogitto/cocogitto-action@v3.8 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..da92883 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,33 @@ +name: Lint + +on: + pull_request: + branches: + - main + +jobs: + golang: + name: Golang + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23.1' + + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.61 + + container: + name: Containerfile + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: Containerfile diff --git a/.gitignore b/.gitignore index 6f72f89..7f0f65e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ go.work.sum # env file .env + +# Custom part +ovh-exporter +vendor diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..9d0c335 --- /dev/null +++ b/Containerfile @@ -0,0 +1,25 @@ +FROM golang:1.23-alpine AS build + +ENV CGO_ENABLED=0 +ENV GOOS=linux +ENV GOARCH=amd64 + +ARG JQ_VERSION=1.7 + +# hadolint ignore=DL3018 +RUN apk update && apk add --no-cache bash git make binutils wget \ + && wget --progress=dot:giga "https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-${GOOS}-${GOARCH}" -O /usr/bin/jq \ + && chmod +x /usr/bin/jq + +WORKDIR $GOPATH/src/github.com/wiremind/ovh-exporter + +COPY . . + +RUN make ovh-exporter && mv ovh-exporter /usr/bin/ + + +FROM busybox:stable AS runtime + +COPY --from=build /usr/bin/ovh-exporter /usr/bin/ovh-exporter + +ENTRYPOINT ["/usr/bin/ovh-exporter"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4fbf44d --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +GO?=go + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +BINARY_NAME=ovh-exporter +# TODO less bruteforce ? +BINARY_FILES=$(shell find ${PWD} -type f -name "*.go") +GOFMT_FILES?=$(shell find . -not -path "./vendor/*" -type f -name '*.go') + +VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always) +REVISION=$(shell git rev-parse HEAD)$(shell if ! git diff --no-ext-diff --quiet --exit-code; then echo .m; fi) +PKG=github.com/wiremind/ovh-exporter + +# Control if static or dynamically linked (static by default) +export CGO_ENABLED:=0 + +GO_GCFLAGS?= +GO_LDFLAGS=-ldflags '-X $(PKG)/version.Version=$(VERSION) -X $(PKG)/version.Revision=$(REVISION) -X $(PKG)/version.Package=$(PKG)' + +.PHONY: build +build: ${BINARY_NAME} + +${BINARY_NAME}: ${BINARY_FILES} + ${GO} build ${GO_GCFLAGS} ${GO_LDFLAGS} -o $@ cmd/${BINARY_NAME}/*.go + strip -x $@ + +## Lints all the go code in the application. +.PHONY: lint +lint: dependencies + gofmt -w $(GOFMT_FILES) + $(GOBIN)/goimports -w $(GOFMT_FILES) + $(GOBIN)/gofumpt -l -w $(GOFMT_FILES) + $(GOBIN)/gci write $(GOFMT_FILES) --skip-generated + $(GOBIN)/golangci-lint run + +## Loads all the dependencies to vendor directory +.PHONY: dependencies +dependencies: + go install golang.org/x/tools/cmd/goimports@v0.25.0 + go install mvdan.cc/gofumpt@v0.7.0 + go install github.com/daixiang0/gci@v0.13.5 + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61 + go mod vendor + go mod tidy diff --git a/README.md b/README.md index 68ad09c..876e98c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,26 @@ # ovh-exporter Prometheus exporter for the OVH API + +# Configuration + +Run this command to generate a unique link with correct ovh permissions needed for this project +```bash +ovh-exporter credentials +``` + +Then source a .env file containing these variables +```bash +export OVH_ENDPOINT="ovh-eu" +export OVH_APP_KEY="" +export OVH_APP_SECRET="" +export OVH_CONSUMER_KEY="" +export OVH_CLOUD_PROJECT_INSTANCE_BILLING_PROJECT_ID="" +export OVH_CACHE_UPDATE_INTERVAL="60" +export SERVER_PORT="8080" +``` + +# Running + +```bash +ovh-exporter serve +``` diff --git a/cmd/ovh-exporter/main.go b/cmd/ovh-exporter/main.go new file mode 100644 index 0000000..6451a9b --- /dev/null +++ b/cmd/ovh-exporter/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "log/slog" + "os" + + "github.com/urfave/cli" + "github.com/wiremind/ovh-exporter/pkg/cmd" + "github.com/wiremind/ovh-exporter/pkg/credentials" + "github.com/wiremind/ovh-exporter/pkg/network" +) + +func main() { + var exitValue int + + app := cli.NewApp() + app.Name = "ovh-exporter" + app.Usage = "Prometheus exporter for the OVH API" + app.Version = cmd.Version + + app.Flags = []cli.Flag{ + cli.IntFlag{ + Name: "exit, e", + Value: 1, + Usage: "value returned on error", + Destination: &exitValue, + }, + } + + app.Commands = []cli.Command{ + credentials.GenerateCommand, + network.ServeCommand, + } + + err := app.Run(os.Args) + if err != nil { + slog.Error( + "main() error", + slog.String("error", err.Error()), + ) + os.Exit(exitValue) + } +} diff --git a/cog.toml b/cog.toml new file mode 100644 index 0000000..2a0fa3b --- /dev/null +++ b/cog.toml @@ -0,0 +1,3 @@ +ignore_merge_commits = true +from_latest_tag = true +tag_prefix = "v" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9fa6cca --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module github.com/wiremind/ovh-exporter + +go 1.23.1 + +require ( + github.com/ovh/go-ovh v1.6.0 + github.com/prometheus/client_golang v1.20.4 + github.com/rs/zerolog v1.33.0 + github.com/urfave/cli v1.22.15 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..08302ba --- /dev/null +++ b/go.sum @@ -0,0 +1,74 @@ +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= +github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= +github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI= +github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= +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/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= +github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go new file mode 100644 index 0000000..40b0526 --- /dev/null +++ b/pkg/cmd/version.go @@ -0,0 +1,22 @@ +package cmd + +import "runtime" + +const ( + defaultVersion = "v0.0.0+unknown" +) + +var ( + // Package is filled at linking time + Package = "github.com/wiremind/ovh-exporter" + + // Version holds the complete version number. Filled in at linking time. + Version = defaultVersion + + // Revision is filled with the VCS (e.g. git) revision being used to build + // the program at linking time. + Revision = "" + + // GoVersion is Go tree's version. + GoVersion = runtime.Version() +) diff --git a/pkg/credentials/generate.go b/pkg/credentials/generate.go new file mode 100644 index 0000000..d79efb8 --- /dev/null +++ b/pkg/credentials/generate.go @@ -0,0 +1,62 @@ +package credentials + +import ( + "fmt" + "os" + "strings" + + "github.com/rs/zerolog" + "github.com/urfave/cli" +) + +var GenerateCommand = cli.Command{ + Name: "credentials", + Usage: "generate credentials link", + Action: generateLink, + Flags: []cli.Flag{ + // Add the OVH_CREATE_TOKEN_ENDPOINT flag here + &cli.StringFlag{ + Name: "endpoint", + Usage: "Specify the OVH create token endpoint", + Value: "https://eu.api.ovh.com/createToken", + }, + }, +} + +var logger = zerolog.New(os.Stdout).With().Timestamp().Logger() + +type APIKeyRight struct { + Method string `json:"method"` + Endpoint string `json:"endpoint"` +} + +var apiKeyRights = []APIKeyRight{ + {Method: "GET", Endpoint: "/cloud/project/*/flavor/*"}, + {Method: "GET", Endpoint: "/cloud/project/*/instance"}, + {Method: "GET", Endpoint: "/cloud/project/*/instance/*"}, +} + +func generateURL(baseURL string, apiKeyRights []APIKeyRight) string { + var generatedURL strings.Builder + + generatedURL.WriteString(baseURL) + + for index, right := range apiKeyRights { + if index == 0 { + generatedURL.WriteString("/?") + } else { + generatedURL.WriteString("&") + } + generatedURL.WriteString(fmt.Sprintf("%s=%s", right.Method, right.Endpoint)) + } + + return generatedURL.String() +} + +func generateLink(clicontext *cli.Context) error { + baseURL := clicontext.String("endpoint") + link := generateURL(baseURL, apiKeyRights) + + logger.Info().Msgf("%s", link) + return nil +} diff --git a/pkg/network/serve.go b/pkg/network/serve.go new file mode 100644 index 0000000..f3220fc --- /dev/null +++ b/pkg/network/serve.go @@ -0,0 +1,140 @@ +package network + +import ( + "fmt" + "log" + "net/http" + "os" + "strconv" + "time" + + "github.com/ovh/go-ovh/ovh" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/zerolog" + "github.com/urfave/cli" + "github.com/wiremind/ovh-exporter/pkg/ovhsdk/api" + "github.com/wiremind/ovh-exporter/pkg/ovhsdk/models" +) + +var ServeCommand = cli.Command{ + Name: "serve", + Usage: "serve all routes", + Action: serveRoutes, +} + +var logger = zerolog.New(os.Stdout).With().Timestamp().Logger() + +var cloudProjectInstanceBilling = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "ovh_exporter_cloud_project_instance_billing", + Help: "Tracks the billing for OVH cloud project instances.", + }, + []string{"project_id", "instance_id", "instance_name", "billing_type", "billing_code", "billing_monthly_date", "billing_monthly_status"}, +) + +func pingHandler(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, "pong") +} + +func initializeMetrics() { + prometheus.MustRegister(cloudProjectInstanceBilling) +} + +func setCloudProjectInstanceBilling(projectID string, instanceID string, instanceName string, billingType string, billingCode string, billingMonthlyDate time.Time, billingMonthlyStatus string, amount float64) { + billingMonthlyDateFormatted := billingMonthlyDate.Format("2006-01-02 15:04:05") + + cloudProjectInstanceBilling.With(prometheus.Labels{ + "project_id": projectID, + "instance_id": instanceID, + "instance_name": instanceName, + "billing_type": billingType, + "billing_code": billingCode, + "billing_monthly_date": billingMonthlyDateFormatted, + "billing_monthly_status": billingMonthlyStatus, + }).Set(amount) +} + +func updateCloudProviderInstanceBilling(ovhClient *ovh.Client) { + logger.Info().Msg("updating cloud provider instance billing") + + projectID := os.Getenv("OVH_CLOUD_PROJECT_INSTANCE_BILLING_PROJECT_ID") + + projectInstances, err := api.GetCloudProjectInstances(ovhClient, projectID) + if err != nil { + logger.Error().Err(err).Msgf("Failed to retrieve instances: %v", err) + } + + var flavors []models.Flavor + for _, instance := range projectInstances { + if api.FindFlavorByID(flavors, instance.FlavorID) == nil { + flavor, err := api.GetCloudProjectFlavor(ovhClient, projectID, instance.FlavorID) + if err != nil { + logger.Error().Err(err).Msgf("Failed to retrieve flavor: %v", err) + } else { + flavors = append(flavors, flavor) + } + } + } + + for _, instance := range projectInstances { + flavor := api.FindFlavorByID(flavors, instance.FlavorID) + planType := "undefined" + if instance.PlanCode != nil && flavor.PlanCodes.Hourly != nil && flavor.PlanCodes.Monthly != nil { + switch { + case *instance.PlanCode == *flavor.PlanCodes.Hourly: + planType = "hourly" + case *instance.PlanCode == *flavor.PlanCodes.Monthly: + planType = "monthly" + } + } + + setCloudProjectInstanceBilling(projectID, instance.ID, instance.Name, planType, *instance.PlanCode, instance.MonthlyBilling.Since, instance.MonthlyBilling.Status, 1) + } +} + +func setupCacheUpdater(ovhClient *ovh.Client) { + intervalStr := os.Getenv("OVH_CACHE_UPDATE_INTERVAL") + intervalSeconds, err := strconv.Atoi(intervalStr) + if err != nil { + log.Fatalf("Failed to parse OVH_CACHE_UPDATE_INTERVAL: %v", err) + } + cacheUpdateInterval := time.Duration(intervalSeconds) * time.Second + + logger.Info().Msgf("setting up cache updater to sync every %v", cacheUpdateInterval) + + ticker := time.NewTicker(cacheUpdateInterval) + defer ticker.Stop() + + for { + updateCloudProviderInstanceBilling(ovhClient) + + <-ticker.C + } +} + +func serveRoutes(clicontext *cli.Context) error { + initializeMetrics() + + ovhClient, err := ovh.NewClient( + os.Getenv("OVH_ENDPOINT"), + os.Getenv("OVH_APP_KEY"), + os.Getenv("OVH_APP_SECRET"), + os.Getenv("OVH_CONSUMER_KEY"), + ) + if err != nil { + log.Fatalf("Failed to create OVH client: %v", err) + } + + http.HandleFunc("/ping", pingHandler) + http.Handle("/metrics", promhttp.Handler()) + + serverPort := os.Getenv("SERVER_PORT") + formattedServerPort := fmt.Sprintf(":%s", serverPort) + logger.Info().Msgf("server started on port %s", formattedServerPort) + + go setupCacheUpdater(ovhClient) + + return http.ListenAndServe(formattedServerPort, nil) +} diff --git a/pkg/ovhsdk/api/cloud_project_flavor.go b/pkg/ovhsdk/api/cloud_project_flavor.go new file mode 100644 index 0000000..2acfb8b --- /dev/null +++ b/pkg/ovhsdk/api/cloud_project_flavor.go @@ -0,0 +1,28 @@ +package api + +import ( + "github.com/ovh/go-ovh/ovh" + "github.com/wiremind/ovh-exporter/pkg/ovhsdk/models" +) + +func FindFlavorByID(flavors []models.Flavor, flavorID string) *models.Flavor { + for _, flavor := range flavors { + if flavor.ID == flavorID { + return &flavor + } + } + return nil +} + +func GetCloudProjectFlavor(client *ovh.Client, projectID string, flavorID string) (models.Flavor, error) { + var flavor models.Flavor + + endpoint := "/cloud/project/" + projectID + "/flavor/" + flavorID + + err := client.Get(endpoint, &flavor) + if err != nil { + return flavor, err + } + + return flavor, nil +} diff --git a/pkg/ovhsdk/api/cloud_project_instance_billing.go b/pkg/ovhsdk/api/cloud_project_instance_billing.go new file mode 100644 index 0000000..f185072 --- /dev/null +++ b/pkg/ovhsdk/api/cloud_project_instance_billing.go @@ -0,0 +1,40 @@ +package api + +import ( + "github.com/ovh/go-ovh/ovh" + "github.com/wiremind/ovh-exporter/pkg/ovhsdk/models" +) + +func GetCloudProjectInstances(client *ovh.Client, projectID string) ([]models.InstanceSummary, error) { + // Define the response structure for the instance billing + var instances []models.InstanceSummary + + // Build the API endpoint + endpoint := "/cloud/project/" + projectID + "/instance" + + // Call OVH API to get instance data + err := client.Get(endpoint, &instances) + if err != nil { + return instances, err + } + + // Return the billing price for this instance + return instances, nil +} + +func GetCloudProjectInstance(client *ovh.Client, projectID string, instanceID string) (models.Instance, error) { + // Define the response structure for the instance billing + var instanceData models.Instance + + // Build the API endpoint + endpoint := "/cloud/project/" + projectID + "/instance/" + instanceID + + // Call OVH API to get instance data + err := client.Get(endpoint, &instanceData) + if err != nil { + return instanceData, err + } + + // Return the billing price for this instance + return instanceData, nil +} diff --git a/pkg/ovhsdk/models/cloud_project_flavor.go b/pkg/ovhsdk/models/cloud_project_flavor.go new file mode 100644 index 0000000..693bc7d --- /dev/null +++ b/pkg/ovhsdk/models/cloud_project_flavor.go @@ -0,0 +1,18 @@ +package models + +type Flavor struct { + Available bool `json:"available"` + Capabilities []FlavorCapability `json:"capabilities"` + Disk int `json:"disk"` + ID string `json:"id"` + InboundBandwidth *int64 `json:"inboundBandwidth"` // Use *int64 for nullable integer + Name string `json:"name"` + OsType string `json:"osType"` + OutboundBandwidth *int64 `json:"outboundBandwidth"` // Use *int64 for nullable integer + PlanCodes PlanCodes `json:"planCodes"` + Quota int `json:"quota"` + RAM int `json:"ram"` + Region string `json:"region"` + Type string `json:"type"` + VCPUs int `json:"vcpus"` +} diff --git a/pkg/ovhsdk/models/cloud_project_instance.go b/pkg/ovhsdk/models/cloud_project_instance.go new file mode 100644 index 0000000..e63f562 --- /dev/null +++ b/pkg/ovhsdk/models/cloud_project_instance.go @@ -0,0 +1,86 @@ +package models + +import ( + "time" +) + +type Instance struct { + Created time.Time `json:"created"` + CurrentMonthOutgoingTraffic *int64 `json:"currentMonthOutgoingTraffic"` // Use *int64 for nullable integer + Flavor Flavor `json:"flavor"` + ID string `json:"id"` + Image Image `json:"image"` + IpAddresses []IPAddress `json:"ipAddresses"` + MonthlyBilling *MonthlyBilling `json:"monthlyBilling"` // Use *MonthlyBilling for nullable object + Name string `json:"name"` + OperationIds []string `json:"operationIds"` + PlanCode *string `json:"planCode"` // Use *string for nullable string + Region string `json:"region"` + RescuePassword *string `json:"rescuePassword"` // Use *string for nullable string + SshKey *SSHKey `json:"sshKey"` // Use *SSHKey for nullable object + Status string `json:"status"` +} + +type FlavorCapability struct { + Enabled bool `json:"enabled"` + Name string `json:"name"` // Allowed: "failoverip", "resize", "snapshot", "volume" +} + +type PlanCodes struct { + Hourly *string `json:"hourly"` // Use *string for nullable string + Monthly *string `json:"monthly"` // Use *string for nullable string +} + +type Image struct { + CreationDate time.Time `json:"creationDate"` + FlavorType *string `json:"flavorType"` // Use *string for nullable string + ID string `json:"id"` + MinDisk int `json:"minDisk"` + MinRam int `json:"minRam"` + Name string `json:"name"` + PlanCode *string `json:"planCode"` // Use *string for nullable string + Region string `json:"region"` + Size float64 `json:"size"` // in GiB + Status string `json:"status"` + Tags []string `json:"tags"` + Type string `json:"type"` + User string `json:"user"` + Visibility string `json:"visibility"` +} + +type IPAddress struct { + GatewayIP string `json:"gatewayIp"` // Nullable, so can be empty string + IP string `json:"ip"` + NetworkID string `json:"networkId"` + Type string `json:"type"` + Version int `json:"version"` +} + +type MonthlyBilling struct { + Since time.Time `json:"since"` + Status string `json:"status"` // Allowed: "activationPending", "ok" +} + +type SSHKey struct { + FingerPrint string `json:"fingerPrint"` + ID string `json:"id"` + Name string `json:"name"` + PublicKey string `json:"publicKey"` + Regions []string `json:"regions"` +} + +type InstanceSummary struct { + Created time.Time `json:"created"` + CurrentMonthOutgoingTraffic *int64 `json:"currentMonthOutgoingTraffic"` // Use *int64 for nullable integer + FlavorID string `json:"flavorId"` // Instance flavor id + ID string `json:"id"` // Instance id + ImageID string `json:"imageId"` // Instance image id + IpAddresses []IPAddress `json:"ipAddresses"` + MonthlyBilling *MonthlyBilling `json:"monthlyBilling"` // Use *MonthlyBilling for nullable object + Name string `json:"name"` // Instance name + OperationIds []string `json:"operationIds"` // Ids of pending public cloud operations + PlanCode *string `json:"planCode"` // Use *string for nullable string + Region string `json:"region"` // Instance region + SshKeyID *string `json:"sshKeyId"` // Use *string for nullable string + Status string `json:"status"` // Instance status +}