diff --git a/Makefile b/Makefile index d8522d72f..302a16ad5 100644 --- a/Makefile +++ b/Makefile @@ -235,7 +235,6 @@ function cleanup { \ set +e; \ $(OPERATOR_SDK) cleanup -n $(SCORECARD_NAMESPACE) $(OPERATOR_NAME); \ $(KUSTOMIZE) build internal/images/custom-scorecard-tests/rbac/ | $(CLUSTER_CLIENT) delete --ignore-not-found=$(ignore-not-found) -f -; \ - $(CLUSTER_CLIENT) delete --ignore-not-found=$(ignore-not-found) -n $(SCORECARD_NAMESPACE) secret registry-key; \ $(CLUSTER_CLIENT) delete --ignore-not-found=$(ignore-not-found) namespace $(SCORECARD_NAMESPACE); \ )\ } @@ -244,7 +243,10 @@ endef define scorecard-local for test in $${SCORECARD_TEST_SELECTION//,/ }; do \ echo "Running scorecard test \"$${test}\""; \ - SCORECARD_NAMESPACE=$(SCORECARD_NAMESPACE) BUNDLE_DIR=./bundle go run internal/images/custom-scorecard-tests/main.go $${test} | sed 's/\\n/\n/g'; \ + SCORECARD_NAMESPACE=$(SCORECARD_NAMESPACE) \ + BUNDLE_DIR=./bundle \ + TESTDATA_DIR=./internal/test/scorecard/testdata \ + go run internal/images/custom-scorecard-tests/main.go $${test} | sed 's/\\n/\n/g'; \ done endef diff --git a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml index be7702234..28beac79f 100644 --- a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml @@ -30,7 +30,7 @@ metadata: capabilities: Seamless Upgrades categories: Monitoring, Developer Tools containerImage: quay.io/cryostat/cryostat-operator:4.0.0-dev - createdAt: "2024-09-11T17:33:08Z" + createdAt: "2024-09-12T23:20:42Z" description: JVM monitoring and profiling tool operatorframework.io/initialization-resource: |- { diff --git a/bundle/tests/scorecard/config.yaml b/bundle/tests/scorecard/config.yaml index cc6e4cb49..cdb33e165 100644 --- a/bundle/tests/scorecard/config.yaml +++ b/bundle/tests/scorecard/config.yaml @@ -70,7 +70,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - operator-install - image: quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240911172313 + image: quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240912231617 labels: suite: cryostat test: operator-install @@ -80,7 +80,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - cryostat-cr - image: quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240911172313 + image: quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240912231617 labels: suite: cryostat test: cryostat-cr @@ -90,7 +90,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - cryostat-multi-namespace - image: quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240911172313 + image: quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240912231617 labels: suite: cryostat test: cryostat-multi-namespace @@ -100,7 +100,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - cryostat-recording - image: quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240911172313 + image: quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240912231617 labels: suite: cryostat test: cryostat-recording @@ -110,7 +110,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - cryostat-config-change - image: quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240911172313 + image: quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240912231617 labels: suite: cryostat test: cryostat-config-change @@ -120,13 +120,23 @@ stages: - entrypoint: - cryostat-scorecard-tests - cryostat-report - image: quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240911172313 + image: quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240912231617 labels: suite: cryostat test: cryostat-report storage: spec: mountPath: {} + - entrypoint: + - cryostat-scorecard-tests + - cryostat-grafana + image: quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240912231617 + labels: + suite: cryostat + test: cryostat-grafana + storage: + spec: + mountPath: {} storage: spec: mountPath: {} diff --git a/config/scorecard/patches/custom.config.yaml b/config/scorecard/patches/custom.config.yaml index bec7ea861..38432bfc8 100644 --- a/config/scorecard/patches/custom.config.yaml +++ b/config/scorecard/patches/custom.config.yaml @@ -8,7 +8,7 @@ entrypoint: - cryostat-scorecard-tests - operator-install - image: "quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240911172313" + image: "quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240912231617" labels: suite: cryostat test: operator-install @@ -18,7 +18,7 @@ entrypoint: - cryostat-scorecard-tests - cryostat-cr - image: "quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240911172313" + image: "quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240912231617" labels: suite: cryostat test: cryostat-cr @@ -28,7 +28,7 @@ entrypoint: - cryostat-scorecard-tests - cryostat-multi-namespace - image: "quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240911172313" + image: "quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240912231617" labels: suite: cryostat test: cryostat-multi-namespace @@ -38,7 +38,7 @@ entrypoint: - cryostat-scorecard-tests - cryostat-recording - image: "quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240911172313" + image: "quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240912231617" labels: suite: cryostat test: cryostat-recording @@ -48,7 +48,7 @@ entrypoint: - cryostat-scorecard-tests - cryostat-config-change - image: "quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240911172313" + image: "quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240912231617" labels: suite: cryostat test: cryostat-config-change @@ -58,7 +58,17 @@ entrypoint: - cryostat-scorecard-tests - cryostat-report - image: "quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240911172313" + image: "quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240912231617" labels: suite: cryostat test: cryostat-report +- op: add + path: /stages/1/tests/- + value: + entrypoint: + - cryostat-scorecard-tests + - cryostat-grafana + image: "quay.io/cryostat/cryostat-operator-scorecard:4.0.0-20240912231617" + labels: + suite: cryostat + test: cryostat-grafana diff --git a/hack/custom.config.yaml.in b/hack/custom.config.yaml.in index 75306a910..464dcbbcd 100644 --- a/hack/custom.config.yaml.in +++ b/hack/custom.config.yaml.in @@ -61,3 +61,13 @@ labels: suite: cryostat test: cryostat-report +- op: add + path: /stages/1/tests/- + value: + entrypoint: + - cryostat-scorecard-tests + - cryostat-grafana + image: "${CUSTOM_SCORECARD_IMG}" + labels: + suite: cryostat + test: cryostat-grafana diff --git a/internal/images/custom-scorecard-tests/main.go b/internal/images/custom-scorecard-tests/main.go index 3ef4d98e4..1091d5276 100644 --- a/internal/images/custom-scorecard-tests/main.go +++ b/internal/images/custom-scorecard-tests/main.go @@ -90,6 +90,7 @@ func printValidTests() []scapiv1alpha3.TestResult { tests.CryostatRecordingTestName, tests.CryostatConfigChangeTestName, tests.CryostatReportTestName, + tests.CryostatGrafanaTestName, }, ",")) result.Errors = append(result.Errors, str) @@ -105,6 +106,7 @@ func validateTests(testNames []string) bool { case tests.CryostatRecordingTestName: case tests.CryostatConfigChangeTestName: case tests.CryostatReportTestName: + case tests.CryostatGrafanaTestName: default: return false } @@ -131,6 +133,8 @@ func runTests(testNames []string, bundle *apimanifests.Bundle, namespace string, results = append(results, *tests.CryostatConfigChangeTest(bundle, namespace, openShiftCertManager)) case tests.CryostatReportTestName: results = append(results, *tests.CryostatReportTest(bundle, namespace, openShiftCertManager)) + case tests.CryostatGrafanaTestName: + results = append(results, *tests.CryostatGrafanaTest(bundle, namespace, openShiftCertManager)) default: log.Fatalf("unknown test found: %s", testName) } diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 734a4e42f..6dcb74258 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -15,13 +15,16 @@ package scorecard import ( + "bytes" "context" "encoding/json" "errors" "fmt" "io" + "mime/multipart" "net/http" "net/url" + "os" "strings" "time" @@ -197,6 +200,7 @@ type CryostatRESTClientset struct { TargetClient *TargetClient RecordingClient *RecordingClient CredentialClient *CredentialClient + GrafanaClient *GrafanaClient } func (c *CryostatRESTClientset) Targets() *TargetClient { @@ -211,6 +215,10 @@ func (c *CryostatRESTClientset) Credential() *CredentialClient { return c.CredentialClient } +func (c *CryostatRESTClientset) Grafana() *GrafanaClient { + return c.GrafanaClient +} + func NewCryostatRESTClientset(base *url.URL) *CryostatRESTClientset { commonClient := &commonCryostatRESTClient{ Base: base, @@ -227,6 +235,10 @@ func NewCryostatRESTClientset(base *url.URL) *CryostatRESTClientset { CredentialClient: &CredentialClient{ commonCryostatRESTClient: commonClient, }, + GrafanaClient: &GrafanaClient{ + commonCryostatRESTClient: commonClient, + BasePath: "grafana", + }, } } @@ -271,7 +283,7 @@ func (client *TargetClient) Create(ctx context.Context, options *Target) (*Targe header.Add("Accept", "*/*") body := options.ToFormData() - resp, err := SendRequest(ctx, client.Client, http.MethodPost, url.String(), &body, header) + resp, err := SendRequest(ctx, client.Client, http.MethodPost, url.String(), strings.NewReader(body), header) if err != nil { return nil, err } @@ -341,7 +353,7 @@ func (client *RecordingClient) Create(ctx context.Context, target *Target, optio header.Add("Content-Type", "application/x-www-form-urlencoded") header.Add("Accept", "*/*") - resp, err := SendRequest(ctx, client.Client, http.MethodPost, url.String(), &body, header) + resp, err := SendRequest(ctx, client.Client, http.MethodPost, url.String(), strings.NewReader(body), header) if err != nil { return nil, err } @@ -367,7 +379,7 @@ func (client *RecordingClient) Archive(ctx context.Context, target *Target, reco header.Add("Content-Type", "text/plain") header.Add("Accept", "*/*") - resp, err := SendRequest(ctx, client.Client, http.MethodPatch, url.String(), &body, header) + resp, err := SendRequest(ctx, client.Client, http.MethodPatch, url.String(), strings.NewReader(body), header) if err != nil { return "", err } @@ -392,7 +404,7 @@ func (client *RecordingClient) Stop(ctx context.Context, target *Target, recordi header.Add("Content-Type", "text/plain") header.Add("Accept", "*/*") - resp, err := SendRequest(ctx, client.Client, http.MethodPatch, url.String(), &body, header) + resp, err := SendRequest(ctx, client.Client, http.MethodPatch, url.String(), strings.NewReader(body), header) if err != nil { return err } @@ -485,13 +497,12 @@ func (client *RecordingClient) ListArchives(ctx context.Context, target *Target) if err != nil { return nil, fmt.Errorf("failed to construct graph query: %s", err.Error()) } - body := string(queryJSON) header := make(http.Header) header.Add("Content-Type", "application/json") header.Add("Accept", "*/*") - resp, err := SendRequest(ctx, client.Client, http.MethodPost, url.String(), &body, header) + resp, err := SendRequest(ctx, client.Client, http.MethodPost, url.String(), bytes.NewReader(queryJSON), header) if err != nil { return nil, err } @@ -510,6 +521,65 @@ func (client *RecordingClient) ListArchives(ctx context.Context, target *Target) return graphQLResponse.Data.TargetNodes[0].Target.ArchivedRecordings.Data, nil } +func (client *RecordingClient) UploadArchive(ctx context.Context, filePath string) error { + url := client.Base.JoinPath("/api/v1/recordings") + + body := &bytes.Buffer{} + mp := multipart.NewWriter(body) + + part, err := mp.CreateFormFile("recording", jfrFilename) + if err != nil { + return err + } + + file, err := os.Open(filePath) + if err != nil { + return err + } + defer file.Close() + + if _, err = io.Copy(part, file); err != nil { + return err + } + + if err = mp.Close(); err != nil { + return err + } + + header := make(http.Header) + header.Add("Content-Type", mp.FormDataContentType()) + header.Add("Accept", "*/*") + + resp, err := SendRequest(ctx, client.Client, http.MethodPost, url.String(), body, header) + if err != nil { + return err + } + defer resp.Body.Close() + + if !StatusOK(resp.StatusCode) { + return fmt.Errorf("API request failed with status code: %d, response body: %s, and headers:\n%s", resp.StatusCode, ReadError(resp), ReadHeader(resp)) + } + + return nil +} + +func (client *RecordingClient) LoadUploadedArchiveToGrafana(ctx context.Context, archiveName string) error { + url := client.Base.JoinPath(fmt.Sprintf("/api/beta/recordings/uploads/%s/upload", archiveName)) + header := make(http.Header) + + resp, err := SendRequest(ctx, client.Client, http.MethodPost, url.String(), nil, header) + if err != nil { + return err + } + defer resp.Body.Close() + + if !StatusOK(resp.StatusCode) { + return fmt.Errorf("API request failed with status code: %d, response body: %s, and headers:\n%s", resp.StatusCode, ReadError(resp), ReadHeader(resp)) + } + + return nil +} + type CredentialClient struct { *commonCryostatRESTClient } @@ -520,7 +590,7 @@ func (client *CredentialClient) Create(ctx context.Context, credential *Credenti header := make(http.Header) header.Add("Content-Type", "application/x-www-form-urlencoded") - resp, err := SendRequest(ctx, client.Client, http.MethodPost, url.String(), &body, header) + resp, err := SendRequest(ctx, client.Client, http.MethodPost, url.String(), strings.NewReader(body), header) if err != nil { return err } @@ -533,6 +603,58 @@ func (client *CredentialClient) Create(ctx context.Context, credential *Credenti return nil } +// Client for Grafana API +type GrafanaClient struct { + BasePath string + *commonCryostatRESTClient +} + +func (client *GrafanaClient) GetDashboardByUID(ctx context.Context, uid string) (*DashBoard, error) { + url := client.Base.JoinPath(client.BasePath, "api/dashboards/uid", uid) + header := make(http.Header) + header.Add("Accept", "*/*") + + resp, err := SendRequest(ctx, client.Client, http.MethodGet, url.String(), nil, header) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if !StatusOK(resp.StatusCode) { + return nil, fmt.Errorf("API request failed with status code: %d, response body: %s, and headers:\n%s", resp.StatusCode, ReadError(resp), ReadHeader(resp)) + } + + dashboard := &DashBoard{} + if err = ReadJSON(resp, dashboard); err != nil { + return nil, fmt.Errorf("failed to read response body: %s", err.Error()) + } + + return dashboard, nil +} + +func (client *GrafanaClient) GetDatasourceByName(ctx context.Context, name string) (*DataSource, error) { + url := client.Base.JoinPath(client.BasePath, "api/datasources/name", GRAFANA_DATASOURCE_NAME) + header := make(http.Header) + header.Add("Accept", "*/*") + + resp, err := SendRequest(ctx, client.Client, http.MethodGet, url.String(), nil, header) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if !StatusOK(resp.StatusCode) { + return nil, fmt.Errorf("API request failed with status code: %d, response body: %s, and headers:\n%s", resp.StatusCode, ReadError(resp), ReadHeader(resp)) + } + + datasource := &DataSource{} + if err = ReadJSON(resp, datasource); err != nil { + return nil, fmt.Errorf("failed to read response body: %s", err.Error()) + } + + return datasource, nil +} + func ReadJSON(resp *http.Response, result interface{}) error { body, err := io.ReadAll(resp.Body) if err != nil { @@ -582,12 +704,8 @@ func NewHttpClient() *http.Client { return client } -func NewHttpRequest(ctx context.Context, method string, url string, body *string, header http.Header) (*http.Request, error) { - var reqBody io.Reader - if body != nil { - reqBody = strings.NewReader(*body) - } - req, err := http.NewRequestWithContext(ctx, method, url, reqBody) +func NewHttpRequest(ctx context.Context, method string, url string, body io.Reader, header http.Header) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, method, url, body) if err != nil { return nil, err } @@ -611,7 +729,7 @@ func StatusOK(statusCode int) bool { return statusCode >= 200 && statusCode < 300 } -func SendRequest(ctx context.Context, httpClient *http.Client, method string, url string, body *string, header http.Header) (*http.Response, error) { +func SendRequest(ctx context.Context, httpClient *http.Client, method string, url string, body io.Reader, header http.Header) (*http.Response, error) { var response *http.Response err := wait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (done bool, err error) { // Create a new request diff --git a/internal/test/scorecard/common_utils.go b/internal/test/scorecard/common_utils.go index 1ee9ecc93..013d288df 100644 --- a/internal/test/scorecard/common_utils.go +++ b/internal/test/scorecard/common_utils.go @@ -42,6 +42,9 @@ import ( const ( operatorDeploymentName string = "cryostat-operator-controller" + jfrConfigMapName string = "scorecard-jfr-cm" + jfrFilename string = "scorecard_sample.jfr" + podTestDataRoot string = "/testdata" testTimeout time.Duration = time.Minute * 10 ) @@ -297,6 +300,7 @@ func configureIngress(name string, cryostatSpec *operatorv1beta2.CryostatSpec) { CoreConfig: &operatorv1beta2.NetworkConfiguration{ Annotations: map[string]string{ "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS", + "nginx.ingress.kubernetes.io/proxy-body-size": "10m", }, IngressSpec: &netv1.IngressSpec{ TLS: []netv1.IngressTLS{{}}, diff --git a/internal/test/scorecard/testdata/scorecard_sample.jfr b/internal/test/scorecard/testdata/scorecard_sample.jfr new file mode 100644 index 000000000..8cb669b33 Binary files /dev/null and b/internal/test/scorecard/testdata/scorecard_sample.jfr differ diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index 55b90f899..ac957a65e 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -18,6 +18,8 @@ import ( "context" "fmt" "net/url" + "os" + "path" "time" operatorv1beta2 "github.com/cryostatio/cryostat-operator/api/v1beta2" @@ -32,6 +34,7 @@ const ( CryostatRecordingTestName string = "cryostat-recording" CryostatConfigChangeTestName string = "cryostat-config-change" CryostatReportTestName string = "cryostat-report" + CryostatGrafanaTestName string = "cryostat-grafana" ) // OperatorInstallTest checks that the operator installed correctly @@ -304,3 +307,75 @@ func CryostatReportTest(bundle *apimanifests.Bundle, namespace string, openShift return r.TestResult } + +func CryostatGrafanaTest(bundle *apimanifests.Bundle, namespace string, openShiftCertManager bool) *scapiv1alpha3.TestResult { + r := newTestResources(CryostatGrafanaTestName, namespace) + + err := r.setupCRTestResources(openShiftCertManager) + if err != nil { + return r.fail(fmt.Sprintf("failed to set up %s test: %s", CryostatGrafanaTestName, err.Error())) + } + + defer r.cleanupAndLogs() + + // Create a default Cryostat CR + cr, err := r.createAndWaitTillCryostatAvailable(r.newCryostatCR()) + if err != nil { + return r.fail(fmt.Sprintf("failed to determine application URL: %s", err.Error())) + } + + err = r.StartLogs(cr) + if err != nil { + r.Log += fmt.Sprintf("failed to retrieve logs for the application: %s", err.Error()) + } + + base, err := url.Parse(cr.Status.ApplicationURL) + if err != nil { + return r.fail(fmt.Sprintf("application URL is invalid: %s", err.Error())) + } + + err = r.waitTillCryostatReady(base) + if err != nil { + return r.fail(fmt.Sprintf("failed to reach the application: %s", err.Error())) + } + + apiClient := NewCryostatRESTClientset(base) + + // Get the path to the testdata directory from fromTESTDATA_DIR environment variable + // If empty, assume running within a pod and use "/testdata" + testDataDir := os.Getenv("TESTDATA_DIR") + if len(testDataDir) == 0 { + testDataDir = podTestDataRoot + } + + err = apiClient.Recordings().UploadArchive(context.Background(), path.Join(testDataDir, jfrFilename)) + if err != nil { + return r.fail(fmt.Sprintf("failed to upload archive %s: %s", jfrFilename, err.Error())) + } + + err = apiClient.Recordings().LoadUploadedArchiveToGrafana(context.Background(), jfrFilename) + if err != nil { + return r.fail(fmt.Sprintf("failed to load archive %s to grafana: %s", jfrFilename, err.Error())) + } + + // Validate datasource + datasource, err := apiClient.Grafana().GetDatasourceByName(context.Background(), GRAFANA_DATASOURCE_NAME) + if err != nil { + return r.fail(fmt.Sprintf("failed to get datasource %s: %s", GRAFANA_DATASOURCE_NAME, err.Error())) + } + + if err = datasource.IsValid(); err != nil { + return r.fail(fmt.Sprintf("datasource %s is invalid: %s", GRAFANA_DATASOURCE_NAME, err.Error())) + } + + dashboard, err := apiClient.Grafana().GetDashboardByUID(context.Background(), GRAFANA_DASHBOARD_UID) + if err != nil { + return r.fail(fmt.Sprintf("failed to get dashboard %s: %s", GRAFANA_DASHBOARD_UID, err.Error())) + } + + if err = dashboard.IsValid(); err != nil { + return r.fail(fmt.Sprintf("dashboard %s is invalid: %s", GRAFANA_DASHBOARD_UID, err.Error())) + } + + return r.TestResult +} diff --git a/internal/test/scorecard/types.go b/internal/test/scorecard/types.go index 8c26603de..cbbc89b65 100644 --- a/internal/test/scorecard/types.go +++ b/internal/test/scorecard/types.go @@ -17,6 +17,7 @@ package scorecard import ( "encoding/json" "errors" + "fmt" "net/url" "strconv" ) @@ -152,3 +153,115 @@ type ArchiveGraphQLResponse struct { } `json:"targetNodes"` } `json:"data"` } + +const ( + GRAFANA_DASHBOARD_UID = "main" + GRAFANA_DASHBOARD_TITLE = "Cryostat Dashboard" + GRAFANA_DATASOURCE_NAME = "jfr-datasource" + GRAFANA_DATASOURCE_TYPE = "yesoreyeram-infinity-datasource" + GRAFANA_DATASOURCE_URL = "http://127.0.0.1:8989" + GRAFANA_DATASOURCE_ACCESS = "proxy" +) + +// DataSource represents a Grafana data source +type DataSource struct { + ID uint32 `json:"id"` + UID string `json:"uid"` + Name string `json:"name"` + + Type string `json:"type"` + + URL string `json:"url"` + Access string `json:"access"` + + BasicAuth bool `json:"basicAuth"` +} + +func (datasource *DataSource) IsValid() error { + if datasource.Name != GRAFANA_DATASOURCE_NAME { + return fmt.Errorf("expected datasource name %s, but got %s", GRAFANA_DATASOURCE_NAME, datasource.Name) + } + + if datasource.Type != GRAFANA_DATASOURCE_TYPE { + return fmt.Errorf("expected datasource type %s, but got %s", GRAFANA_DATASOURCE_TYPE, datasource.Type) + } + + if datasource.URL != GRAFANA_DATASOURCE_URL { + return fmt.Errorf("expected datasource url %s, but got %s", GRAFANA_DATASOURCE_URL, datasource.URL) + } + + if datasource.Access != GRAFANA_DATASOURCE_ACCESS { + return fmt.Errorf("expected datasource access mode %s, but got %s", GRAFANA_DATASOURCE_ACCESS, datasource.Access) + } + + if datasource.BasicAuth { + return errors.New("expected basicAuth to be disabled, but got enabled") + } + + return nil +} + +// DashBoard represents a Grafana dashboard +type DashBoard struct { + DashBoardMeta `json:"meta"` + DashBoardInfo `json:"dashboard"` +} + +type DashBoardMeta struct { + Slug string `json:"slug"` + URL string `json:"url"` + Provisioned bool `json:"provisioned"` +} + +type DashBoardInfo struct { + UID string `json:"uid"` + Title string `json:"title"` + Annotations map[string]interface{} `json:"annotations"` + Panels []Panel `json:"panels"` +} + +// Panel represents a Grafana panel. +// A panel can be used either for displaying data or separating groups +type Panel struct { + ID uint32 `json:"id"` + Title string `json:"title"` + Type string `json:"type"` + Targets []PanelQuery `json:"targets"` + Panels []Panel `json:"panels"` +} + +// PanelQuery represents a query for datapoints +type PanelQuery struct { + RefID string `json:"refId"` + Target string `json:"target"` + Type string `json:"type"` + URL string `json:"url"` + URLOptions PanelQueryOptions `json:"url_options"` +} + +type PanelQueryOptions struct { + BodyContentType string `json:"body_content_type"` + BodyType string `json:"body_type"` + Method string `json:"method"` + Data string `json:"data"` +} + +func (dashboard *DashBoard) IsValid() error { + if dashboard.UID != GRAFANA_DASHBOARD_UID { + return fmt.Errorf("expected dashboard uid %s, but got %s", GRAFANA_DASHBOARD_UID, dashboard.UID) + } + + if dashboard.Title != GRAFANA_DASHBOARD_TITLE { + return fmt.Errorf("expected dashboard title %s, but got %s", GRAFANA_DASHBOARD_TITLE, dashboard.Title) + } + + if !dashboard.Provisioned { + return errors.New("expected dashboard to be provisioned, but got unprovisioned") + } + + if len(dashboard.Panels) == 0 { + return errors.New("expected dashboard to have panels, but got 0") + } + + return nil +} diff --git a/scripts/generate_datapoints.bash b/scripts/generate_datapoints.bash new file mode 100644 index 000000000..cf65ac542 --- /dev/null +++ b/scripts/generate_datapoints.bash @@ -0,0 +1,21 @@ +#!/usr/local/env bash + +set -xe + +CRYOSTAT_URL="$1" + +if [ -z $CRYOSTAT_URL ]; then + echo "Cryostat URL is expected as an argument. Found none"; exit 2 +fi + +# Get datasource UID + +DATASOURCE_UID=$(curl -kLs ${CRYOSTAT_URL}/grafana/api/datasources/name/jfr-datasource | jq .uid) + +echo $DATASOURCE_UID + + +BODY="$(curl -ksL https://localhost:8080/grafana/api/dashboards/uid/main | jq .dashboard.panels[0].targets[0].url_options.data | sed -e 's/${__timeTo:date}/2024-08-28T20:23:35.127Z/' -e 's/${__timeFrom:date}/2024-05-01T20:23:35.127Z/')" + + +curl -kL --data $BODY ${CRYOSTAT_URL}/grafana/api/datasources/proxy/uid/$DATASOURCE_UID