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