add grafana package & temporary debug checks
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Tom Neuber 2024-02-15 01:12:57 +01:00
parent 1247fc3fcb
commit 2d08f75545
Signed by: tom
GPG key ID: F17EFE4272D89FF6
9 changed files with 373 additions and 0 deletions

19
.drone.yml Normal file
View file

@ -0,0 +1,19 @@
kind: pipeline
name: build
steps:
- name: gofmt
image: golang:1.21.5
commands:
- gofmt -l -s .
when:
event:
- push
- name: vuln-check
image: golang:1.21.5
commands:
- go install golang.org/x/vuln/cmd/govulncheck@latest
- govulncheck ./...
when:
event:
- push

55
cfg/cfg.go Normal file
View file

@ -0,0 +1,55 @@
package cfg
import (
"fmt"
"os"
"github.com/alecthomas/kong"
)
type AppSettings struct {
GrafanaURL string
Token string
}
var cliStruct struct {
GrafanaURL string `name:"grafana-url" env:"GRAFANA_URL" help:"Grafana URL to access the API"`
AuthToken string `name:"grafana-auth-token" env:"GRAFANA_AUTH_TOKEN" help:"Grafana auth token to access the API"`
}
func Parse() *AppSettings {
ctx := kong.Parse(
&cliStruct,
kong.Name("grafana-backuper"),
kong.Description("🚀 CLI tool to backup grafana dashboards"),
kong.UsageOnError(),
)
validateFlags(ctx)
settings := &AppSettings{
GrafanaURL: cliStruct.GrafanaURL,
Token: cliStruct.AuthToken,
}
return settings
}
func validateFlags(cliCtx *kong.Context) {
var flagsValid = true
var messages = []string{}
if cliStruct.GrafanaURL == "" {
messages = append(messages, "error: invalid grafana URL, must not be blank")
flagsValid = false
}
if cliStruct.AuthToken == "" {
messages = append(messages, "error: invalid auth token for grafana, must not be blank")
flagsValid = false
}
if !flagsValid {
cliCtx.PrintUsage(false)
fmt.Println()
for i := 0; i < len(messages); i++ {
fmt.Println(messages[i])
}
os.Exit(1)
}
}

5
go.mod Normal file
View file

@ -0,0 +1,5 @@
module git.ar21.de/yolokube/grafana-backuper
go 1.21.5
require github.com/alecthomas/kong v0.8.1

8
go.sum Normal file
View file

@ -0,0 +1,8 @@
github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0=
github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA=
github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY=
github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=

42
main.go Normal file
View file

@ -0,0 +1,42 @@
package main
import (
"context"
"fmt"
"log"
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana"
)
const (
grafanaURL = "https://grafana.services.yolokube.de"
grafanaAPIKey = ""
gitRepoURL = ""
gitRepoDirectory = ""
)
func main() {
ctx := context.Background()
client, err := grafana.NewClient(grafanaURL, grafanaAPIKey, grafana.DefaultHTTPClient)
if err != nil {
log.Fatalf("Error creating the grafana client: %v", err)
}
dashboards, err := client.SearchDashboards(ctx, "", false)
if err != nil {
log.Fatalf("Error fetching dashboards: %v", err)
}
for _, dashboard := range dashboards {
versions, err := client.GetDashboardVersionsByDashboardID(ctx, dashboard.ID)
if err != nil {
log.Fatalf("Error fetching versions for dashboard %s: %v", dashboard.Title, err)
}
fmt.Println(dashboard.Title)
fmt.Println(versions)
for _, version := range versions {
fmt.Printf("%s: %s\n", version.CreatedBy, version.Message)
}
}
}

60
pkg/grafana/dashboard.go Normal file
View file

@ -0,0 +1,60 @@
package grafana
import (
"context"
"encoding/json"
"fmt"
"net/url"
"time"
)
type DashboardVersion struct {
ID uint `json:"id"`
DashboardID uint `json:"dashboardId"`
ParentVersion uint `json:"parentVersion"`
RestoredFrom uint `json:"restoredFrom"`
Version uint `json:"version"`
Created time.Time `json:"created"`
CreatedBy string `json:"createdBy"`
Message string `json:"message"`
}
func queryParams(params ...QueryParam) url.Values {
u := url.URL{}
q := u.Query()
for _, p := range params {
p(&q)
}
return q
}
func (c *Client) GetDashboardVersionsByDashboardID(ctx context.Context, dashboardID uint, params ...QueryParam) ([]DashboardVersion, error) {
var (
raw []byte
code int
err error
)
if raw, code, err = c.get(ctx, fmt.Sprintf("api/dashboards/id/%d/versions", dashboardID), queryParams(params...)); err != nil {
return nil, err
}
if code != 200 {
return nil, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
var versions []DashboardVersion
err = json.Unmarshal(raw, &versions)
return versions, err
}
func (c *Client) SearchDashboards(ctx context.Context, query string, starred bool, tags ...string) ([]FoundBoard, error) {
params := []SearchParam{
SearchType(SearchTypeDashboard),
SearchQuery(query),
SearchStarred(starred),
}
for _, tag := range tags {
params = append(params, SearchTag(tag))
}
return c.Search(ctx, params...)
}

80
pkg/grafana/requests.go Normal file
View file

@ -0,0 +1,80 @@
package grafana
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
)
var DefaultHTTPClient = http.DefaultClient
type Client struct {
baseURL string
key string
basicAuth bool
client *http.Client
}
func NewClient(apiURL, authString string, client *http.Client) (*Client, error) {
basicAuth := strings.Contains(authString, ":")
baseURL, err := url.Parse(apiURL)
if err != nil {
return nil, err
}
var key string
if len(authString) > 0 {
if !basicAuth {
key = fmt.Sprintf("Bearer %s", authString)
} else {
parts := strings.SplitN(authString, ":", 2)
baseURL.User = url.UserPassword(parts[0], parts[1])
}
}
return &Client{
baseURL: baseURL.String(),
basicAuth: basicAuth,
key: key,
client: client,
}, nil
}
func (c *Client) doRequest(ctx context.Context, method, query string, params url.Values, buf io.Reader) ([]byte, int, error) {
u, _ := url.Parse(c.baseURL)
u.Path = path.Join(u.Path, query)
if params != nil {
u.RawQuery = params.Encode()
}
req, err := http.NewRequest(method, u.String(), buf)
if err != nil {
return nil, 0, err
}
req = req.WithContext(ctx)
if !c.basicAuth && len(c.key) > 0 {
req.Header.Set("Authorization", c.key)
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "grafana-backuper")
resp, err := c.client.Do(req)
if err != nil {
return nil, 0, err
}
data, err := io.ReadAll(resp.Body)
resp.Body.Close()
return data, resp.StatusCode, err
}
func (c *Client) get(ctx context.Context, query string, params url.Values) ([]byte, int, error) {
return c.doRequest(ctx, "GET", query, params, nil)
}
func (c *Client) post(ctx context.Context, query string, params url.Values, body []byte) ([]byte, int, error) {
return c.doRequest(ctx, "POST", query, params, bytes.NewBuffer(body))
}

89
pkg/grafana/search.go Normal file
View file

@ -0,0 +1,89 @@
package grafana
import (
"context"
"encoding/json"
"fmt"
"net/url"
"strconv"
)
const (
SearchTypeFolder SearchParamType = "dash-folder"
SearchTypeDashboard SearchParamType = "dash-db"
)
type FoundBoard struct {
ID uint `json:"id"`
UID string `json:"uid"`
Title string `json:"title"`
URI string `json:"uri"`
URL string `json:"url"`
Slug string `json:"slug"`
Type string `json:"type"`
Tags []string `json:"tags"`
IsStarred bool `json:"isStarred"`
FolderID int `json:"folderId"`
FolderUID string `json:"folderUid"`
FolderTitle string `json:"folderTitle"`
FolderURL string `json:"folderUrl"`
}
type (
// SearchParam is a type for specifying Search params.
SearchParam func(*url.Values)
// SearchParamType is a type accepted by SearchType func.
SearchParamType string
// QueryParam is a type for specifying arbitrary API parameters
QueryParam func(*url.Values)
)
func (c *Client) Search(ctx context.Context, params ...SearchParam) ([]FoundBoard, error) {
var (
raw []byte
boards []FoundBoard
code int
err error
)
u := url.URL{}
q := u.Query()
for _, p := range params {
p(&q)
}
if raw, code, err = c.get(ctx, "api/search", q); err != nil {
return nil, err
}
if code != 200 {
return nil, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
err = json.Unmarshal(raw, &boards)
return boards, err
}
func SearchQuery(query string) SearchParam {
return func(v *url.Values) {
if query != "" {
v.Set("query", query)
}
}
}
func SearchStarred(starred bool) SearchParam {
return func(v *url.Values) {
v.Set("starred", strconv.FormatBool(starred))
}
}
func SearchTag(tag string) SearchParam {
return func(v *url.Values) {
if tag != "" {
v.Add("tag", tag)
}
}
}
func SearchType(searchType SearchParamType) SearchParam {
return func(v *url.Values) {
v.Set("type", string(searchType))
}
}

15
renovate.json Normal file
View file

@ -0,0 +1,15 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
],
"packageRules": [
{
"matchPackagePatterns": ["*"],
"automerge": true,
"automergeType": "pr",
"platformAutomerge": true,
"dependencyDashboard": true
}
]
}