add grafana package & temporary debug checks
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
1247fc3fcb
commit
2d08f75545
9 changed files with 373 additions and 0 deletions
19
.drone.yml
Normal file
19
.drone.yml
Normal 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
55
cfg/cfg.go
Normal 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
5
go.mod
Normal 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
8
go.sum
Normal 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
42
main.go
Normal 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
60
pkg/grafana/dashboard.go
Normal 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
80
pkg/grafana/requests.go
Normal 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
89
pkg/grafana/search.go
Normal 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
15
renovate.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in a new issue