refactor(grafana): rework grafana package and make it more modular

This commit is contained in:
Tom Neuber 2024-06-23 02:37:22 +02:00
parent cfcf3c6c2b
commit 7ce90dacc4
Signed by: tom
GPG key ID: F17EFE4272D89FF6
12 changed files with 710 additions and 282 deletions

3
go.mod
View file

@ -7,6 +7,7 @@ require (
github.com/alecthomas/kong v0.9.0
github.com/go-git/go-billy/v5 v5.5.0
github.com/go-git/go-git/v5 v5.12.0
golang.org/x/net v0.22.0
)
require (
@ -25,8 +26,8 @@ require (
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.15.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.18.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

34
go.sum
View file

@ -5,14 +5,12 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
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/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA=
github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
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/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@ -30,16 +28,14 @@ github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcej
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
@ -69,20 +65,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
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/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@ -91,8 +83,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@ -106,8 +96,6 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -128,8 +116,6 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -137,8 +123,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

143
pkg/grafana/client.go Normal file
View file

@ -0,0 +1,143 @@
package grafana
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema"
"golang.org/x/net/http/httpguts"
)
const UserAgent = "grafana-backuper"
type Client struct {
endpoint string
token string
tokenValid bool
userAgent string
httpClient *http.Client
Dashboard DashboardClient
DashboardVersion DashboardVersionClient
File SearchClient
}
type ClientOption func(*Client)
func WithToken(token string) ClientOption {
return func(client *Client) {
client.token = token
client.tokenValid = httpguts.ValidHeaderFieldName(token)
}
}
func WithHTTPClient(httpClient *http.Client) ClientOption {
return func(client *Client) {
client.httpClient = httpClient
}
}
func WithUserAgent(userAgent string) ClientOption {
return func(client *Client) {
client.userAgent = userAgent
}
}
func NewClient(endpoint string, options ...ClientOption) *Client {
client := &Client{
endpoint: endpoint,
tokenValid: true,
httpClient: &http.Client{},
userAgent: UserAgent,
}
for _, option := range options {
option(client)
}
client.Dashboard = DashboardClient{client: client}
client.DashboardVersion = DashboardVersionClient{client: client}
client.File = SearchClient{client: client}
return client
}
func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
url := fmt.Sprintf("%s/%s", c.endpoint, path)
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", c.userAgent)
if !c.tokenValid {
return nil, errors.New("authorization token contains invalid characters")
}
if c.token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
return req.WithContext(ctx), nil
}
type Response struct {
*http.Response
ErrorResponse *ErrorResponse
body []byte
}
type ErrorResponse struct {
Message string
TraceID uint
}
func (c *Client) Do(req *http.Request, v any) (*Response, error) {
httpResp, err := c.httpClient.Do(req)
resp := &Response{Response: httpResp}
if err != nil {
return resp, err
}
defer httpResp.Body.Close()
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return resp, err
}
resp.body = body
resp.Body = io.NopCloser(bytes.NewReader(body))
if resp.StatusCode != http.StatusOK {
var errorResponse schema.ErrorResponse
if err = json.Unmarshal(resp.body, &errorResponse); err == nil {
return resp, fmt.Errorf(
"grafana: got error with status code %d: %s",
resp.StatusCode,
ErrorResponseFromSchema(errorResponse).Message,
)
}
return resp, fmt.Errorf("grafana: server responded with an unexpected status code %d", resp.StatusCode)
}
if v != nil {
if writer, ok := v.(io.Writer); ok {
_, err = io.Copy(writer, bytes.NewReader(resp.body))
} else {
err = json.Unmarshal(resp.body, v)
}
}
return resp, err
}

View file

@ -4,142 +4,141 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"time"
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema"
)
type BoardProperties struct {
IsStarred bool `json:"isStarred,omitempty"`
IsHome bool `json:"isHome,omitempty"`
IsSnapshot bool `json:"isSnapshot,omitempty"`
Type string `json:"type,omitempty"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanStar bool `json:"canStar"`
Slug string `json:"slug"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"`
Version int `json:"version"`
FolderID int `json:"folderId"`
FolderTitle string `json:"folderTitle"`
FolderURL string `json:"folderUrl"`
type DashboardMeta struct {
IsStarred bool
Type string
CanSave bool
CanEdit bool
CanAdmin bool
CanStar bool
CanDelete bool
Slug string
URL string
Expires time.Time
Created time.Time
Updated time.Time
UpdatedBy string
CreatedBy string
Version uint
HasACL bool
IsFolder bool
FolderID uint
FolderUID string
FolderTitle string
FolderURL string
Provisioned bool
ProvisionedExternalID string
AnnotationsPermissions AnnotationsPermissions
Dashboard any
}
type DashboardVersion struct {
ID uint `json:"id"`
DashboardID uint `json:"dashboardId"`
DashboardUID string `json:"uid"`
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"`
type AnnotationsPermissions struct {
Dashboard AnnotationPermissions
Organization AnnotationPermissions
}
func (c *Client) getRawDashboardByUID(ctx context.Context, path string) ([]byte, BoardProperties, error) {
raw, code, err := c.get(ctx, fmt.Sprintf("api/dashboards/%s", path), nil)
type AnnotationPermissions struct {
CanAdd bool
CanEdit bool
CanDelete bool
}
type DashboardClient struct {
client *Client
}
func (c *DashboardClient) Get(ctx context.Context, uid string) (*DashboardMeta, *Response, error) {
req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/dashboards/uid/%s", uid), nil)
if err != nil {
return nil, BoardProperties{}, err
}
if code != 200 {
return raw, BoardProperties{}, fmt.Errorf("HTTP error %d: returns %s", code, raw)
return nil, nil, err
}
var result struct {
Meta BoardProperties `json:"meta"`
}
var body schema.DashboardMeta
dec := json.NewDecoder(bytes.NewReader(raw))
dec.UseNumber()
if err := dec.Decode(&result); err != nil {
return raw, BoardProperties{}, fmt.Errorf("failed unmarshalling dashboard from path %s: %v", path, err)
}
return raw, result.Meta, nil
}
func (c *Client) getRawDashboardFromVersion(ctx context.Context, path string) ([]byte, DashboardVersion, error) {
var versionInfo DashboardVersion
raw, code, err := c.get(ctx, fmt.Sprintf("api/dashboards/%s", path), nil)
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, versionInfo, err
}
if code != 200 {
return raw, versionInfo, fmt.Errorf("HTTP error %d: returns %s", code, raw)
return nil, resp, err
}
dec := json.NewDecoder(bytes.NewReader(raw))
dec.UseNumber()
if err := dec.Decode(&versionInfo); err != nil {
return raw, versionInfo, fmt.Errorf("failed unmarshalling dashboard from path %s: %v", path, err)
}
return raw, versionInfo, nil
return DashboardMetaFromSchema(body), resp, nil
}
func queryParams(params ...QueryParam) url.Values {
u := url.URL{}
q := u.Query()
for _, p := range params {
p(&q)
}
return q
type DashboardCreateOpts struct {
Dashboard any
FolderID uint
FolderUID string
Message string
Overwrite bool
}
func (c *Client) GetDashboardVersionsByDashboardUID(ctx context.Context, uid string, params ...QueryParam) ([]DashboardVersion, error) {
var (
raw []byte
code int
err error
)
func (o DashboardCreateOpts) Validate() error {
if o.FolderUID == "" && o.FolderID == 0 {
return errors.New("folder ID or UID missing")
}
if o.Dashboard == nil {
return errors.New("dashboard is nil")
}
return nil
}
if raw, code, err = c.get(ctx, fmt.Sprintf("api/dashboards/uid/%s/versions", uid), queryParams(params...)); err != nil {
type DashboardCreateResponse struct {
DashboardID uint
DashboardUID string
URL string
Status string
Version uint
Slug string
}
func (c *DashboardClient) Create(
ctx context.Context,
opts DashboardCreateOpts,
) (*DashboardCreateResponse, *Response, error) {
if err := opts.Validate(); err != nil {
return nil, nil, err
}
var reqBody schema.DashboardCreateRequest
reqBody.Dashboard = opts.Dashboard
reqBody.Overwrite = opts.Overwrite
reqBody.Message = opts.Message
if opts.FolderUID != "" {
reqBody.FolderUID = opts.FolderUID
} else {
reqBody.FolderID = opts.FolderID
}
reqBodyData, err := json.Marshal(reqBody)
if err != nil {
return nil, nil, err
}
req, err := c.client.NewRequest(ctx, "POST", "/dashboards/db", bytes.NewReader(reqBodyData))
if err != nil {
return nil, nil, err
}
var respBody schema.DashboardCreateResponse
resp, err := c.client.Do(req, &respBody)
if err != nil {
return nil, resp, err
}
return DashboardCreateResponseFromSchema(respBody), resp, nil
}
func (c *DashboardClient) Delete(ctx context.Context, uid string) (*Response, error) {
req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/dashboards/uid/%s", uid), nil)
if 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) GetRawDashboardByUID(ctx context.Context, uid string) ([]byte, BoardProperties, error) {
return c.getRawDashboardByUID(ctx, "uid/"+uid)
}
func (c *Client) GetRawDashboardByUIDAndVersion(ctx context.Context, uid string, version uint) ([]byte, DashboardVersion, error) {
return c.getRawDashboardFromVersion(ctx, "uid/"+uid+"/versions/"+fmt.Sprint(version))
}
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...)
}
func ConvertRawToIndent(raw []byte) (string, error) {
var buf bytes.Buffer
err := json.Indent(&buf, raw, "", " ")
if err != nil {
return "", fmt.Errorf("error pritty-printing raw json string: %v", err)
}
return buf.String(), nil
return c.client.Do(req, nil)
}

View file

@ -0,0 +1,163 @@
package grafana
import (
"context"
"fmt"
"net/url"
"strconv"
"time"
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema"
)
type DashboardVersionParam func(*url.Values)
func WithLimit(limit uint) DashboardVersionParam {
return func(v *url.Values) {
v.Set("limit", strconv.FormatUint(uint64(limit), 10))
}
}
func WithStart(start uint) DashboardVersionParam {
return func(v *url.Values) {
v.Set("start", strconv.FormatUint(uint64(start), 10))
}
}
type DashboardVersion struct {
ID uint
DashboardID uint
DashboardUID string
ParentVersion uint
RestoredFrom uint
Version uint
Created time.Time
CreatedBy string
Message string
Data any
}
type DashboardVersionClient struct {
client *Client
}
func (c *DashboardVersionClient) GetByID(
ctx context.Context,
id, version uint,
params ...DashboardVersionParam,
) (*DashboardVersion, *Response, error) {
dashboardVersionURL, err := url.Parse(fmt.Sprintf("/dashboards/id/%d/versions/%d", id, version))
if err != nil {
return nil, nil, err
}
dashboards, resp, err := c.get(ctx, dashboardVersionURL, params...)
if err != nil || len(dashboards) < 1 {
return nil, resp, err
}
return dashboards[0], resp, nil
}
func (c *DashboardVersionClient) GetByUID(
ctx context.Context,
uid string,
version uint,
params ...DashboardVersionParam,
) (*DashboardVersion, *Response, error) {
dashboardVersionURL, err := url.Parse(fmt.Sprintf("/dashboards/id/%s/versions/%d", uid, version))
if err != nil {
return nil, nil, err
}
dashboards, resp, err := c.get(ctx, dashboardVersionURL, params...)
if err != nil || len(dashboards) < 1 {
return nil, resp, err
}
return dashboards[0], resp, nil
}
func (c *DashboardVersionClient) Get(
ctx context.Context,
input string,
version uint,
params ...DashboardVersionParam,
) (*DashboardVersion, *Response, error) {
if id, err := strconv.Atoi(input); err == nil {
return c.GetByID(ctx, uint(id), version, params...)
}
return c.GetByUID(ctx, input, version, params...)
}
func (c *DashboardVersionClient) ListByID(
ctx context.Context,
id uint,
params ...DashboardVersionParam,
) ([]*DashboardVersion, *Response, error) {
dashboardVersionURL, err := url.Parse(fmt.Sprintf("/dashboards/id/%d/versions", id))
if err != nil {
return nil, nil, err
}
return c.get(ctx, dashboardVersionURL, params...)
}
func (c *DashboardVersionClient) ListByUID(
ctx context.Context,
uid string,
params ...DashboardVersionParam,
) ([]*DashboardVersion, *Response, error) {
dashboardVersionURL, err := url.Parse(fmt.Sprintf("/dashboards/uid/%s/versions", uid))
if err != nil {
return nil, nil, err
}
return c.get(ctx, dashboardVersionURL, params...)
}
func (c *DashboardVersionClient) List(
ctx context.Context,
input string,
params ...DashboardVersionParam,
) ([]*DashboardVersion, *Response, error) {
if id, err := strconv.Atoi(input); err == nil {
return c.ListByID(ctx, uint(id), params...)
}
return c.ListByUID(ctx, input, params...)
}
func (c *DashboardVersionClient) get(
ctx context.Context,
dashboardVersionURL *url.URL,
params ...DashboardVersionParam,
) ([]*DashboardVersion, *Response, error) {
if len(params) > 0 {
query := dashboardVersionURL.Query()
for _, param := range params {
param(&query)
}
dashboardVersionURL.RawQuery = query.Encode()
}
req, err := c.client.NewRequest(ctx, "GET", dashboardVersionURL.String(), nil)
if err != nil {
return nil, nil, err
}
var body schema.DashboardVersionListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, resp, err
}
dashboardVersions := make([]*DashboardVersion, 0, len(body.DashboardVersions))
for _, dashboardVersion := range body.DashboardVersions {
dashboardVersions = append(dashboardVersions, DashboardVersionFromSchema(dashboardVersion))
}
return dashboardVersions, resp, nil
}

View file

@ -1,82 +0,0 @@
package grafana
import (
"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) {
baseURL, err := url.Parse(apiURL)
if err != nil {
return nil, err
}
var key string
basicAuth := strings.Contains(authString, ":")
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)
}

98
pkg/grafana/schema.go Normal file
View file

@ -0,0 +1,98 @@
package grafana
import (
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema"
)
func DashboardCreateResponseFromSchema(source schema.DashboardCreateResponse) *DashboardCreateResponse {
return &DashboardCreateResponse{
DashboardID: source.DashboardID,
DashboardUID: source.DashboardUID,
URL: source.URL,
Status: source.Status,
Version: source.Version,
Slug: source.Slug,
}
}
func DashboardMetaFromSchema(source schema.DashboardMeta) *DashboardMeta {
return &DashboardMeta{
IsStarred: source.IsStarred,
Type: source.Type,
CanSave: source.CanSave,
CanEdit: source.CanEdit,
CanAdmin: source.CanAdmin,
CanStar: source.CanStar,
CanDelete: source.CanDelete,
Slug: source.Slug,
URL: source.URL,
Expires: source.Expires,
Created: source.Created,
Updated: source.Updated,
UpdatedBy: source.UpdatedBy,
CreatedBy: source.CreatedBy,
Version: source.Version,
HasACL: source.HasACL,
IsFolder: source.IsFolder,
FolderID: source.FolderID,
FolderUID: source.FolderUID,
FolderTitle: source.FolderTitle,
FolderURL: source.FolderURL,
Provisioned: source.Provisioned,
ProvisionedExternalID: source.ProvisionedExternalID,
AnnotationsPermissions: AnnotationsPermissions{
Dashboard: AnnotationPermissions{
CanAdd: source.AnnotationsPermissions.Dashboard.CanAdd,
CanEdit: source.AnnotationsPermissions.Dashboard.CanEdit,
CanDelete: source.AnnotationsPermissions.Dashboard.CanDelete,
},
Organization: AnnotationPermissions{
CanAdd: source.AnnotationsPermissions.Organization.CanAdd,
CanEdit: source.AnnotationsPermissions.Organization.CanEdit,
CanDelete: source.AnnotationsPermissions.Organization.CanDelete,
},
},
Dashboard: source.Dashboard,
}
}
func DashboardVersionFromSchema(source schema.DashboardVersion) *DashboardVersion {
return &DashboardVersion{
ID: source.ID,
DashboardID: source.DashboardID,
DashboardUID: source.UID,
ParentVersion: source.ParentVersion,
RestoredFrom: source.RestoredFrom,
Version: source.Version,
Created: source.Created,
CreatedBy: source.CreatedBy,
Message: source.Message,
Data: source.Data,
}
}
func ErrorResponseFromSchema(source schema.ErrorResponse) *ErrorResponse {
return &ErrorResponse{
Message: source.Message,
TraceID: source.TraceID,
}
}
func SearchResultFromSchema(source schema.SearchResult) *SearchResult {
return &SearchResult{
ID: source.ID,
UID: source.UID,
Title: source.Title,
URI: source.URI,
URL: source.URL,
Slug: source.Slug,
Type: source.Type,
Tags: source.Tags,
IsStarred: source.IsStarred,
SortMeta: source.SortMeta,
FolderID: source.FolderID,
FolderUID: source.FolderUID,
FolderTitle: source.FolderTitle,
FolderURL: source.FolderURL,
}
}

View file

@ -0,0 +1,59 @@
package schema
import "time"
type DashboardCreateRequest struct {
Dashboard any `json:"dasboard"`
FolderID uint `json:"folderId,omitempty"`
FolderUID string `json:"folderUid"`
Message string `json:"message,omitempty"`
Overwrite bool `json:"overwrite"`
}
type DashboardCreateResponse struct {
DashboardID uint `json:"id"`
DashboardUID string `json:"uid"`
URL string `json:"url"`
Status string `json:"status"`
Version uint `json:"version"`
Slug string `json:"slug"`
}
type DashboardMeta struct {
IsStarred bool `json:"isStarred"`
Type string `json:"type"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"`
CanStar bool `json:"canStar"`
CanDelete bool `json:"canDelete"`
Slug string `json:"slug"`
URL string `json:"url"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"`
Version uint `json:"version"`
HasACL bool `json:"hasAcl"`
IsFolder bool `json:"isFolder"`
FolderID uint `json:"folderId"`
FolderUID string `json:"folderUid"`
FolderTitle string `json:"folderTitle"`
FolderURL string `json:"folderUrl"`
Provisioned bool `json:"provisioned"`
ProvisionedExternalID string `json:"provisionedExternalId"`
AnnotationsPermissions struct {
Dashboard struct {
CanAdd bool `json:"canAdd"`
CanEdit bool `json:"canEdit"`
CanDelete bool `json:"canDelete"`
} `json:"dashboard"`
Organization struct {
CanAdd bool `json:"canAdd"`
CanEdit bool `json:"canEdit"`
CanDelete bool `json:"canDelete"`
} `json:"organization"`
} `json:"annotationsPermissions"`
Dashboard any `json:"dashboard"`
}

View file

@ -0,0 +1,20 @@
package schema
import "time"
type DashboardVersion struct {
ID uint `json:"id"`
DashboardID uint `json:"dashboardId"`
UID string `json:"uid"`
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"`
Data any `json:"data"`
}
type DashboardVersionListResponse struct {
DashboardVersions []DashboardVersion
}

View file

@ -0,0 +1,6 @@
package schema
type ErrorResponse struct {
Message string `json:"message"`
TraceID uint `json:"traceid"`
}

View file

@ -0,0 +1,22 @@
package schema
type SearchResult 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"`
SortMeta uint `json:"sortMeta"`
FolderID int `json:"folderId"`
FolderUID string `json:"folderUid"`
FolderTitle string `json:"folderTitle"`
FolderURL string `json:"folderUrl"`
}
type SearchResultListResponse struct {
SearchResults []SearchResult
}

View file

@ -2,68 +2,30 @@ package grafana
import (
"context"
"encoding/json"
"fmt"
"net/url"
"strconv"
"git.ar21.de/yolokube/grafana-backuper/pkg/grafana/schema"
)
type (
SearchParam func(*url.Values)
SearchType string
)
const (
SearchTypeFolder SearchParamType = "dash-folder"
SearchTypeDashboard SearchParamType = "dash-db"
SearchTypeFolder SearchType = "dash-folder"
SearchTypeDashboard SearchType = "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"`
func WithType(searchType SearchType) SearchParam {
return func(v *url.Values) {
v.Set("type", string(searchType))
}
}
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 {
func WithQuery(query string) SearchParam {
return func(v *url.Values) {
if query != "" {
v.Set("query", query)
@ -71,13 +33,13 @@ func SearchQuery(query string) SearchParam {
}
}
func SearchStarred(starred bool) SearchParam {
func WithStarred() SearchParam {
return func(v *url.Values) {
v.Set("starred", strconv.FormatBool(starred))
v.Set("starred", strconv.FormatBool(true))
}
}
func SearchTag(tag string) SearchParam {
func WithTag(tag string) SearchParam {
return func(v *url.Values) {
if tag != "" {
v.Add("tag", tag)
@ -85,8 +47,59 @@ func SearchTag(tag string) SearchParam {
}
}
func SearchType(searchType SearchParamType) SearchParam {
return func(v *url.Values) {
v.Set("type", string(searchType))
}
type SearchResult struct {
ID uint
UID string
Title string
URI string
URL string
Slug string
Type string
Tags []string
IsStarred bool
SortMeta uint
FolderID int
FolderUID string
FolderTitle string
FolderURL string
}
type SearchClient struct {
client *Client
}
func (c *SearchClient) Search(ctx context.Context, params ...SearchParam) ([]*SearchResult, *Response, error) {
searchURL, err := url.Parse("/search")
if err != nil {
return nil, nil, err
}
if len(params) > 0 {
query := searchURL.Query()
for _, param := range params {
param(&query)
}
searchURL.RawQuery = query.Encode()
}
req, err := c.client.NewRequest(ctx, "GET", searchURL.String(), nil)
if err != nil {
return nil, nil, err
}
var body schema.SearchResultListResponse
resp, err := c.client.Do(req, &body)
if err != nil {
return nil, resp, err
}
searchResults := make([]*SearchResult, 0, len(body.SearchResults))
for _, searchResult := range body.SearchResults {
searchResults = append(searchResults, SearchResultFromSchema(searchResult))
}
return searchResults, resp, nil
}