mirror of
https://github.com/paymog/groundcover-cli.git
synced 2026-09-14 20:36:51 +08:00
feat(raw): add grafana dashboard endpoints
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# Napkin
|
||||
|
||||
## Corrections
|
||||
| Date | Source | What Went Wrong | What To Do Instead |
|
||||
|------|--------|----------------|-------------------|
|
||||
| 2026-07-06 | self | Ran the HAR generator directly on a narrow dashboard HAR and it overwrote the broad raw command registry with only 24 commands | For additive HAR integrations, preserve the generated baseline and add/merge new commands instead of replacing unrelated captured endpoints |
|
||||
|
||||
## User Preferences
|
||||
- Always commit any changes made to this napkin file.
|
||||
|
||||
## Patterns That Work
|
||||
- (approaches that succeeded)
|
||||
|
||||
## Patterns That Don't Work
|
||||
- (approaches that failed and why)
|
||||
|
||||
## Domain Notes
|
||||
- (project/domain context that matters)
|
||||
@@ -160,6 +160,10 @@ groundcover raw k8s clusters list
|
||||
groundcover raw dashboards get --dashboard-id <id>
|
||||
groundcover raw metrics query-range --body-file body.json
|
||||
groundcover raw prometheus api query --query query='up'
|
||||
groundcover raw grafana dashboards get --dashboard-uid <uid>
|
||||
groundcover raw grafana dashboards save --body-file dashboard.json
|
||||
groundcover raw grafana folders list
|
||||
groundcover raw grafana ds query --body-file query.json
|
||||
```
|
||||
|
||||
Raw commands support:
|
||||
|
||||
@@ -44,7 +44,7 @@ func NewCommand(cfg *config.Config) *cobra.Command {
|
||||
}
|
||||
|
||||
func printCommands(cmd *cobra.Command, filter string) {
|
||||
for _, command := range Commands {
|
||||
for _, command := range allCommands() {
|
||||
name := command.Key()
|
||||
if filter != "" && !strings.Contains(name, filter) {
|
||||
continue
|
||||
|
||||
+28
-1
@@ -2,6 +2,7 @@ package raw
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -14,6 +15,7 @@ type Command struct {
|
||||
DefaultQuery map[string]string
|
||||
DefaultBody json.RawMessage
|
||||
BodyContentType string
|
||||
WebApp bool
|
||||
}
|
||||
|
||||
func (c Command) Key() string {
|
||||
@@ -21,7 +23,7 @@ func (c Command) Key() string {
|
||||
}
|
||||
|
||||
func Find(tokens []string) (Command, bool) {
|
||||
for _, command := range Commands {
|
||||
for _, command := range allCommands() {
|
||||
if len(command.Name) != len(tokens) {
|
||||
continue
|
||||
}
|
||||
@@ -39,6 +41,31 @@ func Find(tokens []string) (Command, bool) {
|
||||
return Command{}, false
|
||||
}
|
||||
|
||||
func allCommands() []Command {
|
||||
commands := make([]Command, 0, len(Commands)+len(ExtraCommands))
|
||||
commands = append(commands, Commands...)
|
||||
commands = append(commands, ExtraCommands...)
|
||||
byName := map[string]Command{}
|
||||
for _, command := range commands {
|
||||
if _, exists := byName[command.Key()]; exists {
|
||||
continue
|
||||
}
|
||||
byName[command.Key()] = command
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(byName))
|
||||
for key := range byName {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
ordered := make([]Command, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
ordered = append(ordered, byName[key])
|
||||
}
|
||||
return ordered
|
||||
}
|
||||
|
||||
func kebab(value string) string {
|
||||
var out strings.Builder
|
||||
for i, r := range value {
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package raw
|
||||
|
||||
var ExtraCommands = []Command{
|
||||
{
|
||||
Name: []string{"grafana", "search"},
|
||||
Method: "GET",
|
||||
Path: "/grafana/api/search",
|
||||
Description: "GET /grafana/api/search",
|
||||
DefaultQuery: map[string]string{"limit": "50", "page": "1", "type": "dash-db"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "search", "sorting"},
|
||||
Method: "GET",
|
||||
Path: "/grafana/api/search/sorting",
|
||||
Description: "GET /grafana/api/search/sorting",
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "dashboards", "save"},
|
||||
Method: "POST",
|
||||
Path: "/grafana/api/dashboards/db",
|
||||
Description: "POST /grafana/api/dashboards/db",
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "dashboards", "get"},
|
||||
Method: "GET",
|
||||
Path: "/grafana/api/dashboards/uid/:dashboardUid",
|
||||
Description: "GET /grafana/api/dashboards/uid/:dashboardUid",
|
||||
PathParams: []string{"dashboardUid"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "dashboards", "delete"},
|
||||
Method: "DELETE",
|
||||
Path: "/grafana/api/dashboards/uid/:dashboardUid",
|
||||
Description: "DELETE /grafana/api/dashboards/uid/:dashboardUid",
|
||||
PathParams: []string{"dashboardUid"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "dashboards", "permissions", "get"},
|
||||
Method: "GET",
|
||||
Path: "/grafana/api/dashboards/uid/:dashboardUid/permissions",
|
||||
Description: "GET /grafana/api/dashboards/uid/:dashboardUid/permissions",
|
||||
PathParams: []string{"dashboardUid"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "dashboards", "permissions", "update"},
|
||||
Method: "POST",
|
||||
Path: "/grafana/api/dashboards/uid/:dashboardUid/permissions",
|
||||
Description: "POST /grafana/api/dashboards/uid/:dashboardUid/permissions",
|
||||
PathParams: []string{"dashboardUid"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "dashboards", "versions"},
|
||||
Method: "GET",
|
||||
Path: "/grafana/api/dashboards/id/:dashboardId/versions",
|
||||
Description: "GET /grafana/api/dashboards/id/:dashboardId/versions",
|
||||
PathParams: []string{"dashboardId"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "dashboards", "versions", "get"},
|
||||
Method: "GET",
|
||||
Path: "/grafana/api/dashboards/id/:dashboardId/versions/:version",
|
||||
Description: "GET /grafana/api/dashboards/id/:dashboardId/versions/:version",
|
||||
PathParams: []string{"dashboardId", "version"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "dashboards", "versions", "restore"},
|
||||
Method: "POST",
|
||||
Path: "/grafana/api/dashboards/id/:dashboardId/restore",
|
||||
Description: "POST /grafana/api/dashboards/id/:dashboardId/restore",
|
||||
PathParams: []string{"dashboardId"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "folders", "list"},
|
||||
Method: "GET",
|
||||
Path: "/grafana/api/folders",
|
||||
Description: "GET /grafana/api/folders",
|
||||
DefaultQuery: map[string]string{"limit": "50", "page": "1"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "folders", "get"},
|
||||
Method: "GET",
|
||||
Path: "/grafana/api/folders/:folderUid",
|
||||
Description: "GET /grafana/api/folders/:folderUid",
|
||||
PathParams: []string{"folderUid"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "folders", "create"},
|
||||
Method: "POST",
|
||||
Path: "/grafana/api/folders",
|
||||
Description: "POST /grafana/api/folders",
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "folders", "update"},
|
||||
Method: "PUT",
|
||||
Path: "/grafana/api/folders/:folderUid",
|
||||
Description: "PUT /grafana/api/folders/:folderUid",
|
||||
PathParams: []string{"folderUid"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "folders", "delete"},
|
||||
Method: "DELETE",
|
||||
Path: "/grafana/api/folders/:folderUid",
|
||||
Description: "DELETE /grafana/api/folders/:folderUid",
|
||||
PathParams: []string{"folderUid"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "folders", "permissions", "get"},
|
||||
Method: "GET",
|
||||
Path: "/grafana/api/folders/:folderUid/permissions",
|
||||
Description: "GET /grafana/api/folders/:folderUid/permissions",
|
||||
PathParams: []string{"folderUid"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "folders", "permissions", "update"},
|
||||
Method: "POST",
|
||||
Path: "/grafana/api/folders/:folderUid/permissions",
|
||||
Description: "POST /grafana/api/folders/:folderUid/permissions",
|
||||
PathParams: []string{"folderUid"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "prometheus", "rules"},
|
||||
Method: "GET",
|
||||
Path: "/grafana/api/prometheus/grafana/api/v1/rules",
|
||||
Description: "GET /grafana/api/prometheus/grafana/api/v1/rules",
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "annotations", "list"},
|
||||
Method: "GET",
|
||||
Path: "/grafana/api/annotations",
|
||||
Description: "GET /grafana/api/annotations",
|
||||
DefaultQuery: map[string]string{"limit": "100", "matchAny": "false"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "annotations", "create"},
|
||||
Method: "POST",
|
||||
Path: "/grafana/api/annotations",
|
||||
Description: "POST /grafana/api/annotations",
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "annotations", "update"},
|
||||
Method: "PUT",
|
||||
Path: "/grafana/api/annotations/:annotationId",
|
||||
Description: "PUT /grafana/api/annotations/:annotationId",
|
||||
PathParams: []string{"annotationId"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "annotations", "delete"},
|
||||
Method: "DELETE",
|
||||
Path: "/grafana/api/annotations/:annotationId",
|
||||
Description: "DELETE /grafana/api/annotations/:annotationId",
|
||||
PathParams: []string{"annotationId"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "datasources", "label-values"},
|
||||
Method: "GET",
|
||||
Path: "/grafana/api/datasources/uid/:datasourceUid/resources/api/v1/label/:label/values",
|
||||
Description: "GET /grafana/api/datasources/uid/:datasourceUid/resources/api/v1/label/:label/values",
|
||||
PathParams: []string{"datasourceUid", "label"},
|
||||
WebApp: true,
|
||||
},
|
||||
{
|
||||
Name: []string{"grafana", "ds", "query"},
|
||||
Method: "POST",
|
||||
Path: "/grafana/api/ds/query",
|
||||
Description: "POST /grafana/api/ds/query",
|
||||
WebApp: true,
|
||||
},
|
||||
}
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const defaultWebAppBaseURL = "https://app.groundcover.com"
|
||||
|
||||
func Run(command Command, cfg config.Config, opts Options, out io.Writer) error {
|
||||
if err := cfg.RequireAPIKey(); err != nil {
|
||||
return err
|
||||
@@ -76,7 +78,11 @@ func buildURL(command Command, cfg config.Config, opts Options) (*url.URL, error
|
||||
path = strings.ReplaceAll(path, ":"+param, url.PathEscape(value))
|
||||
}
|
||||
|
||||
base, err := url.Parse(cfg.NormalizedBaseURL())
|
||||
baseURL := cfg.NormalizedBaseURL()
|
||||
if command.WebApp && baseURL == config.DefaultBaseURL {
|
||||
baseURL = defaultWebAppBaseURL
|
||||
}
|
||||
base, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package raw
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/paymog/groundcover-cli/internal/config"
|
||||
)
|
||||
|
||||
func TestSetDeep(t *testing.T) {
|
||||
target := map[string]any{}
|
||||
@@ -28,3 +32,52 @@ func TestFind(t *testing.T) {
|
||||
t.Fatalf("unexpected path %s", command.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindGrafanaCommand(t *testing.T) {
|
||||
command, ok := Find([]string{"grafana", "dashboards", "get"})
|
||||
if !ok {
|
||||
t.Fatalf("expected grafana dashboards get command")
|
||||
}
|
||||
if command.Path != "/grafana/api/dashboards/uid/:dashboardUid" {
|
||||
t.Fatalf("unexpected path %s", command.Path)
|
||||
}
|
||||
if len(command.PathParams) != 1 || command.PathParams[0] != "dashboardUid" {
|
||||
t.Fatalf("unexpected path params %#v", command.PathParams)
|
||||
}
|
||||
if !command.WebApp {
|
||||
t.Fatalf("expected grafana command to target webapp host")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildURLUsesWebAppDefault(t *testing.T) {
|
||||
command, ok := Find([]string{"grafana", "dashboards", "get"})
|
||||
if !ok {
|
||||
t.Fatalf("expected grafana dashboards get command")
|
||||
}
|
||||
requestURL, err := buildURL(command, config.Config{BaseURL: config.DefaultBaseURL}, Options{
|
||||
PathValues: map[string]string{"dashboardUid": "streamling-pipeline-slo"},
|
||||
Query: []string{"orgId=1"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("buildURL failed: %v", err)
|
||||
}
|
||||
if got, want := requestURL.String(), "https://app.groundcover.com/grafana/api/dashboards/uid/streamling-pipeline-slo?orgId=1"; got != want {
|
||||
t.Fatalf("unexpected URL\n got: %s\nwant: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildURLHonorsCustomBaseURL(t *testing.T) {
|
||||
command, ok := Find([]string{"grafana", "folders", "get"})
|
||||
if !ok {
|
||||
t.Fatalf("expected grafana folders get command")
|
||||
}
|
||||
requestURL, err := buildURL(command, config.Config{BaseURL: "https://groundcover.example"}, Options{
|
||||
PathValues: map[string]string{"folderUid": "bend1nm1f0ruod"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("buildURL failed: %v", err)
|
||||
}
|
||||
if got, want := requestURL.String(), "https://groundcover.example/grafana/api/folders/bend1nm1f0ruod"; got != want {
|
||||
t.Fatalf("unexpected URL\n got: %s\nwant: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
+110
-11
@@ -40,12 +40,19 @@ type command struct {
|
||||
PathParams []string
|
||||
DefaultQuery map[string]string
|
||||
DefaultBody []byte
|
||||
WebApp bool
|
||||
}
|
||||
|
||||
var (
|
||||
uuidPattern = regexp.MustCompile(`(?i)^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
||||
uuidPattern = regexp.MustCompile(`(?i)^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
||||
numericPattern = regexp.MustCompile(`^\d+$`)
|
||||
versionPattern = regexp.MustCompile(`^v\d+$`)
|
||||
excludedPaths = map[string]bool{"/api/track/events": true}
|
||||
excludedPaths = map[string]bool{
|
||||
"/api/track/events": true,
|
||||
"/grafana/api/access-control/user/actions": true,
|
||||
"/grafana/api/frontend/assets": true,
|
||||
"/grafana/api/frontend-metrics": true,
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -73,7 +80,7 @@ func main() {
|
||||
if !strings.HasSuffix(parsedURL.Hostname(), "groundcover.com") {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(parsedURL.Path, "/api/") {
|
||||
if !supportedPath(parsedURL.Path) {
|
||||
continue
|
||||
}
|
||||
if excludedPaths[parsedURL.Path] || entry.Response.Status >= 400 {
|
||||
@@ -99,13 +106,14 @@ func main() {
|
||||
}
|
||||
|
||||
byEndpoint[key] = command{
|
||||
Name: commandName(parsedURL.Path),
|
||||
Name: commandName(method, path),
|
||||
Method: method,
|
||||
Path: path,
|
||||
Description: method + " " + path,
|
||||
PathParams: params,
|
||||
DefaultQuery: defaultQuery,
|
||||
DefaultBody: parseBody(entry.Request.PostData),
|
||||
WebApp: strings.HasPrefix(parsedURL.Path, "/grafana/api/"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,15 +140,30 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func commandName(path string) []string {
|
||||
parts := strings.Split(strings.TrimPrefix(path, "/api/"), "/")
|
||||
func supportedPath(path string) bool {
|
||||
return strings.HasPrefix(path, "/api/") || strings.HasPrefix(path, "/grafana/api/")
|
||||
}
|
||||
|
||||
func commandName(method string, path string) []string {
|
||||
name := []string{}
|
||||
trimmed := path
|
||||
if strings.HasPrefix(path, "/grafana/api/") {
|
||||
name = append(name, "grafana")
|
||||
trimmed = strings.TrimPrefix(path, "/grafana/api/")
|
||||
} else {
|
||||
trimmed = strings.TrimPrefix(path, "/api/")
|
||||
}
|
||||
|
||||
parts := strings.Split(trimmed, "/")
|
||||
for i, part := range parts {
|
||||
if part == "" || versionPattern.MatchString(part) {
|
||||
continue
|
||||
}
|
||||
if uuidPattern.MatchString(part) {
|
||||
if i == 0 || len(name) == 0 || name[len(name)-1] != "get" {
|
||||
if (part == "uid" || part == "id") && i+1 < len(parts) && strings.HasPrefix(parts[i+1], ":") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(part, ":") {
|
||||
if method == "GET" && i == len(parts)-1 && (len(name) == 0 || name[len(name)-1] != "get") {
|
||||
name = append(name, "get")
|
||||
}
|
||||
continue
|
||||
@@ -154,11 +177,13 @@ func normalizedPath(path string) (string, []string) {
|
||||
parts := strings.Split(path, "/")
|
||||
params := []string{}
|
||||
for i, part := range parts {
|
||||
if !uuidPattern.MatchString(part) {
|
||||
if !isDynamicPathPart(parts, i, part) {
|
||||
continue
|
||||
}
|
||||
name := paramName(previousPart(parts, i))
|
||||
params = append(params, name)
|
||||
name := dynamicParamName(parts, i)
|
||||
if !contains(params, name) {
|
||||
params = append(params, name)
|
||||
}
|
||||
parts[i] = ":" + name
|
||||
}
|
||||
return strings.Join(parts, "/"), params
|
||||
@@ -173,6 +198,69 @@ func previousPart(parts []string, index int) string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
func isDynamicPathPart(parts []string, index int, part string) bool {
|
||||
if uuidPattern.MatchString(part) {
|
||||
return true
|
||||
}
|
||||
if !isGrafanaPath(parts) {
|
||||
return false
|
||||
}
|
||||
previous := previousPart(parts, index)
|
||||
switch previous {
|
||||
case "uid", "label":
|
||||
return true
|
||||
case "folders":
|
||||
return part != "folders"
|
||||
case "annotations", "id", "versions":
|
||||
return numericPattern.MatchString(part)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isGrafanaPath(parts []string) bool {
|
||||
return len(parts) > 3 && parts[1] == "grafana" && parts[2] == "api"
|
||||
}
|
||||
|
||||
func dynamicParamName(parts []string, index int) string {
|
||||
previous := previousPart(parts, index)
|
||||
if isGrafanaPath(parts) {
|
||||
switch previous {
|
||||
case "uid":
|
||||
return uidParamName(previousPartBefore(parts, index-1))
|
||||
case "folders":
|
||||
return "folderUid"
|
||||
case "label":
|
||||
return "label"
|
||||
case "annotations":
|
||||
return "annotationId"
|
||||
case "id":
|
||||
return paramName(previousPartBefore(parts, index-1))
|
||||
case "versions":
|
||||
return "version"
|
||||
}
|
||||
}
|
||||
return paramName(previous)
|
||||
}
|
||||
|
||||
func previousPartBefore(parts []string, index int) string {
|
||||
for i := index - 1; i >= 0; i-- {
|
||||
if parts[i] != "" {
|
||||
return parts[i]
|
||||
}
|
||||
}
|
||||
return "id"
|
||||
}
|
||||
|
||||
func contains(values []string, target string) bool {
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func paramName(previous string) string {
|
||||
base := strings.TrimSuffix(previous, "s")
|
||||
if base == "" {
|
||||
@@ -181,6 +269,14 @@ func paramName(previous string) string {
|
||||
return base + "Id"
|
||||
}
|
||||
|
||||
func uidParamName(previous string) string {
|
||||
base := strings.TrimSuffix(previous, "s")
|
||||
if base == "" {
|
||||
base = "id"
|
||||
}
|
||||
return base + "Uid"
|
||||
}
|
||||
|
||||
func parseBody(postData *struct {
|
||||
Text string `json:"text"`
|
||||
MimeType string `json:"mimeType"`
|
||||
@@ -254,6 +350,9 @@ func emit(commands []command) ([]byte, error) {
|
||||
if len(command.DefaultBody) > 0 {
|
||||
fmt.Fprintf(&b, "\t\tDefaultBody: json.RawMessage(%s),\n", strconv.Quote(string(command.DefaultBody)))
|
||||
}
|
||||
if command.WebApp {
|
||||
b.WriteString("\t\tWebApp: true,\n")
|
||||
}
|
||||
b.WriteString("\t},\n")
|
||||
}
|
||||
b.WriteString("}\n")
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizedPathGrafanaDashboardUID(t *testing.T) {
|
||||
path, params := normalizedPath("/grafana/api/dashboards/uid/streamling-pipeline-slo")
|
||||
if path != "/grafana/api/dashboards/uid/:dashboardUid" {
|
||||
t.Fatalf("unexpected path %s", path)
|
||||
}
|
||||
if !reflect.DeepEqual(params, []string{"dashboardUid"}) {
|
||||
t.Fatalf("unexpected params %#v", params)
|
||||
}
|
||||
|
||||
name := commandName("GET", path)
|
||||
if !reflect.DeepEqual(name, []string{"grafana", "dashboards", "get"}) {
|
||||
t.Fatalf("unexpected name %#v", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizedPathGrafanaFolderUID(t *testing.T) {
|
||||
path, params := normalizedPath("/grafana/api/folders/bend1nm1f0ruod")
|
||||
if path != "/grafana/api/folders/:folderUid" {
|
||||
t.Fatalf("unexpected path %s", path)
|
||||
}
|
||||
if !reflect.DeepEqual(params, []string{"folderUid"}) {
|
||||
t.Fatalf("unexpected params %#v", params)
|
||||
}
|
||||
|
||||
name := commandName("GET", path)
|
||||
if !reflect.DeepEqual(name, []string{"grafana", "folders", "get"}) {
|
||||
t.Fatalf("unexpected name %#v", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizedPathGrafanaDatasourceLabelValues(t *testing.T) {
|
||||
path, params := normalizedPath("/grafana/api/datasources/uid/aelovgen78268b/resources/api/v1/label/project_id/values")
|
||||
if path != "/grafana/api/datasources/uid/:datasourceUid/resources/api/v1/label/:label/values" {
|
||||
t.Fatalf("unexpected path %s", path)
|
||||
}
|
||||
if !reflect.DeepEqual(params, []string{"datasourceUid", "label"}) {
|
||||
t.Fatalf("unexpected params %#v", params)
|
||||
}
|
||||
|
||||
name := commandName("GET", path)
|
||||
want := []string{"grafana", "datasources", "resources", "api", "label", "values"}
|
||||
if !reflect.DeepEqual(name, want) {
|
||||
t.Fatalf("unexpected name %#v", name)
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,7 @@ Reach for `groundcover raw …` for any of these; the SDK has the *parent* resou
|
||||
- **traces:** `attributes`, `details`, `errors`, `filters`, `insights`, `latencies`, `requests`, `values-distribution` (SDK only has `search`)
|
||||
- **metrics:** `cardinality`, `cardinality-graph`, `discovery`, `labels-cardinality`, `query-range`, `resources errors|latencies|list|requests` (SDK has `query`, `names`, `keys`, `values`)
|
||||
- **prometheus:** `prometheus api query` (raw Prom passthrough — handy for ad-hoc PromQL via `--query query='up'`)
|
||||
- **Grafana dashboards:** `grafana search`, `grafana dashboards get|save|delete`, `grafana dashboards permissions get|update`, `grafana dashboards versions|get|restore`, `grafana folders list|get|create|update|delete`, `grafana folders permissions get|update`, `grafana annotations list|create|update|delete`, `grafana prometheus rules`, `grafana datasources label-values`, `grafana ds query`
|
||||
- **monitors drilldowns:** `instances filters|query|timeline`, `labels keys`, `silences`, `summary filters|query`, `timeline` (SDK only has CRUD)
|
||||
- **k8s drilldowns:** `configmaps|cronjob|daemonsets|deployments|jobs|pods|pvcs|replicasets|statefulsets list`, `container info`, `context events`, `namespaces info|list`, `nodes info-with-resources|list|resources|usage top10`, `pod container usage`, `pods status-over-time`, `workloads availability|events|usage top10`, `network connections|cross-az|cross-az-regions|partners|throughput|top-connections`, `events search-time-series` (SDK only has `clusters`, `workloads`, `events-search`, `events-over-time`)
|
||||
- **infra:** `infra hosts info-with-resources`
|
||||
@@ -205,8 +206,29 @@ groundcover raw k8s clusters list
|
||||
groundcover raw dashboards get --dashboard-id <id>
|
||||
groundcover raw metrics query-range --body-file body.json
|
||||
groundcover raw prometheus api query --query query='up'
|
||||
groundcover raw grafana dashboards get --dashboard-uid <uid>
|
||||
groundcover raw grafana dashboards save --body-file dashboard.json
|
||||
groundcover raw grafana folders list
|
||||
groundcover raw grafana ds query --body-file query.json
|
||||
```
|
||||
|
||||
|
||||
### Grafana native dashboards
|
||||
|
||||
Groundcover also embeds Grafana at `/grafana`. These are **not** the same as Groundcover's first-class `dashboards` SDK resource, so use `raw grafana …` when you need native Grafana JSON dashboards, folders, permissions, annotations, datasource-backed variable values, or panel query execution.
|
||||
|
||||
Common commands:
|
||||
```sh
|
||||
groundcover raw grafana search --query query='service slo' --query folderUIDs=general
|
||||
groundcover raw grafana dashboards get --dashboard-uid streamling-pipeline-slo
|
||||
groundcover raw grafana dashboards save --body-file dashboard.json
|
||||
groundcover raw grafana folders list
|
||||
groundcover raw grafana datasources label-values --datasource-uid <uid> --label project_id --query start=<unix> --query end=<unix>
|
||||
groundcover raw grafana ds query --query ds_type=prometheus --body-file query.json
|
||||
```
|
||||
|
||||
Grafana raw commands default to `https://app.groundcover.com` (the webapp host), while normal API/SDK commands default to `https://api.groundcover.com`. Pass `--base-url` only for non-standard deployments.
|
||||
|
||||
Raw flags: `--body-json`, `--body-file`, `--set dotted.path=value`, `--query key=value` (repeatable), generated path flags (e.g. `--dashboard-id`), `--raw`. Many raw commands ship a default body captured from the webapp HAR, so you can run them with no `--body-*` at all and override specific fields with `--set`.
|
||||
|
||||
## Recipes
|
||||
|
||||
Reference in New Issue
Block a user