Merge branch 'main' into kristina/rtk-corr
This commit is contained in:
+4
-4
@@ -561,6 +561,7 @@
|
||||
/packages/grafana-data/src/themes/ @grafana/grafana-frontend-platform
|
||||
/packages/grafana-data/src/transformations/ @grafana/datapro
|
||||
/packages/grafana-data/src/types/ @grafana/grafana-frontend-platform
|
||||
/packages/grafana-data/src/types/scopes.ts @grafana/grafana-operator-experience-squad
|
||||
/packages/grafana-data/src/utils/__snapshots__/ @grafanabot
|
||||
/packages/grafana-data/src/utils/anyToNumber.ts @grafana/grafana-frontend-platform
|
||||
/packages/grafana-data/src/utils/arrayUtils* @grafana/grafana-frontend-platform
|
||||
@@ -740,6 +741,9 @@
|
||||
# @grafana/test-utils
|
||||
/packages/grafana-test-utils @grafana/grafana-frontend-platform
|
||||
|
||||
# @grafana/api-clients
|
||||
/packages/grafana-api-clients/ @grafana/grafana-frontend-platform @grafana/grafana-search-navigate-organise
|
||||
|
||||
# root files, mostly frontend
|
||||
/.browserslistrc @grafana/frontend-ops
|
||||
/package.json @grafana/frontend-ops
|
||||
@@ -958,7 +962,6 @@ playwright.storybook.config.ts @grafana/grafana-frontend-platform
|
||||
/public/app/features/transformers/timeSeriesTable/ @grafana/dataviz-squad @grafana/app-o11y-visualizations
|
||||
/public/app/features/users/ @grafana/access-squad
|
||||
/public/app/features/variables/ @grafana/dashboards-squad
|
||||
/public/app/features/preferences/ @grafana/grafana-frontend-platform
|
||||
/public/app/features/bookmarks/ @grafana/grafana-search-navigate-organise
|
||||
/public/app/plugins/panel/* @grafana/dataviz-squad
|
||||
/public/app/plugins/panel/alertlist/ @grafana/alerting-frontend
|
||||
@@ -1054,13 +1057,10 @@ playwright.storybook.config.ts @grafana/grafana-frontend-platform
|
||||
/scripts/trigger_windows_build.sh @grafana/grafana-developer-enablement-squad
|
||||
/scripts/cleanup-husky.sh @grafana/frontend-ops
|
||||
/scripts/verify-repo-update/ @grafana/grafana-developer-enablement-squad
|
||||
/scripts/generate-rtk-apis.ts @grafana/grafana-frontend-platform
|
||||
/scripts/process-specs.ts @grafana/grafana-frontend-platform
|
||||
/scripts/generate-alerting-rtk-apis.ts @grafana/alerting-frontend
|
||||
/scripts/levitate-parse-json-report.js @grafana/plugins-platform-frontend
|
||||
/scripts/levitate-show-affected-plugins.js @grafana/plugins-platform-frontend
|
||||
/scripts/codemods/explicit-barrel-imports.cjs @grafana/frontend-ops
|
||||
/scripts/rtk-client-generator/ @grafana/grafana-search-navigate-organise
|
||||
|
||||
/scripts/codeowners-manifest/ @grafana/dataviz-squad
|
||||
/scripts/test-coverage-by-codeowner.js @grafana/dataviz-squad
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: grafana/shared-workflows/actions/cleanup-branches@cleanup-branches/v1.0.0
|
||||
- uses: grafana/shared-workflows/actions/cleanup-branches@cleanup-branches/v0.2.1
|
||||
with:
|
||||
dry-run: true
|
||||
max-date: "1 month ago"
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
id: detect-changes
|
||||
uses: ./.github/actions/change-detection
|
||||
with:
|
||||
self: .github/workflows/pr-test-integration.yml
|
||||
self: .github/workflows/pr-test-docker.yml
|
||||
|
||||
build-dockerfile:
|
||||
needs: detect-changes
|
||||
|
||||
@@ -101,7 +101,8 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
readarray -t PACKAGES <<< "$(./scripts/ci/backend-tests/pkgs-with-tests-named.sh -b TestIntegration | ./scripts/ci/backend-tests/shard.sh -N"$SHARD" -d-)"
|
||||
CGO_ENABLED=0 go test -tags=sqlite -timeout=8m -run '^TestIntegration' "${PACKAGES[@]}"
|
||||
# ionice since tests are IO intensive
|
||||
CGO_ENABLED=0 ionice -c2 -n7 go test -p=4 -tags=sqlite -timeout=8m -run '^TestIntegration' "${PACKAGES[@]}"
|
||||
mysql:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.changed == 'true'
|
||||
|
||||
@@ -156,16 +156,16 @@ jobs:
|
||||
artifacts: targz:grafana:linux/arm/v6,deb:grafana:linux/arm/v6
|
||||
verify: true
|
||||
- name: windows-amd64
|
||||
artifacts: targz:grafana:windows/amd64,zip:grafana:windows/amd64,msi:grafana:windows/amd64
|
||||
artifacts: targz:grafana:windows/amd64:nocgo,zip:grafana:windows/amd64:nocgo,msi:grafana:windows/amd64:nocgo
|
||||
verify: true
|
||||
- name: windows-arm64
|
||||
artifacts: targz:grafana:windows/arm64,zip:grafana:windows/arm64
|
||||
artifacts: targz:grafana:windows/arm64:nocgo,zip:grafana:windows/arm64:nocgo
|
||||
verify: true
|
||||
- name: darwin-amd64
|
||||
artifacts: targz:grafana:darwin/amd64
|
||||
artifacts: targz:grafana:darwin/amd64:nocgo
|
||||
verify: true
|
||||
- name: darwin-arm64
|
||||
artifacts: targz:grafana:darwin/arm64
|
||||
artifacts: targz:grafana:darwin/arm64:nocgo
|
||||
verify: true
|
||||
steps:
|
||||
- uses: grafana/shared-workflows/actions/dockerhub-login@dockerhub-login/v1.0.2
|
||||
|
||||
+327
-327
File diff suppressed because one or more lines are too long
+1
-1
@@ -25,6 +25,6 @@ plugins:
|
||||
path: .yarn/plugins/@yarnpkg/plugin-licenses.cjs
|
||||
spec: "https://raw.githubusercontent.com/mhassan1/yarn-plugin-licenses/v0.15.0/bundles/@yarnpkg/plugin-licenses.js"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.4.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.10.3.cjs
|
||||
|
||||
enableScripts: false
|
||||
|
||||
@@ -69,12 +69,12 @@ func New(cfg app.Config) (app.App, error) {
|
||||
go func() {
|
||||
logger := log.WithContext(ctx).With("check", check.ID())
|
||||
logger.Debug("Processing check", "namespace", req.Object.GetNamespace())
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
orgID, err := getOrgIDFromNamespace(req.Object.GetNamespace())
|
||||
if err != nil {
|
||||
logger.Error("Error getting requester", "error", err)
|
||||
logger.Error("Error getting org ID from namespace", "error", err)
|
||||
return
|
||||
}
|
||||
ctx = identity.WithServiceIdentityContext(context.WithoutCancel(ctx), requester.GetOrgID())
|
||||
ctx = identity.WithServiceIdentityContext(context.WithoutCancel(ctx), orgID)
|
||||
err = processCheck(ctx, logger, client, typesClient, req.Object, check)
|
||||
if err != nil {
|
||||
logger.Error("Error processing check", "error", err)
|
||||
@@ -85,12 +85,12 @@ func New(cfg app.Config) (app.App, error) {
|
||||
go func() {
|
||||
logger := log.WithContext(ctx).With("check", check.ID())
|
||||
logger.Debug("Updating check", "namespace", req.Object.GetNamespace(), "name", req.Object.GetName())
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
orgID, err := getOrgIDFromNamespace(req.Object.GetNamespace())
|
||||
if err != nil {
|
||||
logger.Error("Error getting requester", "error", err)
|
||||
logger.Error("Error getting org ID from namespace", "error", err)
|
||||
return
|
||||
}
|
||||
ctx = identity.WithServiceIdentityContext(context.WithoutCancel(ctx), requester.GetOrgID())
|
||||
ctx = identity.WithServiceIdentityContext(context.WithoutCancel(ctx), orgID)
|
||||
err = processCheckRetry(ctx, logger, client, typesClient, req.Object, check)
|
||||
if err != nil {
|
||||
logger.Error("Error processing check retry", "error", err)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||
@@ -91,4 +92,5 @@ type AdvisorAppConfig struct {
|
||||
CheckRegistry CheckService
|
||||
PluginConfig map[string]string
|
||||
StackID string
|
||||
OrgService org.Service
|
||||
}
|
||||
|
||||
@@ -54,7 +54,13 @@ func New(
|
||||
}
|
||||
|
||||
func (c *check) Items(ctx context.Context) ([]any, error) {
|
||||
dss, err := c.DatasourceSvc.GetAllDataSources(ctx, &datasources.GetAllDataSourcesQuery{})
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dss, err := c.DatasourceSvc.GetDataSources(ctx, &datasources.GetDataSourcesQuery{
|
||||
OrgID: requester.GetOrgID(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ type MockDatasourceSvc struct {
|
||||
dss []*datasources.DataSource
|
||||
}
|
||||
|
||||
func (m *MockDatasourceSvc) GetAllDataSources(context.Context, *datasources.GetAllDataSourcesQuery) ([]*datasources.DataSource, error) {
|
||||
func (m *MockDatasourceSvc) GetDataSources(context.Context, *datasources.GetDataSourcesQuery) ([]*datasources.DataSource, error) {
|
||||
return m.dss, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -57,15 +57,26 @@ func NewCheckReportFailureWithMoreInfo(
|
||||
}
|
||||
}
|
||||
|
||||
func GetNamespace(stackID string) (string, error) {
|
||||
if stackID == "" {
|
||||
return metav1.NamespaceDefault, nil
|
||||
func GetNamespaces(ctx context.Context, stackID string, orgService org.Service) ([]string, error) {
|
||||
var namespaces []string
|
||||
if stackID != "" {
|
||||
// Single namespace for cloud stack
|
||||
stackId, err := strconv.ParseInt(stackID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid stack id: %s", stackID)
|
||||
}
|
||||
namespaces = []string{types.CloudNamespaceFormatter(stackId)}
|
||||
} else {
|
||||
// Multiple namespaces for each org
|
||||
orgs, err := orgService.Search(ctx, &org.SearchOrgsQuery{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch orgs: %w", err)
|
||||
}
|
||||
for _, o := range orgs {
|
||||
namespaces = append(namespaces, types.OrgNamespaceFormatter(o.ID))
|
||||
}
|
||||
}
|
||||
stackId, err := strconv.ParseInt(stackID, 10, 64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid stack id: %s", stackID)
|
||||
}
|
||||
return types.CloudNamespaceFormatter(stackId), nil
|
||||
return namespaces, nil
|
||||
}
|
||||
|
||||
func GetStatusAnnotation(obj resource.Object) string {
|
||||
|
||||
@@ -1,40 +1,61 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestGetNamespace(t *testing.T) {
|
||||
func TestGetNamespaces(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
stackID string
|
||||
orgs []string
|
||||
expected []string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "empty stack ID",
|
||||
input: "",
|
||||
expected: metav1.NamespaceDefault,
|
||||
stackID: "",
|
||||
orgs: []string{"default"},
|
||||
expected: []string{metav1.NamespaceDefault},
|
||||
},
|
||||
{
|
||||
name: "valid stack ID",
|
||||
input: "1234567890",
|
||||
expected: "stacks-1234567890",
|
||||
stackID: "1234567890",
|
||||
orgs: []string{"default"},
|
||||
expected: []string{"stacks-1234567890"},
|
||||
},
|
||||
{
|
||||
name: "invalid stack ID",
|
||||
input: "invalid",
|
||||
expected: "",
|
||||
stackID: "invalid",
|
||||
orgs: []string{"default"},
|
||||
expected: nil,
|
||||
expectedErr: "invalid stack id: invalid",
|
||||
},
|
||||
{
|
||||
name: "multiple orgs and no stack ID",
|
||||
stackID: "",
|
||||
orgs: []string{"default", "org-2"},
|
||||
expected: []string{"default", "org-2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := GetNamespace(tt.input)
|
||||
fakeOrgService := &mockOrgService{
|
||||
SearchFunc: func(ctx context.Context, query *org.SearchOrgsQuery) ([]*org.OrgDTO, error) {
|
||||
orgs := make([]*org.OrgDTO, len(tt.orgs))
|
||||
for i, o := range tt.orgs {
|
||||
orgs[i] = &org.OrgDTO{ID: int64(i + 1), Name: o}
|
||||
}
|
||||
return orgs, nil
|
||||
},
|
||||
}
|
||||
result, err := GetNamespaces(context.Background(), tt.stackID, fakeOrgService)
|
||||
if tt.expectedErr != "" {
|
||||
assert.EqualError(t, err, tt.expectedErr)
|
||||
} else {
|
||||
@@ -44,3 +65,12 @@ func TestGetNamespace(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockOrgService struct {
|
||||
org.Service
|
||||
SearchFunc func(ctx context.Context, query *org.SearchOrgsQuery) ([]*org.OrgDTO, error)
|
||||
}
|
||||
|
||||
func (m *mockOrgService) Search(ctx context.Context, query *org.SearchOrgsQuery) ([]*org.OrgDTO, error) {
|
||||
return m.SearchFunc(ctx, query)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry"
|
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
@@ -37,8 +38,9 @@ type Runner struct {
|
||||
typesClient resource.Client
|
||||
defaultEvalInterval time.Duration
|
||||
maxHistory int
|
||||
namespace string
|
||||
log logging.Logger
|
||||
orgService org.Service
|
||||
stackID string
|
||||
}
|
||||
|
||||
// NewRunner creates a new Runner.
|
||||
@@ -49,6 +51,7 @@ func New(cfg app.Config, log logging.Logger) (app.Runnable, error) {
|
||||
return nil, fmt.Errorf("invalid config type")
|
||||
}
|
||||
checkRegistry := specificConfig.CheckRegistry
|
||||
orgService := specificConfig.OrgService
|
||||
evalInterval, err := getEvaluationInterval(specificConfig.PluginConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -57,10 +60,6 @@ func New(cfg app.Config, log logging.Logger) (app.Runnable, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
namespace, err := checks.GetNamespace(specificConfig.StackID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Prepare storage client
|
||||
clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{})
|
||||
@@ -79,8 +78,9 @@ func New(cfg app.Config, log logging.Logger) (app.Runnable, error) {
|
||||
typesClient: typesClient,
|
||||
defaultEvalInterval: evalInterval,
|
||||
maxHistory: maxHistory,
|
||||
namespace: namespace,
|
||||
log: log.With("runner", "advisor.checkscheduler"),
|
||||
orgService: orgService,
|
||||
stackID: specificConfig.StackID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -89,49 +89,82 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
// We still need the context to eventually be cancelled to exit this function
|
||||
// but we don't want the requests to fail because of it
|
||||
ctxWithoutCancel := context.WithoutCancel(ctx)
|
||||
lastCreated, err := r.checkLastCreated(ctxWithoutCancel, logger)
|
||||
|
||||
// Determine namespaces based on StackID or OrgID
|
||||
namespaces, err := checks.GetNamespaces(ctxWithoutCancel, r.stackID, r.orgService)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get namespaces: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("Scheduling checks", "namespaces", len(namespaces))
|
||||
|
||||
// Get the last created time for this specific namespace
|
||||
lastCreatedMap, err := r.checkLastCreated(ctx, logger, namespaces)
|
||||
if err != nil {
|
||||
logger.Error("Error getting last check creation time", "error", err)
|
||||
return err
|
||||
}
|
||||
// If there are checks already created, run an initial cleanup to remove old checks
|
||||
if !lastCreated.IsZero() {
|
||||
err = r.cleanupChecks(ctxWithoutCancel, logger)
|
||||
if err != nil {
|
||||
logger.Error("Error cleaning up old check reports", "error", err)
|
||||
return err
|
||||
|
||||
// If there are checks already created, run an initial cleanup
|
||||
for _, namespace := range namespaces {
|
||||
logger = logger.With("namespace", namespace)
|
||||
lastCreated := lastCreatedMap[namespace]
|
||||
|
||||
if !lastCreated.IsZero() {
|
||||
err = r.cleanupChecks(ctx, logger, namespace)
|
||||
if err != nil {
|
||||
logger.Error("Error cleaning up old check reports", "error", err)
|
||||
return err
|
||||
}
|
||||
err = r.markUnprocessedChecks(ctx, logger, namespace)
|
||||
if err != nil {
|
||||
logger.Error("Error marking unprocessed checks", "error", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextEvalTime := r.getNextEvalTime(r.defaultEvalInterval, lastCreated)
|
||||
nextEvalTime := r.getNextEvalTime(r.defaultEvalInterval, lastCreatedMap)
|
||||
ticker := time.NewTicker(nextEvalTime)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
lastCreated, err := r.checkLastCreated(ctxWithoutCancel, logger)
|
||||
// Get the current last created time for this namespace
|
||||
lastCreatedMap, err := r.checkLastCreated(ctx, logger, namespaces)
|
||||
if err != nil {
|
||||
logger.Error("Error getting last check creation time", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// If there are checks already created, then we can automatically create more
|
||||
if !lastCreated.IsZero() {
|
||||
err = r.createChecks(ctxWithoutCancel, logger)
|
||||
if err != nil {
|
||||
logger.Error("Error creating new check reports", "error", err)
|
||||
}
|
||||
for _, namespace := range namespaces {
|
||||
logger = logger.With("namespace", namespace)
|
||||
lastCreated := lastCreatedMap[namespace]
|
||||
|
||||
// Clean up old checks to avoid going over the limit
|
||||
err = r.cleanupChecks(ctxWithoutCancel, logger)
|
||||
if err != nil {
|
||||
logger.Error("Error cleaning up old check reports", "error", err)
|
||||
// If there are checks already created and they are older than the evaluation interval
|
||||
// then we can automatically create more
|
||||
if !lastCreated.IsZero() && lastCreated.Before(time.Now().Add(-r.defaultEvalInterval)) {
|
||||
err = r.createChecks(ctx, logger, namespace)
|
||||
if err != nil {
|
||||
logger.Error("Error creating new check reports", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Clean up old checks to avoid going over the limit
|
||||
err = r.cleanupChecks(ctx, logger, namespace)
|
||||
if err != nil {
|
||||
logger.Error("Error cleaning up old check reports", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the last created time with the new created checks
|
||||
lastCreatedMap[namespace] = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the ticker to the next send interval
|
||||
nextEvalTime = r.getNextEvalTime(r.defaultEvalInterval, lastCreated)
|
||||
nextEvalTime = r.getNextEvalTime(r.defaultEvalInterval, lastCreatedMap)
|
||||
ticker.Reset(nextEvalTime)
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
@@ -139,8 +172,8 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) listChecks(ctx context.Context, logger logging.Logger) ([]resource.Object, error) {
|
||||
list, err := r.checksClient.List(ctx, r.namespace, resource.ListOptions{
|
||||
func (r *Runner) listChecks(ctx context.Context, logger logging.Logger, namespace string) ([]resource.Object, error) {
|
||||
list, err := r.checksClient.List(ctx, namespace, resource.ListOptions{
|
||||
Limit: 1000, // Avoid pagination for normal uses cases, which is a costly operation
|
||||
})
|
||||
if err != nil {
|
||||
@@ -150,7 +183,7 @@ func (r *Runner) listChecks(ctx context.Context, logger logging.Logger) ([]resou
|
||||
checks := list.GetItems()
|
||||
for list.GetContinue() != "" {
|
||||
logger.Debug("List has continue token, listing next page", "continue", list.GetContinue())
|
||||
list, err = r.checksClient.List(ctx, r.namespace, resource.ListOptions{Continue: list.GetContinue(), Limit: 1000})
|
||||
list, err = r.checksClient.List(ctx, namespace, resource.ListOptions{Continue: list.GetContinue(), Limit: 1000})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -159,38 +192,48 @@ func (r *Runner) listChecks(ctx context.Context, logger logging.Logger) ([]resou
|
||||
return checks, nil
|
||||
}
|
||||
|
||||
// checkLastCreated returns the creation time of the last check created
|
||||
// regardless of its ID. This assumes that the checks are created in batches
|
||||
// so a batch will have a similar creation time.
|
||||
// checkLastCreated returns the creation time of the last check created for a specific namespace.
|
||||
// This assumes that the checks are created in batches so a batch will have a similar creation time.
|
||||
// In case it finds an unprocessed check from a previous run, it will set it to error.
|
||||
func (r *Runner) checkLastCreated(ctx context.Context, log logging.Logger) (time.Time, error) {
|
||||
checkList, err := r.listChecks(ctx, log)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
lastCreated := time.Time{}
|
||||
for _, item := range checkList {
|
||||
itemCreated := item.GetCreationTimestamp().Time
|
||||
if itemCreated.After(lastCreated) {
|
||||
lastCreated = itemCreated
|
||||
func (r *Runner) checkLastCreated(ctx context.Context, log logging.Logger, namespaces []string) (map[string]time.Time, error) {
|
||||
lastCreated := map[string]time.Time{}
|
||||
for _, namespace := range namespaces {
|
||||
checkList, err := r.listChecks(ctx, log, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the check is unprocessed, set it to error
|
||||
if checks.GetStatusAnnotation(item) == "" {
|
||||
log.Info("Check is unprocessed, marking as error", "check", item.GetStaticMetadata().Identifier())
|
||||
err := checks.SetStatusAnnotation(ctx, r.checksClient, item, checks.StatusAnnotationError)
|
||||
if err != nil {
|
||||
log.Error("Error setting check status to error", "error", err)
|
||||
for _, item := range checkList {
|
||||
itemCreated := item.GetCreationTimestamp().Time
|
||||
if itemCreated.After(lastCreated[namespace]) {
|
||||
lastCreated[namespace] = itemCreated
|
||||
}
|
||||
}
|
||||
}
|
||||
return lastCreated, nil
|
||||
}
|
||||
|
||||
func (r *Runner) markUnprocessedChecks(ctx context.Context, log logging.Logger, namespace string) error {
|
||||
checkList, err := r.listChecks(ctx, log, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range checkList {
|
||||
if checks.GetStatusAnnotation(item) == "" {
|
||||
log.Info("Check is unprocessed, marking as error", "check", item.GetStaticMetadata().Identifier())
|
||||
err := checks.SetStatusAnnotation(ctx, r.checksClient, item, checks.StatusAnnotationError)
|
||||
if err != nil {
|
||||
log.Error("Error setting check status to error", "error", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createChecks creates a new check for each check type in the registry.
|
||||
func (r *Runner) createChecks(ctx context.Context, logger logging.Logger) error {
|
||||
func (r *Runner) createChecks(ctx context.Context, logger logging.Logger, namespace string) error {
|
||||
// List existing CheckType objects
|
||||
list, err := r.typesClient.List(ctx, r.namespace, resource.ListOptions{})
|
||||
list, err := r.typesClient.List(ctx, namespace, resource.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error listing check types: %w", err)
|
||||
}
|
||||
@@ -200,7 +243,7 @@ func (r *Runner) createChecks(ctx context.Context, logger logging.Logger) error
|
||||
for !allChecksRegistered && retryCount < waitMaxRetries {
|
||||
logger.Info("Waiting for all check types to be registered", "retryCount", retryCount, "waitInterval", waitInterval)
|
||||
time.Sleep(waitInterval)
|
||||
list, err = r.typesClient.List(ctx, r.namespace, resource.ListOptions{})
|
||||
list, err = r.typesClient.List(ctx, namespace, resource.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error listing check types: %w", err)
|
||||
}
|
||||
@@ -218,7 +261,7 @@ func (r *Runner) createChecks(ctx context.Context, logger logging.Logger) error
|
||||
obj := &advisorv0alpha1.Check{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "check-",
|
||||
Namespace: r.namespace,
|
||||
Namespace: namespace,
|
||||
Labels: map[string]string{
|
||||
checks.TypeLabel: checkType.Spec.Name,
|
||||
},
|
||||
@@ -235,13 +278,13 @@ func (r *Runner) createChecks(ctx context.Context, logger logging.Logger) error
|
||||
}
|
||||
|
||||
// cleanupChecks deletes the olders checks if the number of checks exceeds the limit.
|
||||
func (r *Runner) cleanupChecks(ctx context.Context, logger logging.Logger) error {
|
||||
checkList, err := r.listChecks(ctx, logger)
|
||||
func (r *Runner) cleanupChecks(ctx context.Context, logger logging.Logger, namespace string) error {
|
||||
checkList, err := r.listChecks(ctx, logger, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debug("Cleaning up checks", "numChecks", len(checkList))
|
||||
logger.Debug("Cleaning up checks", "namespace", namespace, "numChecks", len(checkList))
|
||||
|
||||
// organize checks by type
|
||||
checksByType := map[string][]resource.Object{}
|
||||
@@ -294,12 +337,15 @@ func getEvaluationInterval(pluginConfig map[string]string) (time.Duration, error
|
||||
return evaluationInterval, nil
|
||||
}
|
||||
|
||||
func (r *Runner) getNextEvalTime(defaultEvaluationInterval time.Duration, lastCreated time.Time) time.Duration {
|
||||
func (r *Runner) getNextEvalTime(defaultEvaluationInterval time.Duration, lastCreated map[string]time.Time) time.Duration {
|
||||
nextEvalTime := defaultEvaluationInterval
|
||||
|
||||
baseTime := lastCreated
|
||||
if lastCreated.IsZero() {
|
||||
baseTime = time.Now()
|
||||
// Get the oldest last created time
|
||||
baseTime := time.Now()
|
||||
for _, lastNamespacedCreated := range lastCreated {
|
||||
if !lastNamespacedCreated.IsZero() && lastNamespacedCreated.Before(baseTime) {
|
||||
baseTime = lastNamespacedCreated
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the next evaluation time and add random variation
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry"
|
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
@@ -212,7 +213,8 @@ func TestRunner_Run_UnprocessedChecks(t *testing.T) {
|
||||
Items: []advisorv0alpha1.Check{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "unprocessed-check",
|
||||
Name: "unprocessed-check",
|
||||
CreationTimestamp: metav1.NewTime(time.Now().Add(-1 * time.Hour)),
|
||||
// No status annotation - unprocessed
|
||||
},
|
||||
},
|
||||
@@ -331,8 +333,9 @@ func createTestRunnerWithRegistry(checkClient, typesClient *MockClient, checkReg
|
||||
typesClient: typesClient,
|
||||
defaultEvalInterval: 5 * time.Millisecond,
|
||||
maxHistory: defaultMaxHistory,
|
||||
namespace: "test-namespace",
|
||||
log: &logging.NoOpLogger{},
|
||||
orgService: &mockOrgService{orgs: []*org.OrgDTO{{ID: 1}}},
|
||||
stackID: "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,3 +386,12 @@ func (m *mockCheck) ID() string {
|
||||
func (m *mockCheck) Steps() []checks.Step {
|
||||
return m.steps
|
||||
}
|
||||
|
||||
type mockOrgService struct {
|
||||
org.Service
|
||||
orgs []*org.OrgDTO
|
||||
}
|
||||
|
||||
func (m *mockOrgService) Search(ctx context.Context, query *org.SearchOrgsQuery) ([]*org.OrgDTO, error) {
|
||||
return m.orgs, nil
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry"
|
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
@@ -26,7 +27,8 @@ import (
|
||||
type Runner struct {
|
||||
checkRegistry checkregistry.CheckService
|
||||
client resource.Client
|
||||
namespace string
|
||||
orgService org.Service
|
||||
stackID string
|
||||
log logging.Logger
|
||||
retryAttempts int
|
||||
retryDelay time.Duration
|
||||
@@ -40,10 +42,7 @@ func New(cfg app.Config, log logging.Logger) (app.Runnable, error) {
|
||||
return nil, fmt.Errorf("invalid config type")
|
||||
}
|
||||
checkRegistry := specificConfig.CheckRegistry
|
||||
namespace, err := checks.GetNamespace(specificConfig.StackID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orgService := specificConfig.OrgService
|
||||
|
||||
// Prepare storage client
|
||||
clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{})
|
||||
@@ -55,7 +54,8 @@ func New(cfg app.Config, log logging.Logger) (app.Runnable, error) {
|
||||
return &Runner{
|
||||
checkRegistry: checkRegistry,
|
||||
client: client,
|
||||
namespace: namespace,
|
||||
orgService: orgService,
|
||||
stackID: specificConfig.StackID,
|
||||
log: log.With("runner", "advisor.checktyperegisterer"),
|
||||
retryAttempts: 5,
|
||||
retryDelay: time.Second * 10,
|
||||
@@ -64,36 +64,47 @@ func New(cfg app.Config, log logging.Logger) (app.Runnable, error) {
|
||||
|
||||
func (r *Runner) Run(ctx context.Context) error {
|
||||
logger := r.log.WithContext(ctx)
|
||||
for _, t := range r.checkRegistry.Checks() {
|
||||
steps := t.Steps()
|
||||
stepTypes := make([]advisorv0alpha1.CheckTypeStep, len(steps))
|
||||
for i, s := range steps {
|
||||
stepTypes[i] = advisorv0alpha1.CheckTypeStep{
|
||||
Title: s.Title(),
|
||||
Description: s.Description(),
|
||||
StepID: s.ID(),
|
||||
Resolution: s.Resolution(),
|
||||
|
||||
// Determine namespaces based on StackID or OrgID
|
||||
namespaces, err := checks.GetNamespaces(ctx, r.stackID, r.orgService)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get namespaces: %w", err)
|
||||
}
|
||||
logger.Debug("Registering check types", "namespaces", len(namespaces))
|
||||
|
||||
// Register check types in each namespace
|
||||
for _, namespace := range namespaces {
|
||||
for _, t := range r.checkRegistry.Checks() {
|
||||
steps := t.Steps()
|
||||
stepTypes := make([]advisorv0alpha1.CheckTypeStep, len(steps))
|
||||
for i, s := range steps {
|
||||
stepTypes[i] = advisorv0alpha1.CheckTypeStep{
|
||||
Title: s.Title(),
|
||||
Description: s.Description(),
|
||||
StepID: s.ID(),
|
||||
Resolution: s.Resolution(),
|
||||
}
|
||||
}
|
||||
}
|
||||
obj := &advisorv0alpha1.CheckType{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: t.ID(),
|
||||
Namespace: r.namespace,
|
||||
Annotations: map[string]string{
|
||||
checks.NameAnnotation: t.Name(),
|
||||
// Flag to indicate feature availability
|
||||
checks.RetryAnnotation: "1",
|
||||
checks.IgnoreStepsAnnotation: "1",
|
||||
obj := &advisorv0alpha1.CheckType{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: t.ID(),
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
checks.NameAnnotation: t.Name(),
|
||||
// Flag to indicate feature availability
|
||||
checks.RetryAnnotation: "1",
|
||||
checks.IgnoreStepsAnnotation: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: advisorv0alpha1.CheckTypeSpec{
|
||||
Name: t.ID(),
|
||||
Steps: stepTypes,
|
||||
},
|
||||
}
|
||||
err := r.registerCheckType(ctx, logger, t.ID(), obj)
|
||||
if err != nil {
|
||||
return err
|
||||
Spec: advisorv0alpha1.CheckTypeSpec{
|
||||
Name: t.ID(),
|
||||
Steps: stepTypes,
|
||||
},
|
||||
}
|
||||
err := r.registerCheckType(ctx, logger, t.ID(), obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
k8sErrs "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -67,14 +68,17 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
checks []checks.Check
|
||||
stackID string
|
||||
orgService org.Service
|
||||
getFunc func(ctx context.Context, id resource.Identifier) (resource.Object, error)
|
||||
createFunc func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error)
|
||||
updateFunc func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.UpdateOptions) (resource.Object, error)
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "successful create",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
name: "successful create",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
stackID: "123",
|
||||
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
|
||||
return nil, k8sErrs.NewNotFound(schema.GroupResource{}, id.Name)
|
||||
},
|
||||
@@ -85,8 +89,9 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "resource exists with different annotations, should update",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
name: "resource exists with different annotations, should update",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
stackID: "123",
|
||||
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
|
||||
return existingObjectDifferentAnnotations, nil
|
||||
},
|
||||
@@ -96,8 +101,9 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "resource exists with different steps, should update",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
name: "resource exists with different steps, should update",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
stackID: "123",
|
||||
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
|
||||
return existingObjectDifferentSteps, nil
|
||||
},
|
||||
@@ -107,8 +113,9 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "resource exists with same annotations and steps, should not update",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
name: "resource exists with same annotations and steps, should not update",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
stackID: "123",
|
||||
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
|
||||
return existingObjectSameContent, nil
|
||||
},
|
||||
@@ -118,8 +125,9 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "resource exists, with custom annotations preserved",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
name: "resource exists, with custom annotations preserved",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
stackID: "123",
|
||||
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
|
||||
return existingObjectDifferentAnnotations, nil
|
||||
},
|
||||
@@ -132,8 +140,9 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "create error",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
name: "create error",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
stackID: "123",
|
||||
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
|
||||
return nil, k8sErrs.NewNotFound(schema.GroupResource{}, id.Name)
|
||||
},
|
||||
@@ -144,8 +153,9 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
|
||||
expectedErr: errors.New("create error"),
|
||||
},
|
||||
{
|
||||
name: "update error",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
name: "update error",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
stackID: "123",
|
||||
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
|
||||
return existingObjectDifferentAnnotations, nil
|
||||
},
|
||||
@@ -155,8 +165,9 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
|
||||
expectedErr: errors.New("update error"),
|
||||
},
|
||||
{
|
||||
name: "shutting down error",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
name: "shutting down error",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
stackID: "123",
|
||||
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
|
||||
return existingObjectDifferentAnnotations, nil
|
||||
},
|
||||
@@ -166,14 +177,33 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "custom namespace",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
name: "cloud stack namespace",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
stackID: "456",
|
||||
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
|
||||
return existingObjectDifferentAnnotations, nil
|
||||
return nil, k8sErrs.NewNotFound(schema.GroupResource{}, id.Name)
|
||||
},
|
||||
createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) {
|
||||
if obj.GetNamespace() != "custom-namespace" {
|
||||
return nil, fmt.Errorf("expected namespace %s, got %s", "custom-namespace", obj.GetNamespace())
|
||||
if obj.GetNamespace() != "stack-456" {
|
||||
return nil, fmt.Errorf("expected namespace %s, got %s", "stack-456", obj.GetNamespace())
|
||||
}
|
||||
return obj, nil
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "multiple orgs",
|
||||
checks: []checks.Check{newMockCheck},
|
||||
stackID: "",
|
||||
orgService: &mockOrgService{orgs: []*org.OrgDTO{{ID: 1, Name: "Org1"}, {ID: 2, Name: "Org2"}}},
|
||||
getFunc: func(ctx context.Context, id resource.Identifier) (resource.Object, error) {
|
||||
return nil, k8sErrs.NewNotFound(schema.GroupResource{}, id.Name)
|
||||
},
|
||||
createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) {
|
||||
// Should create in both org-1 and org-2 namespaces
|
||||
ns := obj.GetNamespace()
|
||||
if ns != "org-1" && ns != "org-2" {
|
||||
return nil, fmt.Errorf("expected namespace org-1 or org-2, got %s", ns)
|
||||
}
|
||||
return obj, nil
|
||||
},
|
||||
@@ -183,6 +213,10 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
orgSvc := tt.orgService
|
||||
if orgSvc == nil {
|
||||
orgSvc = &mockOrgService{orgs: []*org.OrgDTO{}}
|
||||
}
|
||||
r := &Runner{
|
||||
checkRegistry: &mockCheckRegistry{checks: tt.checks},
|
||||
client: &mockClient{
|
||||
@@ -190,7 +224,8 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
|
||||
createFunc: tt.createFunc,
|
||||
updateFunc: tt.updateFunc,
|
||||
},
|
||||
namespace: "custom-namespace",
|
||||
orgService: orgSvc,
|
||||
stackID: tt.stackID,
|
||||
log: logging.DefaultLogger,
|
||||
retryAttempts: 1,
|
||||
retryDelay: 0,
|
||||
@@ -298,3 +333,12 @@ func (m *mockClient) Update(ctx context.Context, id resource.Identifier, obj res
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
type mockOrgService struct {
|
||||
org.Service
|
||||
orgs []*org.OrgDTO
|
||||
}
|
||||
|
||||
func (m *mockOrgService) Search(ctx context.Context, query *org.SearchOrgsQuery) ([]*org.OrgDTO, error) {
|
||||
return m.orgs, nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
|
||||
@@ -317,3 +318,12 @@ func waitForRetryAnnotation(ctx context.Context, log logging.Logger, client reso
|
||||
log.Debug("Retry annotation persisted", "check", obj.GetName(), "item", itemToRetry)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getOrgIDFromNamespace extracts the org ID from a namespace using the standard authlib parser.
|
||||
func getOrgIDFromNamespace(namespace string) (int64, error) {
|
||||
info, err := types.ParseNamespace(namespace)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse namespace %s: %w", namespace, err)
|
||||
}
|
||||
return info.OrgID, nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ UserSpec: {
|
||||
emailVerified: bool
|
||||
grafanaAdmin: bool
|
||||
login: string
|
||||
name: string
|
||||
title: string
|
||||
provisioned: bool
|
||||
role: string
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ type UserSpec struct {
|
||||
EmailVerified bool `json:"emailVerified"`
|
||||
GrafanaAdmin bool `json:"grafanaAdmin"`
|
||||
Login string `json:"login"`
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Provisioned bool `json:"provisioned"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
@@ -2591,7 +2591,7 @@ func schema_pkg_apis_iam_v0alpha1_UserSpec(ref common.ReferenceCallback) common.
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"name": {
|
||||
"title": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
@@ -2613,7 +2613,7 @@ func schema_pkg_apis_iam_v0alpha1_UserSpec(ref common.ReferenceCallback) common.
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"disabled", "email", "emailVerified", "grafanaAdmin", "login", "name", "provisioned", "role"},
|
||||
Required: []string{"disabled", "email", "emailVerified", "grafanaAdmin", "login", "title", "provisioned", "role"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -150,7 +150,8 @@ When you log in for the first time, Grafana asks you to change your password.
|
||||
|
||||
#### Build on Windows
|
||||
|
||||
The Grafana backend includes SQLite, a database which requires GCC to compile. So in order to compile Grafana on Windows you need to install GCC. We recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download). Eventually, if you use [Scoop](https://scoop.sh), you can install GCC through that.
|
||||
The Grafana backend includes SQLite, a database which requires GCC to compile. So in order to compile Grafana on Windows you need to install GCC with binutils version 2.37 or later.
|
||||
We recommend [MinGW x64](https://www.mingw-w64.org/downloads). Eventually, if you use [Scoop](https://scoop.sh), you can install GCC through that.
|
||||
|
||||
You can build the back-end as follows:
|
||||
|
||||
|
||||
+8
-9
@@ -548,12 +548,11 @@ The following table outlines the various Entra ID configuration options. You can
|
||||
| `allowed_organizations` | No | Yes | List of comma- or space-separated Azure tenant identifiers. The user should be a member of at least one tenant to log in. | |
|
||||
| `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | |
|
||||
| `domain_hint` | No | Yes | The realm of the user in a federated directory. This skips the email-based discovery process that the user goes through on the Entra ID sign-in page, for a slightly more streamlined user experience. More info [here](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#send-the-sign-in-request). | |
|
||||
|
||||
| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
|
||||
| `tls_client_cert` | No | No | The path to the certificate. | |
|
||||
| `tls_client_key` | No | No | The path to the key. | |
|
||||
| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | |
|
||||
| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` |
|
||||
| `use_refresh_token` | No | Yes | Enables the use of refresh tokens and checks for access token expiration. When enabled, Grafana automatically adds the `offline_access` scope to the list of scopes. | `true` |
|
||||
| `force_use_graph_api` | No | Yes | Set to `true` to always fetch groups from the Microsoft Graph API instead of the `id_token`. If a user belongs to more than 200 groups, the Microsoft Graph API will be used to retrieve the groups regardless of this setting. | `false` |
|
||||
| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | |
|
||||
| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
|
||||
| `tls_client_cert` | No | No | The path to the certificate. | |
|
||||
| `tls_client_key` | No | No | The path to the key. | |
|
||||
| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | |
|
||||
| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` |
|
||||
| `use_refresh_token` | No | Yes | Enables the use of refresh tokens and checks for access token expiration. When enabled, Grafana automatically adds the `offline_access` scope to the list of scopes. | `true` |
|
||||
| `force_use_graph_api` | No | Yes | Set to `true` to always fetch groups from the Microsoft Graph API instead of the `id_token`. If a user belongs to more than 200 groups, the Microsoft Graph API will be used to retrieve the groups regardless of this setting. | `false` |
|
||||
| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | |
|
||||
|
||||
+1
-1
@@ -248,7 +248,7 @@ IdP-initiated SSO has some security risks, so make sure you understand the risks
|
||||
|
||||
For advanced configuration and troubleshooting, please refer to the one of the following pages:
|
||||
|
||||
- [Configure SAML request signing](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-authentication/saml/configure-saml-request-signing/)
|
||||
- [Configure SAML signing and encryption](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-authentication/saml/configure-saml-signing-encryption/)
|
||||
- [Configure SAML single logout](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-authentication/saml/configure-saml-single-logout/)
|
||||
- [Configure Organization mapping](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-authentication/saml/configure-saml-org-mapping/)
|
||||
- [Configure Role and Team sync](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-authentication/saml/configure-saml-team-role-mapping/)
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ Sign in to Grafana and navigate to **Administration > Authentication > Configure
|
||||
|
||||
Use the [PKCS #8](https://en.wikipedia.org/wiki/PKCS_8) format to issue the private key.
|
||||
|
||||
For more information, refer to an [example on how to generate SAML credentials](../configure-saml-request-signing/#example-of-private-key-generation-for-saml-authentication).
|
||||
For more information, refer to an [example on how to generate SAML credentials](../configure-saml-signing-encryption/#example-of-private-key-generation-for-saml-authentication).
|
||||
|
||||
Alternatively, you can generate a new private key and certificate pair directly from the UI. Click on the `Generate key and certificate` button to open a form where you enter some information you want to be embedded into the new certificate.
|
||||
|
||||
|
||||
@@ -48,10 +48,17 @@ test.describe(
|
||||
}) => {
|
||||
// Handle query and query_range API calls
|
||||
await page.route(/\/api\/ds\/query/, async (route) => {
|
||||
const fixture = require('../fixtures/prometheus-response.json');
|
||||
// during the test, we select the "inner_eval" slice to filter; this simulates the behavior
|
||||
// of prometheus applying that filter and removing dataframes from the response.
|
||||
if (route.request().postData()?.includes('{slice=\\\"inner_eval\\\"}')) {
|
||||
delete fixture.results.A.frames[1];
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(require('../fixtures/prometheus-response.json')),
|
||||
body: JSON.stringify(fixture),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,6 +80,11 @@ test.describe(
|
||||
const labelValue = await labelValueCell.textContent();
|
||||
expect(labelValue).toBeTruthy();
|
||||
|
||||
const otherValueCell = await getCell(panel, 2, 1);
|
||||
const otherValueLabel = await otherValueCell.textContent();
|
||||
expect(otherValueLabel).toBeTruthy();
|
||||
expect(otherValueLabel).not.toBe(labelValue);
|
||||
|
||||
// Hover over the first cell to trigger the appearance of filter actions
|
||||
await labelValueCell.hover();
|
||||
|
||||
@@ -92,6 +104,9 @@ test.describe(
|
||||
const hasFilterValue = await submenuItems.filter({ hasText: labelValue! }).count();
|
||||
expect(hasFilterValue).toBeGreaterThan(0);
|
||||
|
||||
const hasOtherValue = await submenuItems.filter({ hasText: otherValueLabel! }).count();
|
||||
expect(hasOtherValue).toBe(0);
|
||||
|
||||
// Check if the URL contains the var-PromAdHoc parameter with the filtered value
|
||||
const currentUrl = page.url();
|
||||
expect(currentUrl).toContain('var-PromAdHoc');
|
||||
@@ -101,6 +116,10 @@ test.describe(
|
||||
const promAdHocParam = urlParams.get('var-PromAdHoc');
|
||||
expect(promAdHocParam).toBeTruthy();
|
||||
expect(promAdHocParam).toContain(labelValue!);
|
||||
expect(promAdHocParam).not.toContain(otherValueLabel!);
|
||||
|
||||
// finally, let's check that the table was updated and that the value was filtered out when the query was re-run
|
||||
await expect(otherValueCell).toBeHidden();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -55,7 +55,20 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "slice"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Slice"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
|
||||
@@ -1194,11 +1194,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/core/components/AccessControl/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/core/components/DynamicImports/SafeDynamicImport.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
@@ -1462,11 +1457,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/core/utils/richHistory.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"public/app/core/utils/ticks.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 2
|
||||
@@ -1993,11 +1983,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/auth-config/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/auth-config/utils/data.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
@@ -2038,21 +2023,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/connections/components/ConnectionsRedirectNotice/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/connections/tabs/ConnectData/CardGrid/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/connections/tabs/ConnectData/CategoryHeader/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/connections/tabs/ConnectData/ConnectData.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
@@ -2064,21 +2034,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/connections/tabs/ConnectData/NoAccessModal/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/connections/tabs/ConnectData/Search/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/connections/tabs/ConnectData/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/correlations/CorrelationsPage.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
@@ -2425,11 +2380,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard/components/AddLibraryPanelWidget/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 7
|
||||
@@ -2448,21 +2398,11 @@
|
||||
"count": 7
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard/components/DashExportModal/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard/components/DashNav/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.test.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
@@ -2486,11 +2426,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard/components/DashboardRow/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard/components/DashboardSettings/AutoRefreshIntervals.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
@@ -2514,11 +2449,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard/components/DashboardSettings/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 3
|
||||
@@ -3037,11 +2967,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/TracePageHeader/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
@@ -3082,11 +3007,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
@@ -3097,11 +3017,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/common/SearchBarInput.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
@@ -3112,11 +3027,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/model/ddg/types.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/model/link-patterns.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 2
|
||||
@@ -3145,11 +3055,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/utils/DraggableManager/demo/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/explore/TraceView/components/utils/sort.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
@@ -3338,11 +3243,6 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"public/app/features/migrate-to-cloud/api/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/CreateTokenModal.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
@@ -3426,11 +3326,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/admin/components/GetStartedWithPlugin/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx": {
|
||||
"react/no-unescaped-entities": {
|
||||
"count": 1
|
||||
@@ -3947,11 +3842,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/variables/pickers/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/variables/pickers/shared/VariableInput.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
@@ -4084,11 +3974,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AppRegistrationCredentials.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 5
|
||||
@@ -4124,41 +4009,16 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/azuremonitor/components/QueryEditor/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/azuremonitor/components/ResourceField/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/azuremonitor/components/ResourcePicker/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 7
|
||||
@@ -4258,21 +4118,11 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/cloudwatch/components/shared/LogGroups/LegacyLogGroupNamesSelection.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/cloudwatch/components/shared/MetricStatEditor/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/datasource/cloudwatch/datasource.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
@@ -5108,11 +4958,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/panel/nodeGraph/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/panel/nodeGraph/layout.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
|
||||
+34
-10
@@ -1,17 +1,18 @@
|
||||
const fs = require('fs');
|
||||
const open = require('open').default;
|
||||
const path = require('path');
|
||||
|
||||
const baseConfig = require('./jest.config.js');
|
||||
|
||||
const CODEOWNERS_MANIFEST_FILENAMES_BY_TEAM_PATH = 'codeowners-manifest/filenames-by-team.json';
|
||||
|
||||
const teamName = process.env.TEAM_NAME;
|
||||
if (!teamName) {
|
||||
console.error('ERROR: TEAM_NAME environment variable is required');
|
||||
const codeownerName = process.env.CODEOWNER_NAME;
|
||||
if (!codeownerName) {
|
||||
console.error('ERROR: CODEOWNER_NAME environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outputDir = `./coverage/by-team/${createOwnerDirectory(teamName)}`;
|
||||
const outputDir = `./coverage/by-team/${createOwnerDirectory(codeownerName)}`;
|
||||
|
||||
const codeownersFilePath = path.join(__dirname, CODEOWNERS_MANIFEST_FILENAMES_BY_TEAM_PATH);
|
||||
|
||||
@@ -22,10 +23,10 @@ if (!fs.existsSync(codeownersFilePath)) {
|
||||
}
|
||||
|
||||
const codeownersData = JSON.parse(fs.readFileSync(codeownersFilePath, 'utf8'));
|
||||
const teamFiles = codeownersData[teamName] || [];
|
||||
const teamFiles = codeownersData[codeownerName] || [];
|
||||
|
||||
if (teamFiles.length === 0) {
|
||||
console.error(`ERROR: No files found for team "${teamName}"`);
|
||||
console.error(`ERROR: No files found for team "${codeownerName}"`);
|
||||
console.error('Available teams:', Object.keys(codeownersData).join(', '));
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -34,10 +35,15 @@ const sourceFiles = teamFiles.filter((file) => {
|
||||
const ext = path.extname(file);
|
||||
return (
|
||||
['.ts', '.tsx', '.js', '.jsx'].includes(ext) &&
|
||||
// exclude all tests
|
||||
!path.matchesGlob(file, '**/test/**/*') &&
|
||||
!file.includes('.test.') &&
|
||||
!file.includes('.spec.') &&
|
||||
// and storybook stories
|
||||
!file.includes('.story.') &&
|
||||
// and generated files
|
||||
!file.includes('.gen.ts') &&
|
||||
// and type definitions
|
||||
!file.includes('.d.ts') &&
|
||||
!file.endsWith('/types.ts')
|
||||
);
|
||||
@@ -49,12 +55,12 @@ const testFiles = teamFiles.filter((file) => {
|
||||
});
|
||||
|
||||
if (testFiles.length === 0) {
|
||||
console.log(`No test files found for team ${teamName}`);
|
||||
console.log(`No test files found for team ${codeownerName}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`🧪 Collecting coverage for ${sourceFiles.length} testable files and running ${testFiles.length} test files of ${teamFiles.length} files owned by ${teamName}.`
|
||||
`🧪 Collecting coverage for ${sourceFiles.length} testable files and running ${testFiles.length} test files of ${teamFiles.length} files owned by ${codeownerName}.`
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
@@ -71,7 +77,7 @@ module.exports = {
|
||||
[
|
||||
'jest-monocart-coverage',
|
||||
{
|
||||
name: `Coverage Report - ${teamName} owned files`,
|
||||
name: `Coverage Report - ${codeownerName} owned files`,
|
||||
outputDir: outputDir,
|
||||
reports: ['console-summary', 'v8', 'json', 'lcov'],
|
||||
sourceFilter: (coveredFile) => sourceFiles.includes(coveredFile),
|
||||
@@ -84,7 +90,13 @@ module.exports = {
|
||||
},
|
||||
cleanCache: true,
|
||||
onEnd: (coverageResults) => {
|
||||
console.log(`📄 Coverage report saved to file://${path.resolve(outputDir)}/index.html`);
|
||||
const reportURL = `file://${path.resolve(outputDir)}/index.html`;
|
||||
console.log(`📄 Coverage report saved to ${reportURL}`);
|
||||
|
||||
if (process.env.SHOULD_OPEN_COVERAGE_REPORT === 'true') {
|
||||
openCoverageReport(reportURL);
|
||||
}
|
||||
|
||||
// TODO: Emit coverage metrics https://github.com/grafana/grafana/issues/111208
|
||||
},
|
||||
},
|
||||
@@ -115,3 +127,15 @@ function createOwnerDirectory(owner) {
|
||||
return `emails/${user}-at-${domain}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the given file URL in the default browser safely, without shell injection risk.
|
||||
* @param {string} reportURL
|
||||
*/
|
||||
async function openCoverageReport(reportURL) {
|
||||
try {
|
||||
await open(reportURL);
|
||||
} catch (err) {
|
||||
console.error(`Failed to open coverage report: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-7
@@ -36,8 +36,8 @@
|
||||
"test": "jest --notify --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"test:coverage:by-codeowner": "yarn codeowners-manifest && node scripts/test-coverage-by-codeowner.js",
|
||||
"test:storybook": "yarn workspace @grafana/ui storybook:test",
|
||||
"test:coverage:changes": "jest --coverage --changedSince=origin/main",
|
||||
"test:storybook": "yarn workspace @grafana/ui storybook:test",
|
||||
"test:accessibility-report": "./scripts/generate-a11y-report.sh",
|
||||
"test:ci": "mkdir -p reports/junit && JEST_JUNIT_OUTPUT_DIR=reports/junit jest --ci --reporters=default --reporters=jest-junit -w ${TEST_MAX_WORKERS:-100%} --shard=${TEST_SHARD:-1}/${TEST_SHARD_TOTAL:-1}",
|
||||
"lint": "yarn run lint:ts && yarn run lint:sass",
|
||||
@@ -75,9 +75,8 @@
|
||||
"plugin:build:dev": "nx run-many -t dev --projects='tag:scope:plugin' --maxParallel=100",
|
||||
"plugin:test:ci": "nx run-many -t test:ci --projects='tag:scope:plugin' --maxParallel=2",
|
||||
"plugin:i18n-extract": "nx run-many -t i18n-extract --projects='tag:scope:plugin'",
|
||||
"process-specs": "node --experimental-strip-types scripts/process-specs.ts",
|
||||
"generate-apis": "yarn process-specs && rtk-query-codegen-openapi ./scripts/generate-rtk-apis.ts",
|
||||
"generate:api-client": "NODE_OPTIONS='--experimental-strip-types --disable-warning=ExperimentalWarning' plop --plopfile scripts/rtk-client-generator/plopfile.ts"
|
||||
"generate-apis": "yarn workspace @grafana/api-clients generate-apis",
|
||||
"generate:api-client": "yarn workspace @grafana/api-clients generate:api-client"
|
||||
},
|
||||
"grafana": {
|
||||
"whatsNewUrl": "https://grafana.com/docs/grafana/next/whatsnew/whats-new-in-v%[1]s-%[2]s/",
|
||||
@@ -183,6 +182,7 @@
|
||||
"cypress": "14.3.2",
|
||||
"cypress-file-upload": "5.0.8",
|
||||
"cypress-recurse": "^1.35.3",
|
||||
"enquirer": "^2.4.1",
|
||||
"esbuild": "0.25.8",
|
||||
"esbuild-loader": "4.3.0",
|
||||
"esbuild-plugin-browserslist": "^1.0.0",
|
||||
@@ -229,10 +229,10 @@
|
||||
"mutationobserver-shim": "0.3.7",
|
||||
"node-notifier": "10.0.1",
|
||||
"nx": "21.3.11",
|
||||
"openapi-types": "^12.1.3",
|
||||
"open": "^10.2.0",
|
||||
"ora": "^9.0.0",
|
||||
"pa11y-ci": "^4.0.0",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"plop": "^4.0.1",
|
||||
"postcss": "8.5.6",
|
||||
"postcss-loader": "8.1.1",
|
||||
"postcss-reporter": "7.1.0",
|
||||
@@ -277,6 +277,7 @@
|
||||
"@formatjs/intl-durationformat": "^0.7.0",
|
||||
"@glideapps/glide-data-grid": "^6.0.0",
|
||||
"@grafana/alerting": "workspace:*",
|
||||
"@grafana/api-clients": "workspace:*",
|
||||
"@grafana/assistant": "0.1.0",
|
||||
"@grafana/aws-sdk": "0.7.1",
|
||||
"@grafana/azure-sdk": "0.0.7",
|
||||
@@ -468,7 +469,7 @@
|
||||
"engines": {
|
||||
"node": ">= 22 <23"
|
||||
},
|
||||
"packageManager": "yarn@4.9.4",
|
||||
"packageManager": "yarn@4.10.3",
|
||||
"dependenciesMeta": {
|
||||
"cypress": {
|
||||
"built": true
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ All packages are versioned according to the current Grafana version:
|
||||
- Grafana v6.2.5 -> @grafana/\* packages @ 6.2.5
|
||||
- Grafana - main branch version (based on package.json, i.e. 6.4.0-pre) -> @grafana/\* packages @ 6.4.0-pre-<COMMIT-SHA> (see details below about packages publishing channels)
|
||||
|
||||
> Please note that @grafana/ui, @grafana/data, and @grafana/runtime packages are considered ALPHA even though they are not released as alpha versions.
|
||||
> Please note that the @grafana/api-clients package is considered ALPHA even though it is not released as an alpha version.
|
||||
|
||||
### Stable releases
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Initial scaffold
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2015 Grafana Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Grafana API Clients
|
||||
|
||||
> **@grafana/api-clients is currently in BETA**.
|
||||
|
||||
This package hosts reusable clients for Grafana APIs. The implementation is currently in BETA and not yet published.
|
||||
|
||||
## Generating RTK Query API Clients
|
||||
|
||||
This guide explains how to generate API clients for Grafana APIs using this package.
|
||||
|
||||
### 1. Generate an OpenAPI snapshot
|
||||
|
||||
First, check if the `group` and the `version` are already present in [openapi_test.go](../../pkg/tests/apis/openapi_test.go). If so, move on to the next step. If you need to add a new block, you can check for the right `group` and `version` in the backend API call that you want to replicate in the frontend.
|
||||
|
||||
```go
|
||||
{
|
||||
Group: "dashboard.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
}
|
||||
```
|
||||
|
||||
Afterwards, you need to run the `TestIntegrationOpenAPIs` test. Note that it will fail the first time you run it. On the second run, it will generate the corresponding OpenAPI spec, which you can find in [openapi_snapshots](../../pkg/tests/apis/openapi_snapshots).
|
||||
|
||||
> Note: You don't need to follow these two steps if the `group` you're working with is already in the `openapi_test.go` file.
|
||||
|
||||
### 2. Run the API generator script
|
||||
|
||||
Run `yarn generate:api-client` and follow the prompts. See [API Client Generator](./src/generator/README.md) for details.
|
||||
|
||||
## Updating generated clients
|
||||
|
||||
To update the existing clients, for example, when the OpenAPI spec has changed, run `yarn generate-apis`. This will regenerate all the clients based on the current OpenAPI snapshots.
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"name": "@grafana/api-clients",
|
||||
"version": "12.3.0-pre",
|
||||
"description": "Grafana API client utilities",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
"typescript"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/grafana/grafana.git",
|
||||
"directory": "packages/grafana-api-clients"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"module": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": "./src/index.ts",
|
||||
"require": "./src/index.ts"
|
||||
},
|
||||
"./rtkq": {
|
||||
"import": "./src/clients/rtkq/index.ts",
|
||||
"require": "./src/clients/rtkq/index.ts"
|
||||
},
|
||||
"./rtkq/advisor/v0alpha1": {
|
||||
"import": "./src/clients/rtkq/advisor/v0alpha1/index.ts",
|
||||
"require": "./src/clients/rtkq/advisor/v0alpha1/index.ts"
|
||||
},
|
||||
"./rtkq/dashboard/v0alpha1": {
|
||||
"import": "./src/clients/rtkq/dashboard/v0alpha1/index.ts",
|
||||
"require": "./src/clients/rtkq/dashboard/v0alpha1/index.ts"
|
||||
},
|
||||
"./rtkq/folder/v1beta1": {
|
||||
"import": "./src/clients/rtkq/folder/v1beta1/index.ts",
|
||||
"require": "./src/clients/rtkq/folder/v1beta1/index.ts"
|
||||
},
|
||||
"./rtkq/iam/v0alpha1": {
|
||||
"import": "./src/clients/rtkq/iam/v0alpha1/index.ts",
|
||||
"require": "./src/clients/rtkq/iam/v0alpha1/index.ts"
|
||||
},
|
||||
"./rtkq/legacy/migrate-to-cloud": {
|
||||
"import": "./src/clients/rtkq/migrate-to-cloud/index.ts",
|
||||
"require": "./src/clients/rtkq/migrate-to-cloud/index.ts"
|
||||
},
|
||||
"./rtkq/legacy/preferences": {
|
||||
"import": "./src/clients/rtkq/preferences/user/index.ts",
|
||||
"require": "./src/clients/rtkq/preferences/user/index.ts"
|
||||
},
|
||||
"./rtkq/legacy/user": {
|
||||
"import": "./src/clients/rtkq/user/index.ts",
|
||||
"require": "./src/clients/rtkq/user/index.ts"
|
||||
},
|
||||
"./rtkq/playlist/v0alpha1": {
|
||||
"import": "./src/clients/rtkq/playlist/v0alpha1/index.ts",
|
||||
"require": "./src/clients/rtkq/playlist/v0alpha1/index.ts"
|
||||
},
|
||||
"./rtkq/preferences/v1alpha1": {
|
||||
"import": "./src/clients/rtkq/preferences/v1alpha1/index.ts",
|
||||
"require": "./src/clients/rtkq/preferences/v1alpha1/index.ts"
|
||||
},
|
||||
"./rtkq/provisioning/v0alpha1": {
|
||||
"import": "./src/clients/rtkq/provisioning/v0alpha1/index.ts",
|
||||
"require": "./src/clients/rtkq/provisioning/v0alpha1/index.ts"
|
||||
},
|
||||
"./rtkq/shorturl/v1alpha1": {
|
||||
"import": "./src/clients/rtkq/shorturl/v1alpha1/index.ts",
|
||||
"require": "./src/clients/rtkq/shorturl/v1alpha1/index.ts"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"main": "./dist/cjs/index.cjs",
|
||||
"module": "./dist/esm/index.mjs",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"./dist",
|
||||
"./README.md",
|
||||
"./CHANGELOG.md",
|
||||
"./LICENSE_APACHE2"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild",
|
||||
"bundle": "rollup -c rollup.config.ts --configPlugin esbuild",
|
||||
"clean": "rimraf ./dist ./compiled ./package.tgz",
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
|
||||
"postpack": "mv package.json.bak package.json",
|
||||
"process-specs": "NODE_OPTIONS='--disable-warning=ExperimentalWarning' node --experimental-strip-types src/scripts/process-specs.ts",
|
||||
"generate-apis": "yarn process-specs && NODE_OPTIONS='--disable-warning=ExperimentalWarning' rtk-query-codegen-openapi ./src/scripts/generate-rtk-apis.ts",
|
||||
"generate:api-client": "NODE_OPTIONS='--experimental-strip-types --disable-warning=ExperimentalWarning' plop --plopfile src/generator/plopfile.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rtk-query/codegen-openapi": "^2.0.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"plop": "^4.0.1",
|
||||
"rollup": "^4.22.4",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@grafana/runtime": ">=11.6 <= 12.x",
|
||||
"@reduxjs/toolkit": "^2.8.0",
|
||||
"rxjs": "7.8.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:package", "type:runtime"],
|
||||
"targets": {
|
||||
"build": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { cjsOutput, entryPoint, esmOutput, plugins } from '../rollup.config.parts';
|
||||
|
||||
const rq = createRequire(import.meta.url);
|
||||
const pkg = rq('./package.json');
|
||||
|
||||
const apiClients = Object.entries<{ import: string; require: string }>(pkg.exports).filter(([key]) =>
|
||||
key.startsWith('./rtkq/')
|
||||
);
|
||||
|
||||
const apiClientConfigs = apiClients.map(([name, { import: importPath }]) => {
|
||||
const baseCjsOutput = cjsOutput(pkg);
|
||||
const entryFileNames = name.replace('./', '') + '.cjs';
|
||||
const cjsOutputConfig = { ...baseCjsOutput, entryFileNames };
|
||||
return {
|
||||
input: importPath.replace('./', ''),
|
||||
|
||||
plugins,
|
||||
output: [cjsOutputConfig, esmOutput(pkg, 'grafana-api-clients')],
|
||||
treeshake: false,
|
||||
};
|
||||
});
|
||||
|
||||
export default [
|
||||
{
|
||||
input: entryPoint,
|
||||
plugins,
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-api-clients')],
|
||||
treeshake: false,
|
||||
},
|
||||
{
|
||||
input: 'src/clients/rtkq/index.ts',
|
||||
plugins,
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-api-clients')],
|
||||
treeshake: false,
|
||||
},
|
||||
...apiClientConfigs,
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { getAPIBaseURL } from '../../../../utils/utils';
|
||||
import { createBaseQuery } from '../../createBaseQuery';
|
||||
|
||||
export const API_GROUP = 'advisor.grafana.app' as const;
|
||||
export const API_VERSION = 'v0alpha1' as const;
|
||||
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'advisorAPIv0alpha1',
|
||||
baseQuery: createBaseQuery({
|
||||
baseURL: BASE_URL,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
|
||||
import { generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export * from './endpoints.gen';
|
||||
export const generatedAPI = rawAPI.enhanceEndpoints({});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { getAPIBaseURL } from '../../../../utils/utils';
|
||||
import { createBaseQuery } from '../../createBaseQuery';
|
||||
|
||||
export const API_GROUP = 'correlations.grafana.app' as const;
|
||||
export const API_VERSION = 'v0alpha1' as const;
|
||||
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'correlationsAPIv0alpha1',
|
||||
baseQuery: createBaseQuery({
|
||||
baseURL: BASE_URL,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
+4
-2
@@ -1,6 +1,8 @@
|
||||
import { generatedAPI } from './endpoints.gen';
|
||||
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
|
||||
import { generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export const correlationAPIv0alpha1 = generatedAPI.enhanceEndpoints({
|
||||
export * from './endpoints.gen';
|
||||
export const generatedAPI = rawAPI.enhanceEndpoints({
|
||||
endpoints: {
|
||||
createCorrelation: (endpointDefinition) => {
|
||||
const originalQuery = endpointDefinition.query;
|
||||
+4
-10
@@ -1,9 +1,11 @@
|
||||
import { BaseQueryFn } from '@reduxjs/toolkit/query';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { BackendSrvRequest, getBackendSrv, isFetchError } from '@grafana/runtime';
|
||||
import { type BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
|
||||
|
||||
interface RequestOptions extends BackendSrvRequest {
|
||||
import { handleRequestError } from '../../utils/utils';
|
||||
|
||||
export interface RequestOptions extends BackendSrvRequest {
|
||||
manageError?: (err: unknown) => { error: unknown };
|
||||
body?: BackendSrvRequest['data'];
|
||||
}
|
||||
@@ -49,11 +51,3 @@ export function createBaseQuery({ baseURL }: CreateBaseQueryOptions): BaseQueryF
|
||||
|
||||
return backendSrvBaseQuery;
|
||||
}
|
||||
|
||||
export function handleRequestError(error: unknown) {
|
||||
if (isFetchError(error) || error instanceof Error) {
|
||||
return { error };
|
||||
} else {
|
||||
return { error: new Error('Unknown error') };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { getAPIBaseURL } from '../../../../utils/utils';
|
||||
import { createBaseQuery } from '../../createBaseQuery';
|
||||
|
||||
export const API_GROUP = 'dashboard.grafana.app' as const;
|
||||
export const API_VERSION = 'v0alpha1' as const;
|
||||
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'dashboardAPIv0alpha1',
|
||||
baseQuery: createBaseQuery({
|
||||
baseURL: BASE_URL,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
|
||||
import { addTagTypes, generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export * from './endpoints.gen';
|
||||
export const generatedAPI = rawAPI.enhanceEndpoints({
|
||||
addTagTypes: [...addTagTypes, 'Folder', 'Dashboard'],
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { getAPIBaseURL } from '../../../../utils/utils';
|
||||
import { createBaseQuery } from '../../createBaseQuery';
|
||||
|
||||
export const API_GROUP = 'folder.grafana.app' as const;
|
||||
export const API_VERSION = 'v1beta1' as const;
|
||||
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'folderAPIv1beta1',
|
||||
baseQuery: createBaseQuery({
|
||||
baseURL: BASE_URL,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
|
||||
import { generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export * from './endpoints.gen';
|
||||
export const generatedAPI = rawAPI.enhanceEndpoints({});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { getAPIBaseURL } from '../../../../utils/utils';
|
||||
import { createBaseQuery } from '../../createBaseQuery';
|
||||
|
||||
export const API_GROUP = 'iam.grafana.app' as const;
|
||||
export const API_VERSION = 'v0alpha1' as const;
|
||||
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'iamAPIv0alpha1',
|
||||
baseQuery: createBaseQuery({
|
||||
baseURL: BASE_URL,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
|
||||
import { generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export * from './endpoints.gen';
|
||||
export const generatedAPI = rawAPI.enhanceEndpoints({});
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Don't manually add to this file! Use the `generate:api-client` command to add new API clients.
|
||||
*/
|
||||
import { generatedAPI as advisorAPIv0alpha1 } from './advisor/v0alpha1';
|
||||
import { generatedAPI as correlationsAPIv0alpha1 } from './correlations/v0alpha1';
|
||||
import { generatedAPI as dashboardAPIv0alpha1 } from './dashboard/v0alpha1';
|
||||
import { generatedAPI as folderAPIv1beta1 } from './folder/v1beta1';
|
||||
import { generatedAPI as iamAPIv0alpha1 } from './iam/v0alpha1';
|
||||
import { generatedAPI as migrateToCloudAPI } from './migrate-to-cloud';
|
||||
import { generatedAPI as playlistAPIv0alpha1 } from './playlist/v0alpha1';
|
||||
import { generatedAPI as preferencesUserAPI } from './preferences/user';
|
||||
import { generatedAPI as preferencesAPIv1alpha1 } from './preferences/v1alpha1';
|
||||
import { generatedAPI as provisioningAPIv0alpha1 } from './provisioning/v0alpha1';
|
||||
import { generatedAPI as shortURLAPIv1alpha1 } from './shorturl/v1alpha1';
|
||||
import { generatedAPI as legacyUserAPI } from './user';
|
||||
// PLOP_INJECT_IMPORT
|
||||
|
||||
/** RTK Query middleware for all API clients */
|
||||
export const allMiddleware = [
|
||||
advisorAPIv0alpha1.middleware,
|
||||
dashboardAPIv0alpha1.middleware,
|
||||
folderAPIv1beta1.middleware,
|
||||
iamAPIv0alpha1.middleware,
|
||||
migrateToCloudAPI.middleware,
|
||||
playlistAPIv0alpha1.middleware,
|
||||
preferencesAPIv1alpha1.middleware,
|
||||
preferencesUserAPI.middleware,
|
||||
provisioningAPIv0alpha1.middleware,
|
||||
shortURLAPIv1alpha1.middleware,
|
||||
correlationsAPIv0alpha1.middleware,
|
||||
legacyUserAPI.middleware,
|
||||
// PLOP_INJECT_MIDDLEWARE
|
||||
] as const;
|
||||
|
||||
/** RTK Query reducers for all API clients */
|
||||
export const allReducers = {
|
||||
[advisorAPIv0alpha1.reducerPath]: advisorAPIv0alpha1.reducer,
|
||||
[dashboardAPIv0alpha1.reducerPath]: dashboardAPIv0alpha1.reducer,
|
||||
[folderAPIv1beta1.reducerPath]: folderAPIv1beta1.reducer,
|
||||
[iamAPIv0alpha1.reducerPath]: iamAPIv0alpha1.reducer,
|
||||
[migrateToCloudAPI.reducerPath]: migrateToCloudAPI.reducer,
|
||||
[playlistAPIv0alpha1.reducerPath]: playlistAPIv0alpha1.reducer,
|
||||
[preferencesAPIv1alpha1.reducerPath]: preferencesAPIv1alpha1.reducer,
|
||||
[preferencesUserAPI.reducerPath]: preferencesUserAPI.reducer,
|
||||
[provisioningAPIv0alpha1.reducerPath]: provisioningAPIv0alpha1.reducer,
|
||||
[shortURLAPIv1alpha1.reducerPath]: shortURLAPIv1alpha1.reducer,
|
||||
[correlationsAPIv0alpha1.reducerPath]: correlationsAPIv0alpha1.reducer,
|
||||
[legacyUserAPI.reducerPath]: legacyUserAPI.reducer,
|
||||
// PLOP_INJECT_REDUCER
|
||||
};
|
||||
|
||||
export * from './createBaseQuery';
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { createBaseQuery } from 'app/api/createBaseQuery';
|
||||
import { createBaseQuery } from '../createBaseQuery';
|
||||
|
||||
export const baseAPI = createApi({
|
||||
export const api = createApi({
|
||||
reducerPath: 'migrateToCloudGeneratedAPI',
|
||||
baseQuery: createBaseQuery({ baseURL: '/api' }),
|
||||
endpoints: () => ({}),
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { baseAPI as api } from './baseAPI';
|
||||
import { api } from './baseAPI';
|
||||
const injectedRtkApi = api.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getSessionList: build.query<GetSessionListApiResponse, GetSessionListApiArg>({
|
||||
@@ -0,0 +1,89 @@
|
||||
import { BaseQueryFn, EndpointDefinition } from '@reduxjs/toolkit/query';
|
||||
|
||||
import { generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export const generatedAPI = rawAPI.enhanceEndpoints({
|
||||
addTagTypes: [
|
||||
'cloud-migration-token',
|
||||
'cloud-migration-session',
|
||||
'cloud-migration-snapshot',
|
||||
'cloud-migration-resource-dependencies',
|
||||
],
|
||||
|
||||
endpoints: {
|
||||
// Cloud-side - create token
|
||||
getCloudMigrationToken: {
|
||||
providesTags: ['cloud-migration-token'],
|
||||
},
|
||||
createCloudMigrationToken: {
|
||||
invalidatesTags: ['cloud-migration-token'],
|
||||
},
|
||||
deleteCloudMigrationToken: {
|
||||
invalidatesTags: ['cloud-migration-token'],
|
||||
},
|
||||
|
||||
// On-prem session management (entering token)
|
||||
getSessionList: {
|
||||
providesTags: ['cloud-migration-session'] /* should this be a -list? */,
|
||||
},
|
||||
getSession: {
|
||||
providesTags: ['cloud-migration-session'],
|
||||
},
|
||||
createSession: {
|
||||
invalidatesTags: ['cloud-migration-session'],
|
||||
},
|
||||
deleteSession: {
|
||||
invalidatesTags: ['cloud-migration-session', 'cloud-migration-snapshot'],
|
||||
},
|
||||
|
||||
// Snapshot management
|
||||
getShapshotList: {
|
||||
providesTags: ['cloud-migration-snapshot'],
|
||||
},
|
||||
getSnapshot: {
|
||||
providesTags: ['cloud-migration-snapshot'],
|
||||
},
|
||||
createSnapshot: {
|
||||
invalidatesTags: ['cloud-migration-snapshot'],
|
||||
},
|
||||
uploadSnapshot: {
|
||||
invalidatesTags: ['cloud-migration-snapshot'],
|
||||
},
|
||||
|
||||
// Resource dependencies
|
||||
getResourceDependencies: {
|
||||
providesTags: ['cloud-migration-resource-dependencies'],
|
||||
},
|
||||
|
||||
getDashboardByUid: suppressErrorsOnQuery,
|
||||
getLibraryElementByUid: suppressErrorsOnQuery,
|
||||
},
|
||||
});
|
||||
|
||||
function suppressErrorsOnQuery<
|
||||
QueryArg,
|
||||
BaseQuery extends BaseQueryFn,
|
||||
TagTypes extends string,
|
||||
ResultType,
|
||||
ReducerPath extends string,
|
||||
PageParam,
|
||||
>(endpoint: EndpointDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath, PageParam>) {
|
||||
if (!endpoint.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
// internal type from rtk-query that isn't exported
|
||||
type InfiniteQueryCombinedArg<QueryArg, PageParam> = {
|
||||
queryArg: QueryArg;
|
||||
pageParam: PageParam;
|
||||
};
|
||||
|
||||
const originalQuery = endpoint.query;
|
||||
endpoint.query = (arg: QueryArg & InfiniteQueryCombinedArg<QueryArg, PageParam>) => {
|
||||
const baseQuery = originalQuery(arg);
|
||||
baseQuery.showErrorAlert = false;
|
||||
return baseQuery;
|
||||
};
|
||||
}
|
||||
|
||||
export * from './endpoints.gen';
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { getAPIBaseURL } from '../../../../utils/utils';
|
||||
import { createBaseQuery } from '../../createBaseQuery';
|
||||
|
||||
export const API_GROUP = 'playlist.grafana.app' as const;
|
||||
export const API_VERSION = 'v0alpha1' as const;
|
||||
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'playlistAPIv0alpha1',
|
||||
baseQuery: createBaseQuery({
|
||||
baseURL: BASE_URL,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
|
||||
import { generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export * from './endpoints.gen';
|
||||
export const generatedAPI = rawAPI.enhanceEndpoints({});
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { createBaseQuery } from 'app/api/createBaseQuery';
|
||||
import { createBaseQuery } from '../../createBaseQuery';
|
||||
|
||||
export const baseAPI = createApi({
|
||||
export const api = createApi({
|
||||
reducerPath: 'userPreferencesAPI',
|
||||
baseQuery: createBaseQuery({ baseURL: '/api' }),
|
||||
endpoints: () => ({}),
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { baseAPI as api } from './baseAPI';
|
||||
import { api } from './baseAPI';
|
||||
const injectedRtkApi = api.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getUserPreferences: build.query<GetUserPreferencesApiResponse, GetUserPreferencesApiArg>({
|
||||
+4
-5
@@ -1,9 +1,6 @@
|
||||
import { generatedAPI } from './user/endpoints.gen';
|
||||
import { generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export const { useGetUserPreferencesQuery, usePatchUserPreferencesMutation, useUpdateUserPreferencesMutation } =
|
||||
generatedAPI;
|
||||
|
||||
export const userPreferencesAPI = generatedAPI.enhanceEndpoints({
|
||||
export const generatedAPI = rawAPI.enhanceEndpoints({
|
||||
addTagTypes: ['UserPreferences'],
|
||||
endpoints: {
|
||||
getUserPreferences: {
|
||||
@@ -17,3 +14,5 @@ export const userPreferencesAPI = generatedAPI.enhanceEndpoints({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export * from './endpoints.gen';
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { getAPIBaseURL } from '../../../../utils/utils';
|
||||
import { createBaseQuery } from '../../createBaseQuery';
|
||||
|
||||
export const API_GROUP = 'preferences.grafana.app' as const;
|
||||
export const API_VERSION = 'v1alpha1' as const;
|
||||
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'preferencesAPIv1alpha1',
|
||||
baseQuery: createBaseQuery({
|
||||
baseURL: BASE_URL,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
|
||||
import { generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export * from './endpoints.gen';
|
||||
export const generatedAPI = rawAPI.enhanceEndpoints({});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { getAPIBaseURL } from '../../../../utils/utils';
|
||||
import { createBaseQuery } from '../../createBaseQuery';
|
||||
|
||||
export const API_GROUP = 'provisioning.grafana.app' as const;
|
||||
export const API_VERSION = 'v0alpha1' as const;
|
||||
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'provisioningAPIv0alpha1',
|
||||
baseQuery: createBaseQuery({
|
||||
baseURL: BASE_URL,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
|
||||
import { generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export * from './endpoints.gen';
|
||||
export const generatedAPI = rawAPI.enhanceEndpoints({});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { getAPIBaseURL } from '../../../../utils/utils';
|
||||
import { createBaseQuery } from '../../createBaseQuery';
|
||||
|
||||
export const API_GROUP = 'shorturl.grafana.app' as const;
|
||||
export const API_VERSION = 'v1alpha1' as const;
|
||||
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'shortURLAPIv1alpha1',
|
||||
baseQuery: createBaseQuery({
|
||||
baseURL: BASE_URL,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
|
||||
import { generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export * from './endpoints.gen';
|
||||
export const generatedAPI = rawAPI.enhanceEndpoints({});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { createBaseQuery } from '../createBaseQuery';
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'legacyUserAPI',
|
||||
baseQuery: createBaseQuery({ baseURL: '/api' }),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { api } from './baseAPI';
|
||||
const injectedRtkApi = api.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
unstarDashboardByUid: build.mutation<UnstarDashboardByUidApiResponse, UnstarDashboardByUidApiArg>({
|
||||
query: (queryArg) => ({ url: `/user/stars/dashboard/uid/${queryArg.dashboardUid}`, method: 'DELETE' }),
|
||||
}),
|
||||
starDashboardByUid: build.mutation<StarDashboardByUidApiResponse, StarDashboardByUidApiArg>({
|
||||
query: (queryArg) => ({ url: `/user/stars/dashboard/uid/${queryArg.dashboardUid}`, method: 'POST' }),
|
||||
}),
|
||||
}),
|
||||
overrideExisting: false,
|
||||
});
|
||||
export { injectedRtkApi as generatedAPI };
|
||||
export type UnstarDashboardByUidApiResponse =
|
||||
/** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody;
|
||||
export type UnstarDashboardByUidApiArg = {
|
||||
dashboardUid: string;
|
||||
};
|
||||
export type StarDashboardByUidApiResponse =
|
||||
/** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody;
|
||||
export type StarDashboardByUidApiArg = {
|
||||
dashboardUid: string;
|
||||
};
|
||||
export type SuccessResponseBody = {
|
||||
message?: string;
|
||||
};
|
||||
export type ErrorResponseBody = {
|
||||
/** Error An optional detailed description of the actual error. Only included if running in developer mode. */
|
||||
error?: string;
|
||||
/** a human readable version of the error */
|
||||
message: string;
|
||||
/** Status An optional status to denote the cause of the error.
|
||||
|
||||
For example, a 412 Precondition Failed error may include additional information of why that error happened. */
|
||||
status?: string;
|
||||
};
|
||||
export const { useUnstarDashboardByUidMutation, useStarDashboardByUidMutation } = injectedRtkApi;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export const generatedAPI = rawAPI
|
||||
.enhanceEndpoints({
|
||||
addTagTypes: ['dashboardStars'],
|
||||
endpoints: {
|
||||
starDashboardByUid: {
|
||||
invalidatesTags: ['dashboardStars'],
|
||||
},
|
||||
unstarDashboardByUid: {
|
||||
invalidatesTags: ['dashboardStars'],
|
||||
},
|
||||
},
|
||||
})
|
||||
.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getStars: build.query<string[], void>({
|
||||
query: () => ({ url: '/user/stars' }),
|
||||
providesTags: ['dashboardStars'],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetStarsQuery } = generatedAPI;
|
||||
export * from './endpoints.gen';
|
||||
+3
-1
@@ -26,7 +26,9 @@ The generator automates the following:
|
||||
- `scripts/generate-rtk-apis.ts` for OSS APIs
|
||||
- `local/generate-enterprise-apis.ts` for Enterprise APIs
|
||||
3. Creates the `index.ts` file with proper exports
|
||||
4. For OSS APIs only: Registers Redux reducers and middleware in the store. For Enterprise this needs to be done manually
|
||||
4. For OSS APIs only:
|
||||
- Exports Redux reducers and middleware in `rtkq.ts`
|
||||
- Updates `package.json` exports to include the new API client
|
||||
5. Formats all generated files using Prettier and ESLint
|
||||
6. Automatically runs the appropriate command to generate endpoints from the OpenAPI schema
|
||||
|
||||
+77
-5
@@ -1,4 +1,6 @@
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
import path from 'path';
|
||||
|
||||
type PlopActionFunction = (
|
||||
@@ -34,14 +36,24 @@ export const formatEndpoints = () => (endpointsInput: string | string[]) => {
|
||||
|
||||
// List of created or modified files
|
||||
export const getFilesToFormat = (groupName: string, version: string, isEnterprise = false) => {
|
||||
const apiClientBasePath = isEnterprise ? 'public/app/extensions/api/clients' : 'public/app/api/clients';
|
||||
const generateScriptPath = isEnterprise ? 'local/generate-enterprise-apis.ts' : 'scripts/generate-rtk-apis.ts';
|
||||
const apiClientBasePath = isEnterprise
|
||||
? 'public/app/extensions/api/clients'
|
||||
: 'packages/grafana-api-clients/src/clients/rtkq';
|
||||
const generateScriptPath = isEnterprise
|
||||
? 'local/generate-enterprise-apis.ts'
|
||||
: 'packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts';
|
||||
|
||||
return [
|
||||
`${apiClientBasePath}/${groupName}/${version}/baseAPI.ts`,
|
||||
`${apiClientBasePath}/${groupName}/${version}/index.ts`,
|
||||
generateScriptPath,
|
||||
...(isEnterprise ? [] : [`public/app/core/reducers/root.ts`, `public/app/store/configureStore.ts`]),
|
||||
...(isEnterprise
|
||||
? []
|
||||
: [
|
||||
`packages/grafana-api-clients/src/index.ts`,
|
||||
`packages/grafana-api-clients/src/clients/rtkq/index.ts`,
|
||||
`packages/grafana-api-clients/package.json`,
|
||||
]),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -53,9 +65,10 @@ export const runGenerateApis =
|
||||
|
||||
let command;
|
||||
if (isEnterprise) {
|
||||
command = 'yarn process-specs && npx rtk-query-codegen-openapi ./local/generate-enterprise-apis.ts';
|
||||
command =
|
||||
'yarn workspace @grafana/api-clients process-specs && npx rtk-query-codegen-openapi ./local/generate-enterprise-apis.ts';
|
||||
} else {
|
||||
command = 'yarn generate-apis';
|
||||
command = 'yarn workspace @grafana/api-clients generate-apis';
|
||||
}
|
||||
|
||||
console.log(`⏳ Running ${command} to generate endpoints...`);
|
||||
@@ -113,3 +126,62 @@ export const validateGroup = (group: string) => {
|
||||
export const validateVersion = (version: string) => {
|
||||
return version && /^v\d+[a-z]*\d+$/.test(version) ? true : 'Version should be in format: v0alpha1, v1beta2, etc.';
|
||||
};
|
||||
|
||||
export const updatePackageJsonExports =
|
||||
(basePath: string): PlopActionFunction =>
|
||||
(answers) => {
|
||||
try {
|
||||
const { groupName, version } = answers;
|
||||
|
||||
if (!groupName || !version) {
|
||||
return '❌ Missing groupName or version for package.json update';
|
||||
}
|
||||
|
||||
const packageJsonPath = path.join(basePath, 'packages/grafana-api-clients/package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
// Create the new export entry
|
||||
const newExportKey = `./rtkq/${groupName}/${version}`;
|
||||
const newExportValue = {
|
||||
import: `./src/clients/rtkq/${groupName}/${version}/index.ts`,
|
||||
require: `./src/clients/rtkq/${groupName}/${version}/index.ts`,
|
||||
};
|
||||
|
||||
// Check if export already exists
|
||||
if (packageJson.exports[newExportKey]) {
|
||||
return `✅ Export for ${newExportKey} already exists in package.json`;
|
||||
}
|
||||
|
||||
// Add the new export entry
|
||||
packageJson.exports[newExportKey] = newExportValue;
|
||||
|
||||
// Write the updated package.json back to file
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
||||
|
||||
return `✅ Added export for ${newExportKey} to package.json`;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('❌ Failed to update package.json exports:', errorMessage);
|
||||
return '❌ Failed to update package.json exports. See error above.';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: Make this work more generically with endpoints that don't have operationIds defined -
|
||||
* then we can allow selection of endpoints in the generator
|
||||
*/
|
||||
export const getOperationIds = (apiSpec: OpenAPIV3.Document) => {
|
||||
return (
|
||||
Object.values(apiSpec.paths).flatMap((path) => {
|
||||
return Object.entries(path || {})
|
||||
.filter(([name]) => name !== 'parameters')
|
||||
.flatMap(([_, pathItem]) => {
|
||||
if (typeof pathItem === 'object' && 'operationId' in pathItem && pathItem.operationId) {
|
||||
return pathItem.operationId;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((id) => id !== null);
|
||||
}) || []
|
||||
);
|
||||
};
|
||||
+54
-32
@@ -1,27 +1,26 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type { NodePlopAPI, PlopGeneratorConfig } from 'plop';
|
||||
|
||||
import {
|
||||
formatEndpoints,
|
||||
validateGroup,
|
||||
validateVersion,
|
||||
formatFiles,
|
||||
getFilesToFormat,
|
||||
runGenerateApis,
|
||||
formatFiles,
|
||||
// The file extension is necessary to make the imports
|
||||
// work with the '--experimental-strip-types' flag
|
||||
// @ts-ignore
|
||||
updatePackageJsonExports,
|
||||
validateGroup,
|
||||
validateVersion,
|
||||
} from './helpers.ts';
|
||||
// @ts-ignore
|
||||
import { type ActionConfig, type PlopData, isPlopData } from './types.ts';
|
||||
|
||||
export default function plopGenerator(plop: NodePlopAPI) {
|
||||
// Grafana root path
|
||||
const basePath = path.resolve(import.meta.dirname, '../..');
|
||||
const basePath = path.resolve(import.meta.dirname, '../../../..');
|
||||
|
||||
// Register custom action types
|
||||
plop.setActionType('runGenerateApis', runGenerateApis(basePath));
|
||||
plop.setActionType('formatFiles', formatFiles(basePath));
|
||||
plop.setActionType('updatePackageJsonExports', updatePackageJsonExports(basePath));
|
||||
|
||||
// Used in templates to format endpoints
|
||||
plop.setHelper('formatEndpoints', formatEndpoints());
|
||||
@@ -29,13 +28,16 @@ export default function plopGenerator(plop: NodePlopAPI) {
|
||||
const generateRtkApiActions = (data: PlopData) => {
|
||||
const { reducerPath, groupName, version, isEnterprise } = data;
|
||||
|
||||
const apiClientBasePath = isEnterprise ? 'public/app/extensions/api/clients' : 'public/app/api/clients';
|
||||
const generateScriptPath = isEnterprise ? 'local/generate-enterprise-apis.ts' : 'scripts/generate-rtk-apis.ts';
|
||||
const apiClientBasePath = isEnterprise
|
||||
? 'public/app/extensions/api/clients'
|
||||
: 'packages/grafana-api-clients/src/clients/rtkq';
|
||||
const generateScriptPath = isEnterprise
|
||||
? 'local/generate-enterprise-apis.ts'
|
||||
: 'packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts';
|
||||
|
||||
// Using app path, so the imports work on any file level
|
||||
const clientImportPath = isEnterprise ? '../extensions/api/clients' : 'app/api/clients';
|
||||
const clientImportPath = isEnterprise ? '../extensions/api/clients' : '.';
|
||||
|
||||
const apiPathPrefix = isEnterprise ? '../public/app/extensions/api/clients' : '../public/app/api/clients';
|
||||
const apiPathPrefix = isEnterprise ? '../public/app/extensions/api/clients' : '../clients';
|
||||
|
||||
const templateData = {
|
||||
...data,
|
||||
@@ -68,27 +70,24 @@ export default function plopGenerator(plop: NodePlopAPI) {
|
||||
actions.push(
|
||||
{
|
||||
type: 'modify',
|
||||
path: path.join(basePath, 'public/app/core/reducers/root.ts'),
|
||||
path: '../clients/rtkq/index.ts',
|
||||
pattern: '// PLOP_INJECT_IMPORT',
|
||||
template: `import { ${reducerPath} } from '${clientImportPath}/${groupName}/${version}';\n// PLOP_INJECT_IMPORT`,
|
||||
template: `import { generatedAPI as ${reducerPath} } from '${clientImportPath}/${groupName}/${version}';\n// PLOP_INJECT_IMPORT`,
|
||||
},
|
||||
{
|
||||
type: 'modify',
|
||||
path: path.join(basePath, 'public/app/core/reducers/root.ts'),
|
||||
path: '../clients/rtkq/index.ts',
|
||||
pattern: '// PLOP_INJECT_REDUCER',
|
||||
template: `[${reducerPath}.reducerPath]: ${reducerPath}.reducer,\n // PLOP_INJECT_REDUCER`,
|
||||
},
|
||||
{
|
||||
type: 'modify',
|
||||
path: path.join(basePath, 'public/app/store/configureStore.ts'),
|
||||
pattern: '// PLOP_INJECT_IMPORT',
|
||||
template: `import { ${reducerPath} } from '${clientImportPath}/${groupName}/${version}';\n// PLOP_INJECT_IMPORT`,
|
||||
},
|
||||
{
|
||||
type: 'modify',
|
||||
path: path.join(basePath, 'public/app/store/configureStore.ts'),
|
||||
path: '../clients/rtkq/index.ts',
|
||||
pattern: '// PLOP_INJECT_MIDDLEWARE',
|
||||
template: `${reducerPath}.middleware,\n // PLOP_INJECT_MIDDLEWARE`,
|
||||
},
|
||||
{
|
||||
type: 'updatePackageJsonExports',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -108,6 +107,16 @@ export default function plopGenerator(plop: NodePlopAPI) {
|
||||
return actions;
|
||||
};
|
||||
|
||||
// Read files from data/openapi directory and use as an array of API clients that the user can select
|
||||
|
||||
const openapiDir = path.join(basePath, 'data/openapi');
|
||||
let possibleOpenAPISpecs: string[] = [];
|
||||
try {
|
||||
possibleOpenAPISpecs = fs.readdirSync(openapiDir).filter((file: string) => file.endsWith('.json'));
|
||||
} catch (e) {
|
||||
possibleOpenAPISpecs = [];
|
||||
}
|
||||
|
||||
const generator: PlopGeneratorConfig = {
|
||||
description: 'Generate RTK Query API client for a Grafana API group',
|
||||
prompts: [
|
||||
@@ -118,30 +127,43 @@ export default function plopGenerator(plop: NodePlopAPI) {
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'groupName',
|
||||
message: 'API group name (e.g. dashboard):',
|
||||
type: 'list',
|
||||
loop: false,
|
||||
choices: possibleOpenAPISpecs,
|
||||
pageSize: 50,
|
||||
name: 'apiInfo',
|
||||
message: 'OpenAPI spec:',
|
||||
validate: (input: string) => (input?.trim() ? true : 'Group name is required'),
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'groupName',
|
||||
message: 'API group name:',
|
||||
validate: (input: string) => (input?.trim() ? true : 'Group name is required'),
|
||||
default: (answers: { apiInfo?: string }) => answers.apiInfo?.split('.grafana.app-')[0],
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'group',
|
||||
message: 'API group (e.g. dashboard.grafana.app):',
|
||||
message: 'API group:',
|
||||
default: (answers: { groupName?: string }) => `${answers.groupName}.grafana.app`,
|
||||
validate: validateGroup,
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'version',
|
||||
message: 'API version (e.g. v0alpha1):',
|
||||
default: 'v0alpha1',
|
||||
message: 'API version:',
|
||||
default: (answers: { apiInfo?: string }) => answers.apiInfo?.split('.grafana.app-')[1].replace(/\.json$/, ''),
|
||||
validate: validateVersion,
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'reducerPath',
|
||||
message: 'Reducer path (e.g. dashboardAPIv0alpha1):',
|
||||
default: (answers: { groupName?: string; version?: string }) => `${answers.groupName}API${answers.version}`,
|
||||
message: 'Reducer path:',
|
||||
default: (answers: { groupName?: string; version?: string }) => {
|
||||
const groupNameParsed = answers.groupName?.replace(/\.([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
return `${groupNameParsed}API${answers.version}`;
|
||||
},
|
||||
validate: (input: string) =>
|
||||
input?.endsWith('API') || input?.match(/API[a-z]\d+[a-z]*\d*$/)
|
||||
? true
|
||||
@@ -150,8 +172,8 @@ export default function plopGenerator(plop: NodePlopAPI) {
|
||||
{
|
||||
type: 'input',
|
||||
name: 'endpoints',
|
||||
default: undefined,
|
||||
message: 'Endpoints to include (comma-separated, optional):',
|
||||
validate: () => true,
|
||||
},
|
||||
],
|
||||
actions: function (data) {
|
||||
@@ -0,0 +1,21 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
{{#if isEnterprise}}
|
||||
import { getAPIBaseURL } from '@grafana/api-clients';
|
||||
import { createBaseQuery } from '@grafana/api-clients/rtkq';
|
||||
{{else}}
|
||||
import { getAPIBaseURL } from '../../../../utils/utils';
|
||||
import { createBaseQuery } from '../../createBaseQuery';
|
||||
{{/if}}
|
||||
|
||||
export const API_GROUP = '{{group}}' as const;
|
||||
export const API_VERSION = '{{version}}' as const;
|
||||
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: '{{reducerPath}}',
|
||||
baseQuery: createBaseQuery({
|
||||
baseURL: BASE_URL,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
...createAPIConfig('{{groupName}}', '{{version}}'{{#if endpoints}}, [{{{formatEndpoints endpoints}}}]{{/if}}),
|
||||
// PLOP_INJECT_API_CLIENT - Used by the API client generator
|
||||
@@ -0,0 +1,5 @@
|
||||
export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
|
||||
import { generatedAPI as rawAPI } from './endpoints.gen';
|
||||
|
||||
export * from './endpoints.gen';
|
||||
export const generatedAPI = rawAPI.enhanceEndpoints({});
|
||||
+10
-1
@@ -10,8 +10,17 @@ export interface RunGenerateApisActionConfig {
|
||||
isEnterprise: boolean;
|
||||
}
|
||||
|
||||
export interface UpdatePackageJsonExportsActionConfig {
|
||||
type: 'updatePackageJsonExports';
|
||||
}
|
||||
|
||||
// Union type of all possible action configs
|
||||
export type ActionConfig = AddActionConfig | ModifyActionConfig | FormatFilesActionConfig | RunGenerateApisActionConfig;
|
||||
export type ActionConfig =
|
||||
| AddActionConfig
|
||||
| ModifyActionConfig
|
||||
| FormatFilesActionConfig
|
||||
| RunGenerateApisActionConfig
|
||||
| UpdatePackageJsonExportsActionConfig;
|
||||
|
||||
export interface PlopData {
|
||||
groupName: string;
|
||||
@@ -0,0 +1 @@
|
||||
export { getAPINamespace, getAPIBaseURL, normalizeError, handleRequestError } from './utils/utils';
|
||||
@@ -0,0 +1,111 @@
|
||||
// Generates Redux Toolkit API slices for certain APIs from the OpenAPI spec
|
||||
import type { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
import path from 'path';
|
||||
|
||||
// Grafana root path - navigate up from this script's directory
|
||||
const basePath = path.resolve(__dirname, '../../../..');
|
||||
|
||||
// Include some types that are used inside the @rtk-query/codegen-openapi package
|
||||
// but not exported
|
||||
declare const operationKeys: readonly ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
|
||||
type OperationDefinition = {
|
||||
path: string;
|
||||
verb: (typeof operationKeys)[number];
|
||||
pathItem: OpenAPIV3.PathItemObject;
|
||||
operation: OpenAPIV3.OperationObject;
|
||||
};
|
||||
type EndpointMatcher = string[] | ((operationName: string, operationDefinition: OperationDefinition) => boolean);
|
||||
|
||||
/**
|
||||
* Helper to return consistent base API generation config
|
||||
*/
|
||||
const createAPIConfig = (app: string, version: string, filterEndpoints?: EndpointMatcher, additional = {}) => {
|
||||
const filePath = `../clients/rtkq/${app}/${version}/endpoints.gen.ts`;
|
||||
return {
|
||||
[filePath]: {
|
||||
schemaFile: path.join(basePath, `data/openapi/${app}.grafana.app-${version}.json`),
|
||||
apiFile: `../clients/rtkq/${app}/${version}/baseAPI.ts`,
|
||||
filterEndpoints,
|
||||
tag: true,
|
||||
...additional,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const config: ConfigFile = {
|
||||
schemaFile: '', // leave this empty, and instead populate the outputFiles object below
|
||||
apiFile: '', // leave this empty, and instead populate the outputFiles object below
|
||||
exportName: 'generatedAPI',
|
||||
|
||||
outputFiles: {
|
||||
'../clients/rtkq/migrate-to-cloud/endpoints.gen.ts': {
|
||||
schemaFile: path.join(basePath, 'public/openapi3.json'),
|
||||
apiFile: '../clients/rtkq/migrate-to-cloud/baseAPI.ts',
|
||||
hooks: true,
|
||||
filterEndpoints: [
|
||||
'getSessionList',
|
||||
'getSession',
|
||||
'deleteSession',
|
||||
'createSession',
|
||||
|
||||
'getShapshotList',
|
||||
'getSnapshot',
|
||||
'uploadSnapshot',
|
||||
'createSnapshot',
|
||||
'cancelSnapshot',
|
||||
|
||||
'createCloudMigrationToken',
|
||||
'deleteCloudMigrationToken',
|
||||
'getCloudMigrationToken',
|
||||
|
||||
'getDashboardByUid',
|
||||
'getLibraryElementByUid',
|
||||
|
||||
'getResourceDependencies',
|
||||
],
|
||||
},
|
||||
'../clients/rtkq/preferences/user/endpoints.gen.ts': {
|
||||
schemaFile: path.join(basePath, 'public/openapi3.json'),
|
||||
hooks: true,
|
||||
apiFile: '../clients/rtkq/preferences/user/baseAPI.ts',
|
||||
filterEndpoints: ['getUserPreferences', 'updateUserPreferences', 'patchUserPreferences'],
|
||||
},
|
||||
'../clients/rtkq/user/endpoints.gen.ts': {
|
||||
schemaFile: path.join(basePath, 'public/openapi3.json'),
|
||||
hooks: true,
|
||||
apiFile: '../clients/rtkq/user/baseAPI.ts',
|
||||
filterEndpoints: ['starDashboardByUid', 'unstarDashboardByUid'],
|
||||
},
|
||||
...createAPIConfig('iam', 'v0alpha1', ['getDisplayMapping']),
|
||||
...createAPIConfig('provisioning', 'v0alpha1', filterEndpoints, { hooks: true }),
|
||||
...createAPIConfig('folder', 'v1beta1', undefined),
|
||||
...createAPIConfig('advisor', 'v0alpha1', [
|
||||
'createCheck',
|
||||
'getCheck',
|
||||
'listCheck',
|
||||
'deleteCheck',
|
||||
'updateCheck',
|
||||
'listCheckType',
|
||||
'updateCheckType',
|
||||
]),
|
||||
...createAPIConfig('playlist', 'v0alpha1', [
|
||||
'listPlaylist',
|
||||
'getPlaylist',
|
||||
'createPlaylist',
|
||||
'deletePlaylist',
|
||||
'replacePlaylist',
|
||||
]),
|
||||
...createAPIConfig('shorturl', 'v1alpha1'),
|
||||
...createAPIConfig('preferences', 'v1alpha1', undefined, { hooks: true }),
|
||||
...createAPIConfig('dashboard', 'v0alpha1', ['getSearch']),
|
||||
...createAPIConfig('correlations', 'v0alpha1'),
|
||||
// PLOP_INJECT_API_CLIENT - Used by the API client generator
|
||||
},
|
||||
};
|
||||
|
||||
function filterEndpoints(name: string) {
|
||||
return !name.toLowerCase().includes('getapiresources') && !name.toLowerCase().includes('update');
|
||||
}
|
||||
|
||||
export default config;
|
||||
+6
-3
@@ -172,11 +172,14 @@ function processDirectory(sourceDir: string, outputDir: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Grafana root path - navigate up from this script's directory
|
||||
const basePath = path.resolve(import.meta.dirname, '../../../..');
|
||||
|
||||
const sourceDirs = [
|
||||
path.resolve(import.meta.dirname, '../pkg/tests/apis/openapi_snapshots'),
|
||||
path.resolve(import.meta.dirname, '../pkg/extensions/apiserver/tests/openapi_snapshots'),
|
||||
path.join(basePath, 'pkg/tests/apis/openapi_snapshots'),
|
||||
path.join(basePath, 'pkg/extensions/apiserver/tests/openapi_snapshots'),
|
||||
];
|
||||
const outputDir = path.resolve(import.meta.dirname, '../data/openapi');
|
||||
const outputDir = path.join(basePath, 'data/openapi');
|
||||
|
||||
for (const sourceDir of sourceDirs) {
|
||||
processDirectory(sourceDir, outputDir);
|
||||
@@ -0,0 +1,62 @@
|
||||
import { config, isFetchError } from '@grafana/runtime';
|
||||
|
||||
export const getAPINamespace = () => config.namespace;
|
||||
|
||||
/**
|
||||
* Get a base URL for a k8s API endpoint with parameterised namespace given it's group and version
|
||||
* @param group the k8s group, e.g. dashboard.grafana.app
|
||||
* @param version e.g. v0alpha1
|
||||
* @returns
|
||||
*/
|
||||
export const getAPIBaseURL = (group: string, version: string) => {
|
||||
return `/apis/${group}/${version}/namespaces/${getAPINamespace()}` as const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize error from various error formats into a string message
|
||||
* @param e the raw error
|
||||
* @returns string error message
|
||||
*/
|
||||
export const normalizeError = (e: unknown): string => {
|
||||
if (!e) {
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
if (typeof e === 'object') {
|
||||
if ('data' in e && e.data && typeof e.data === 'object' && 'message' in e.data) {
|
||||
return String(e.data.message);
|
||||
}
|
||||
|
||||
// Direct error with message property
|
||||
if ('message' in e) {
|
||||
return String(e.message);
|
||||
}
|
||||
|
||||
// Nested error wrapper (RTK Query format)
|
||||
if ('error' in e) {
|
||||
if (e.error instanceof Error) {
|
||||
return e.error.message;
|
||||
} else if (isFetchError(e.error)) {
|
||||
if (e.error.data && typeof e.error.data === 'object' && 'message' in e.error.data) {
|
||||
return String(e.error.data.message);
|
||||
}
|
||||
if (Array.isArray(e.error.data.errors) && e.error.data.errors.length) {
|
||||
return e.error.data.errors.join('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e instanceof Error) {
|
||||
return e.message;
|
||||
}
|
||||
|
||||
return String(e);
|
||||
};
|
||||
export function handleRequestError(error: unknown) {
|
||||
if (isFetchError(error) || error instanceof Error) {
|
||||
return { error };
|
||||
} else {
|
||||
return { error: new Error('Unknown error') };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["dist", "node_modules", "**/*.test.ts*"]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "../../scripts/tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"declaration": true,
|
||||
"jsx": "react-jsx",
|
||||
"declarationDir": "./dist/types",
|
||||
"emitDeclarationOnly": true,
|
||||
"isolatedModules": true,
|
||||
"rootDirs": ["."],
|
||||
"allowImportingTsExtensions": true
|
||||
},
|
||||
"exclude": ["dist/**/*"],
|
||||
"include": [
|
||||
"../../public/app/types/window.d.ts",
|
||||
"../../public/app/types/images.d.ts",
|
||||
"../grafana-ui/src/types/*.d.ts",
|
||||
"../grafana-i18n/src/types/*.d.ts",
|
||||
"src/**/*.ts*"
|
||||
]
|
||||
}
|
||||
@@ -25,6 +25,7 @@ export interface Action {
|
||||
*/
|
||||
export interface ActionModel<T = any> {
|
||||
title: string;
|
||||
type?: ActionType;
|
||||
onClick: (event: any, origin?: any, actionVars?: ActionVariableInput) => void;
|
||||
confirmation: (actionVars?: ActionVariableInput) => ReactNode;
|
||||
oneClick?: boolean;
|
||||
|
||||
@@ -712,10 +712,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
unifiedStorageSearchSprinkles?: boolean;
|
||||
/**
|
||||
* Use full n-gram indexing instead of edge n-gram for unified storage search
|
||||
*/
|
||||
unifiedStorageUseFullNgram?: boolean;
|
||||
/**
|
||||
* Pick the dual write mode from database configs
|
||||
*/
|
||||
managedDualWriter?: boolean;
|
||||
@@ -1055,11 +1051,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
restoreDashboards?: boolean;
|
||||
/**
|
||||
* Skip token rotation if it was already rotated less than 5 seconds ago
|
||||
* @default true
|
||||
*/
|
||||
skipTokenRotationIfRecent?: boolean;
|
||||
/**
|
||||
* Enable configuration of alert enrichments in Grafana Cloud.
|
||||
* @default false
|
||||
*/
|
||||
@@ -1119,11 +1110,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
foldersAppPlatformAPI?: boolean;
|
||||
/**
|
||||
* Set this to true to use the new PluginImporter functionality
|
||||
* @default false
|
||||
*/
|
||||
enablePluginImporter?: boolean;
|
||||
/**
|
||||
* Applies OTel formatting templates to displayed logs
|
||||
*/
|
||||
otelLogsFormatting?: boolean;
|
||||
@@ -1232,7 +1218,7 @@ export interface FeatureToggles {
|
||||
*/
|
||||
preventPanelChromeOverflow?: boolean;
|
||||
/**
|
||||
* Load plugins during store service startup instead of wire provider
|
||||
* Load plugins on store service startup instead of wire provider, and call RegisterFixedRoles after all plugins are loaded
|
||||
* @default false
|
||||
*/
|
||||
pluginStoreServiceLoading?: boolean;
|
||||
|
||||
@@ -33,5 +33,5 @@
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-virtual-modules": "^0.6.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.4"
|
||||
"packageManager": "yarn@4.10.3"
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface Options {
|
||||
setDisplayedFields?: unknown;
|
||||
showCommonLabels: boolean;
|
||||
showControls?: boolean;
|
||||
showFieldSelector?: boolean;
|
||||
showLabels: boolean;
|
||||
showLogAttributes?: boolean;
|
||||
showLogContextToggle: boolean;
|
||||
|
||||
@@ -93,6 +93,7 @@ export const getModalStyles = (theme: GrafanaTheme2) => {
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(0.5),
|
||||
zIndex: 1,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -293,10 +293,6 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
// Adds subtle shadow in light themes to help bar stand out
|
||||
'.radial-arc-path': {
|
||||
filter: theme.isLight ? `drop-shadow(0px 0px 1px #888);` : '',
|
||||
},
|
||||
}),
|
||||
clearButton: css({
|
||||
background: 'transparent',
|
||||
|
||||
@@ -59,7 +59,7 @@ export const TableCellActions = memo(
|
||||
aria-label={t('grafana-ui.table.cell-filter-on', 'Filter for value')}
|
||||
onClick={() => {
|
||||
onCellFilterAdded?.({
|
||||
key: displayName,
|
||||
key: field.name,
|
||||
operator: FILTER_FOR_OPERATOR,
|
||||
value: String(value ?? ''),
|
||||
});
|
||||
@@ -70,7 +70,7 @@ export const TableCellActions = memo(
|
||||
aria-label={t('grafana-ui.table.cell-filter-out', 'Filter out value')}
|
||||
onClick={() => {
|
||||
onCellFilterAdded?.({
|
||||
key: displayName,
|
||||
key: field.name,
|
||||
operator: FILTER_OUT_OPERATOR,
|
||||
value: String(value ?? ''),
|
||||
});
|
||||
|
||||
@@ -1262,7 +1262,7 @@ type GetHomeDashboardResponseBody struct {
|
||||
// swagger:response dashboardVersionsResponse
|
||||
type DashboardVersionsResponse struct {
|
||||
// in: body
|
||||
Body []dashver.DashboardVersionMeta `json:"body"`
|
||||
Body *dashver.DashboardVersionResponseMeta `json:"body"`
|
||||
}
|
||||
|
||||
// swagger:response dashboardVersionResponse
|
||||
|
||||
@@ -132,6 +132,7 @@ type NewBackendOpts struct {
|
||||
Tags []string
|
||||
Static bool
|
||||
WireTag string
|
||||
CGOEnabled bool
|
||||
GoBuildCache *dagger.CacheVolume
|
||||
GoModCache *dagger.CacheVolume
|
||||
}
|
||||
@@ -198,6 +199,12 @@ func NewBackendFromString(ctx context.Context, log *slog.Logger, artifact string
|
||||
goCacheProg = val
|
||||
}
|
||||
|
||||
cgoDisabled, err := options.Bool(flags.CGODisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cgoEnabled := !cgoDisabled
|
||||
|
||||
bopts := &backend.BuildOpts{
|
||||
Version: p.Version,
|
||||
Enterprise: p.Enterprise,
|
||||
@@ -206,6 +213,7 @@ func NewBackendFromString(ctx context.Context, log *slog.Logger, artifact string
|
||||
Static: static,
|
||||
WireTag: wireTag,
|
||||
Tags: tags,
|
||||
CGOEnabled: cgoEnabled,
|
||||
}
|
||||
|
||||
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
|
||||
@@ -233,9 +241,10 @@ func NewBackend(ctx context.Context, log *slog.Logger, artifact string, opts *Ne
|
||||
Tags: opts.Tags,
|
||||
Static: opts.Static,
|
||||
WireTag: opts.WireTag,
|
||||
CGOEnabled: opts.CGOEnabled,
|
||||
}
|
||||
|
||||
log.Info("Initializing backend artifact with options", "static", opts.Static, "version", opts.Version, "name", opts.Name, "distro", opts.Distribution)
|
||||
log.Info("Initializing backend artifact with options", "static", opts.Static, "version", opts.Version, "name", opts.Name, "distro", opts.Distribution, "cgo enabled", opts.CGOEnabled)
|
||||
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
|
||||
ArtifactString: artifact,
|
||||
Type: pipeline.ArtifactTypeDirectory,
|
||||
|
||||
@@ -119,7 +119,12 @@ func NewTarballFromString(ctx context.Context, log *slog.Logger, artifact string
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewTarball(ctx, log, artifact, p.Distribution, p.Enterprise, p.Name, p.Version, p.BuildID, src, yarnCache, goModCache, goBuildCache, static, wireTag, tags, goVersion, viceroyVersion, experiments)
|
||||
cgoDisabled, err := options.Bool(flags.CGODisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cgoEnabled := !cgoDisabled
|
||||
return NewTarball(ctx, log, artifact, p.Distribution, p.Enterprise, p.Name, p.Version, p.BuildID, src, yarnCache, goModCache, goBuildCache, static, wireTag, tags, goVersion, viceroyVersion, experiments, cgoEnabled)
|
||||
}
|
||||
|
||||
// NewTarball returns a properly initialized Tarball artifact.
|
||||
@@ -143,6 +148,7 @@ func NewTarball(
|
||||
goVersion string,
|
||||
viceroyVersion string,
|
||||
experiments []string,
|
||||
cgoEnabled bool,
|
||||
) (*pipeline.Artifact, error) {
|
||||
backendArtifact, err := NewBackend(ctx, log, artifact, &NewBackendOpts{
|
||||
Name: name,
|
||||
@@ -158,6 +164,7 @@ func NewTarball(
|
||||
Enterprise: enterprise,
|
||||
GoBuildCache: goBuildCache,
|
||||
GoModCache: goModCache,
|
||||
CGOEnabled: cgoEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -62,7 +62,7 @@ func Build(
|
||||
|
||||
ldflags := LDFlagsDynamic(vcsinfo)
|
||||
|
||||
if opts.Static {
|
||||
if opts.Static && opts.CGOEnabled {
|
||||
ldflags = LDFlagsStatic(vcsinfo)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,23 @@ type BuildOpts struct {
|
||||
GoCacheProg string
|
||||
Static bool
|
||||
Enterprise bool
|
||||
CGOEnabled bool
|
||||
}
|
||||
|
||||
func distroOptsFunc(log *slog.Logger, distro Distribution) (DistroBuildOptsFunc, error) {
|
||||
func distroOptsFunc(log *slog.Logger, distro Distribution, opts *BuildOpts) (DistroBuildOptsFunc, error) {
|
||||
if !opts.CGOEnabled {
|
||||
return func(distro Distribution, experiments, tags []string) *GoBuildOpts {
|
||||
os, arch := OSAndArch(distro)
|
||||
archv := ArchVersion(distro)
|
||||
return &GoBuildOpts{
|
||||
OS: os,
|
||||
Arch: arch,
|
||||
GoARM: GoARM(archv),
|
||||
CGOEnabled: false,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
if val, ok := DistributionGoOpts[distro]; ok {
|
||||
return DistroOptsLogger(log, val), nil
|
||||
}
|
||||
@@ -29,7 +43,7 @@ func distroOptsFunc(log *slog.Logger, distro Distribution) (DistroBuildOptsFunc,
|
||||
}
|
||||
|
||||
func WithGoEnv(log *slog.Logger, container *dagger.Container, distro Distribution, opts *BuildOpts) (*dagger.Container, error) {
|
||||
fn, err := distroOptsFunc(log, distro)
|
||||
fn, err := distroOptsFunc(log, distro, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -39,7 +53,7 @@ func WithGoEnv(log *slog.Logger, container *dagger.Container, distro Distributio
|
||||
}
|
||||
|
||||
func WithViceroyEnv(log *slog.Logger, container *dagger.Container, distro Distribution, opts *BuildOpts) (*dagger.Container, error) {
|
||||
fn, err := distroOptsFunc(log, distro)
|
||||
fn, err := distroOptsFunc(log, distro, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -92,24 +106,27 @@ func GolangContainer(
|
||||
) (*dagger.Container, error) {
|
||||
os, _ := OSAndArch(distro)
|
||||
// Only use viceroy for all darwin and only windows/amd64
|
||||
if os == "darwin" || distro == DistWindowsAMD64 {
|
||||
if opts.CGOEnabled && (os == "darwin" || distro == DistWindowsAMD64) {
|
||||
return ViceroyContainer(d, log, distro, goVersion, viceroyVersion, opts)
|
||||
}
|
||||
|
||||
container := golang.Container(d, platform, goVersion).
|
||||
WithExec([]string{"apk", "add", "--update", "wget", "build-base", "alpine-sdk", "musl", "musl-dev", "xz"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/zig-linux-x86_64-0.11.0.tar.xz"}).
|
||||
WithExec([]string{"tar", "--strip-components=1", "-C", "/", "-xf", "zig-linux-x86_64-0.11.0.tar.xz"}).
|
||||
WithExec([]string{"mv", "/zig", "/bin/zig"}).
|
||||
// Install the toolchain specifically for armv7 until we figure out why it's crashing w/ zig container = container.
|
||||
WithExec([]string{"mkdir", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "http://dl.grafana.com/ci/arm-linux-musleabihf-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/arm-linux-musleabihf-cross.tgz", "-C", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/s390x-linux-musl-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/s390x-linux-musl-cross.tgz", "-C", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/riscv64-linux-musl-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/riscv64-linux-musl-cross.tgz", "-C", "/toolchain"})
|
||||
container := golang.Container(d, platform, goVersion)
|
||||
|
||||
if opts.CGOEnabled {
|
||||
container = container.
|
||||
WithExec([]string{"apk", "add", "--update", "wget", "build-base", "alpine-sdk", "musl", "musl-dev", "xz"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/zig-linux-x86_64-0.11.0.tar.xz"}).
|
||||
WithExec([]string{"tar", "--strip-components=1", "-C", "/", "-xf", "zig-linux-x86_64-0.11.0.tar.xz"}).
|
||||
WithExec([]string{"mv", "/zig", "/bin/zig"}).
|
||||
// Install the toolchain specifically for armv7 until we figure out why it's crashing w/ zig container = container.
|
||||
WithExec([]string{"mkdir", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "http://dl.grafana.com/ci/arm-linux-musleabihf-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/arm-linux-musleabihf-cross.tgz", "-C", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/s390x-linux-musl-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/s390x-linux-musl-cross.tgz", "-C", "/toolchain"}).
|
||||
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/riscv64-linux-musl-cross.tgz", "-P", "/toolchain"}).
|
||||
WithExec([]string{"tar", "-xf", "/toolchain/riscv64-linux-musl-cross.tgz", "-C", "/toolchain"})
|
||||
}
|
||||
return WithGoEnv(log, container, distro, opts)
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user