Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pull] master from grafana:master #53

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c023732
Bump golang.org/x/net from 0.12.0 to 0.17.0 (#261)
dependabot[bot] Oct 13, 2023
39a906d
Deprecate --directory/-d flag in favor of os.Stat (#259)
joanlopez Oct 13, 2023
a4962f6
Add support for pulling and pushing dashboard json (#255)
undef1nd Oct 17, 2023
8b3ce5e
Switch to grafana-openapi-client-go (#262)
mbarrien Nov 13, 2023
3ad1fdb
Re-strip id and version fields (#265)
malcolmholmes Nov 13, 2023
91aa73b
Run tests on port 3001 (#266)
malcolmholmes Nov 14, 2023
2d61537
Fix tests after openapi client merge (#269)
malcolmholmes Nov 23, 2023
f629138
Simplify handlers (#270)
malcolmholmes Nov 24, 2023
88a3bda
Add CODEOWNERS (#273)
malcolmholmes Nov 28, 2023
da5092e
Add support for Contexts (#271)
malcolmholmes Nov 30, 2023
5f301fd
Bump google.golang.org/grpc from 1.58.2 to 1.58.3 (#274)
dependabot[bot] Nov 30, 2023
9eca851
fix: update folders unconditionally (#280)
theSuess Dec 13, 2023
1203a20
fix: unprepare dashboard on create (#281)
theSuess Dec 13, 2023
900c975
fix: rename current context key in settings file (#284)
theSuess Dec 15, 2023
79280de
chore: update openapi client (#285)
theSuess Dec 15, 2023
adf068c
Don't panic when no overrides (#287)
malcolmholmes Dec 18, 2023
8b7b189
Bump golang.org/x/crypto from 0.14.0 to 0.17.0 (#288)
dependabot[bot] Dec 19, 2023
00d2394
feat: add support for library elements (#282)
theSuess Dec 20, 2023
735b193
feat: add basic alerting resources (#286)
theSuess Dec 20, 2023
536c23b
feat: add contact points (#289)
theSuess Dec 21, 2023
13fd272
feat: add notificationpolicy type (#290)
theSuess Dec 21, 2023
c515774
fix: correctly handle context overrides (#291)
theSuess Dec 29, 2023
77e5550
ci: deploy docs on release only (#292)
theSuess Dec 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: add support for library elements (grafana#282)
* feat: add support for library elements

This is a reimplementation/follow up to
grafana#218, using the openapi client to keep in
line with the rest of the codebase. It allows pull & push of library panels and
variables.

Tested pull & push with a simple library panel and a dashboard using this panel

* fix: correctly order handlers
  • Loading branch information
theSuess authored Dec 20, 2023
commit 00d23944a0f8e5791e5d592eab703976a4b0505e
219 changes: 219 additions & 0 deletions pkg/grafana/library-elements-handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package grafana

import (
"encoding/json"
"fmt"
"path/filepath"

"errors"

"github.com/grafana/grizzly/pkg/grizzly"
"github.com/grafana/tanka/pkg/kubernetes/manifest"

library "github.com/grafana/grafana-openapi-client-go/client/library_elements"
"github.com/grafana/grafana-openapi-client-go/models"
)

// LibraryElementHandler is a Grizzly Handler for Grafana dashboard folders
type LibraryElementHandler struct {
Provider grizzly.Provider
}

var _ grizzly.Handler = &LibraryElementHandler{}

// NewLibraryElementHandler returns configuration defining a new Grafana Library Element Handler
func NewLibraryElementHandler(provider grizzly.Provider) *LibraryElementHandler {
return &LibraryElementHandler{
Provider: provider,
}
}

// Kind returns the name for this handler
func (h *LibraryElementHandler) Kind() string {
return "LibraryElement"
}

// Validate returns the uid of resource
func (h *LibraryElementHandler) Validate(resource grizzly.Resource) error {
uid, exist := resource.GetSpecString("uid")
if exist {
if uid != resource.Name() {
return fmt.Errorf("uid '%s' and name '%s', don't match", uid, resource.Name())
}
}

return nil
}

// APIVersion returns the group and version for the provider of which this handler is a part
func (h *LibraryElementHandler) APIVersion() string {
return h.Provider.APIVersion()
}

// GetExtension returns the file name extension for a library element
func (h *LibraryElementHandler) GetExtension() string {
return "json"
}

const (
libraryElementGlob = "library-elements/*-*"
libraryElementPattern = "library-elements/%s-%s.%s"
)

// FindResourceFiles identifies files within a directory that this handler can process
func (h *LibraryElementHandler) FindResourceFiles(dir string) ([]string, error) {
path := filepath.Join(dir, libraryElementGlob)
return filepath.Glob(path)
}

// ResourceFilePath returns the location on disk where a resource should be updated
func (h *LibraryElementHandler) ResourceFilePath(resource grizzly.Resource, filetype string) string {
kind := "element"
t := resource.GetSpecValue("kind").(float64)

switch t {
case 1:
kind = "panel"
case 2:
kind = "variable"
}
return fmt.Sprintf(libraryElementPattern, kind, resource.Name(), filetype)
}

// Parse parses a manifest object into a struct for this resource type
func (h *LibraryElementHandler) Parse(m manifest.Manifest) (grizzly.Resources, error) {
resource := grizzly.Resource(m)
resource.SetSpecString("uid", resource.Name())
return grizzly.Resources{resource}, nil
}

// Unprepare removes unnecessary elements from a remote resource ready for presentation/comparison
func (h *LibraryElementHandler) Unprepare(resource grizzly.Resource) *grizzly.Resource {
resource.DeleteSpecKey("meta")
resource.DeleteSpecKey("version")
resource.DeleteSpecKey("id")
return &resource
}

// Prepare gets a resource ready for dispatch to the remote endpoint
func (h *LibraryElementHandler) Prepare(existing, resource grizzly.Resource) *grizzly.Resource {
if existing != nil {
val := existing.GetSpecValue("version")
resource.SetSpecValue("version", val)
}
resource.DeleteSpecKey("meta")
return &resource
}

// GetUID returns the UID for a resource
func (h *LibraryElementHandler) GetUID(resource grizzly.Resource) (string, error) {
return resource.Name(), nil
}

// GetByUID retrieves JSON for a resource from an endpoint, by UID
func (h *LibraryElementHandler) GetByUID(UID string) (*grizzly.Resource, error) {
resource, err := h.getRemoteLibraryElement(UID)
if err != nil {
return nil, fmt.Errorf("Error retrieving library element %s: %w", UID, err)
}

return resource, nil
}

// GetRemote retrieves an element as a resource
func (h *LibraryElementHandler) GetRemote(resource grizzly.Resource) (*grizzly.Resource, error) {
return h.getRemoteLibraryElement(resource.Name())
}

// ListRemote retrieves as list of UIDs of all remote resources
func (h *LibraryElementHandler) ListRemote() ([]string, error) {
return h.listElements()
}

// Add pushes a new element to Grafana via the API
func (h *LibraryElementHandler) Add(resource grizzly.Resource) error {
return h.createElement(resource)
}

// Update pushes an element to Grafana via the API
func (h *LibraryElementHandler) Update(existing, resource grizzly.Resource) error {
return h.updateElement(existing, resource)
}

func (h *LibraryElementHandler) listElements() ([]string, error) {
params := library.NewGetLibraryElementsParams()
client, err := h.Provider.(ClientProvider).Client()
if err != nil {
return nil, err
}
elemsOK, err := client.LibraryElements.GetLibraryElements(params, nil)
if err != nil {
return nil, err
}
elems := elemsOK.GetPayload().Result.Elements
uids := make([]string, len(elems))
for i, e := range elems {
uids[i] = e.UID
}
return uids, nil
}

func (h *LibraryElementHandler) updateElement(existing, resource grizzly.Resource) error {
data, err := json.Marshal(resource.Spec())
if err != nil {
return err
}
var command models.PatchLibraryElementCommand
err = json.Unmarshal(data, &command)
if err != nil {
return err
}
client, err := h.Provider.(ClientProvider).Client()
if err != nil {
return err
}
_, err = client.LibraryElements.UpdateLibraryElement(resource.UID(), &command)
return err
}

func (h *LibraryElementHandler) createElement(resource grizzly.Resource) error {
data, err := json.Marshal(resource.Spec())
if err != nil {
return err
}
var command models.CreateLibraryElementCommand
err = json.Unmarshal(data, &command)
if err != nil {
return err
}
client, err := h.Provider.(ClientProvider).Client()
if err != nil {
return err
}
_, err = client.LibraryElements.CreateLibraryElement(&command, nil)
return err
}

func (h *LibraryElementHandler) getRemoteLibraryElement(uid string) (*grizzly.Resource, error) {
client, err := h.Provider.(ClientProvider).Client()
if err != nil {
return nil, err
}
libraryElementsOk, err := client.LibraryElements.GetLibraryElementByUID(uid, nil)
if err != nil {
var gErr *library.GetLibraryElementByUIDNotFound
if errors.As(err, &gErr) {
return nil, grizzly.ErrNotFound
}
return nil, err
}
libraryElement := libraryElementsOk.GetPayload()

spec, err := structToMap(libraryElement.Result)
if err != nil {
return nil, err
}

resource := grizzly.NewResource(h.APIVersion(), h.Kind(), uid, spec)
return &resource, nil
}
59 changes: 59 additions & 0 deletions pkg/grafana/library-elements_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package grafana

import (
"encoding/json"
"os"
"testing"

"github.com/grafana/grizzly/pkg/grizzly"
. "github.com/grafana/grizzly/pkg/internal/testutil"
"github.com/stretchr/testify/require"
)

func TestLibraryElements(t *testing.T) {
InitialiseTestConfig()
handler := NewLibraryElementHandler(NewProvider())

ticker := PingService(GetUrl())
defer ticker.Stop()

t.Run("create libraryElement - success", func(t *testing.T) {
libraryElement, err := os.ReadFile("testdata/test_json/post_library-element.json")
require.NoError(t, err)

var resource grizzly.Resource

err = json.Unmarshal(libraryElement, &resource)
require.NoError(t, err)

err = handler.Add(resource)
require.NoError(t, err)

remoteLibraryElement, err := handler.GetByUID("example-panel")
require.NoError(t, err)
require.NotNil(t, remoteLibraryElement)
require.Equal(t, remoteLibraryElement.GetSpecValue("name").(string), "Example Panel")
})

t.Run("get remote libraryElement - success", func(t *testing.T) {
resource, err := handler.GetByUID("example-panel")
require.NoError(t, err)

require.Equal(t, "grizzly.grafana.com/v1alpha1", resource.APIVersion())
require.Equal(t, "example-panel", resource.Name())
require.Len(t, resource.Spec(), 9)
})

t.Run("get remote libraryElement - not found", func(t *testing.T) {
_, err := handler.GetByUID("dummy")
require.ErrorContains(t, err, "Error retrieving library element dummy: not found")
})

t.Run("get libraryElements list", func(t *testing.T) {
resources, err := handler.ListRemote()
require.NoError(t, err)

require.NotNil(t, resources)
require.Len(t, resources, 1)
})
}
1 change: 1 addition & 0 deletions pkg/grafana/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func (p *Provider) GetHandlers() []grizzly.Handler {
return []grizzly.Handler{
NewDatasourceHandler(p),
NewFolderHandler(p),
NewLibraryElementHandler(p),
NewDashboardHandler(p),
NewRuleHandler(p),
NewSyntheticMonitoringHandler(p),
Expand Down
42 changes: 42 additions & 0 deletions pkg/grafana/testdata/test_json/post_library-element.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"apiVersion": "grizzly.grafana.com/v1alpha1",
"kind": "LibraryElement",
"metadata": {
"name": "example-panel"
},
"spec": {
"id": 3,
"kind": 1,
"model": {
"datasource": {
"type": "prometheus",
"uid": "grafanacloud-prom"
},
"description": "",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"content": "Example Content",
"mode": "markdown"
},
"pluginVersion": "10.3.0-64167",
"title": "Example Panel",
"type": "text"
},
"name": "Example Panel",
"orgId": 1,
"type": "text",
"uid": "example-panel",
"version": 2
}
}