- Split up sync requests into batches to run concurrently - Add more logging for when API calls to Grafana fail to parse - Call sync from backend plugin when status is called - Lock sync from backend plugin to only run every 5 mins - Add timer display for API call to sync to return remaining time before sync can execute - Remove locks from celery task since it's work is low cost and we lock in the backend plugin anyways.
277 lines
9.4 KiB
Go
277 lines
9.4 KiB
Go
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
|
)
|
|
|
|
type OnCallPluginConnectionEntry struct {
|
|
Ok bool `json:"ok"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
func (e *OnCallPluginConnectionEntry) SetValid() {
|
|
e.Ok = true
|
|
e.Error = ""
|
|
}
|
|
|
|
func (e *OnCallPluginConnectionEntry) SetInvalid(reason string) {
|
|
e.Ok = false
|
|
e.Error = reason
|
|
}
|
|
|
|
func DefaultPluginConnectionEntry() OnCallPluginConnectionEntry {
|
|
return OnCallPluginConnectionEntry{
|
|
Ok: false,
|
|
Error: "Not validated",
|
|
}
|
|
}
|
|
|
|
type OnCallPluginConnection struct {
|
|
Settings OnCallPluginConnectionEntry `json:"settings"`
|
|
ServiceAccountToken OnCallPluginConnectionEntry `json:"service_account_token"`
|
|
GrafanaURLFromPlugin OnCallPluginConnectionEntry `json:"grafana_url_from_plugin"`
|
|
GrafanaURLFromEngine OnCallPluginConnectionEntry `json:"grafana_url_from_engine"`
|
|
OnCallAPIURL OnCallPluginConnectionEntry `json:"oncall_api_url"`
|
|
OnCallToken OnCallPluginConnectionEntry `json:"oncall_token"`
|
|
}
|
|
|
|
func DefaultPluginConnection() OnCallPluginConnection {
|
|
return OnCallPluginConnection{
|
|
Settings: DefaultPluginConnectionEntry(),
|
|
GrafanaURLFromPlugin: DefaultPluginConnectionEntry(),
|
|
ServiceAccountToken: DefaultPluginConnectionEntry(),
|
|
OnCallAPIURL: DefaultPluginConnectionEntry(),
|
|
OnCallToken: DefaultPluginConnectionEntry(),
|
|
GrafanaURLFromEngine: DefaultPluginConnectionEntry(),
|
|
}
|
|
}
|
|
|
|
type OnCallEngineConnection struct {
|
|
GrafanaURL string `json:"url"`
|
|
Connected bool `json:"connected"`
|
|
StatusCode int `json:"status_code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type OnCallEngineStatus struct {
|
|
ConnectionToGrafana OnCallEngineConnection `json:"connection_to_grafana"`
|
|
License string `json:"license"`
|
|
Version string `json:"version"`
|
|
CurrentlyUndergoingMaintenanceMessage string `json:"currently_undergoing_maintenance_message"`
|
|
APIURL string `json:"api_url"`
|
|
}
|
|
|
|
type OnCallStatus struct {
|
|
PluginConnection OnCallPluginConnection `json:"pluginConnection,omitempty"`
|
|
License string `json:"license"`
|
|
Version string `json:"version"`
|
|
CurrentlyUndergoingMaintenanceMessage string `json:"currently_undergoing_maintenance_message"`
|
|
APIURL string `json:"api_url"`
|
|
}
|
|
|
|
func (s *OnCallStatus) AllOk() bool {
|
|
return s.PluginConnection.Settings.Ok &&
|
|
s.PluginConnection.GrafanaURLFromPlugin.Ok &&
|
|
s.PluginConnection.ServiceAccountToken.Ok &&
|
|
s.PluginConnection.OnCallAPIURL.Ok &&
|
|
s.PluginConnection.OnCallToken.Ok &&
|
|
s.PluginConnection.GrafanaURLFromEngine.Ok
|
|
}
|
|
|
|
func (c *OnCallPluginConnection) ValidateOnCallPluginSettings(settings *OnCallPluginSettings) bool {
|
|
// TODO: Return all instead of first?
|
|
if settings.StackID == 0 {
|
|
c.Settings.SetInvalid("jsonData.stackId is not set")
|
|
} else if settings.OrgID == 0 {
|
|
c.Settings.SetInvalid("jsonData.orgId is not set")
|
|
} else if settings.License == "" {
|
|
c.Settings.SetInvalid("jsonData.license is not set")
|
|
} else if settings.OnCallAPIURL == "" {
|
|
c.Settings.SetInvalid("jsonData.onCallApiUrl is not set")
|
|
} else if settings.GrafanaURL == "" {
|
|
c.Settings.SetInvalid("jsonData.grafanaUrl is not set")
|
|
} else {
|
|
c.Settings.SetValid()
|
|
}
|
|
return c.Settings.Ok
|
|
}
|
|
|
|
func (a *App) ValidateGrafanaConnectionFromPlugin(status *OnCallStatus, settings *OnCallPluginSettings) (bool, error) {
|
|
reqURL, err := url.Parse(settings.GrafanaURL)
|
|
if err != nil {
|
|
status.PluginConnection.GrafanaURLFromPlugin.SetInvalid(fmt.Sprintf("Failed to parse grafana URL %s, %v", settings.GrafanaURL, err))
|
|
return false, nil
|
|
}
|
|
|
|
reqURL.Path += "api/org"
|
|
req, err := http.NewRequest("GET", reqURL.String(), nil)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error creating new request: %+v", err)
|
|
}
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", settings.GrafanaToken))
|
|
|
|
res, err := a.httpClient.Do(req)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error making request: %+v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode == http.StatusOK {
|
|
status.PluginConnection.GrafanaURLFromPlugin.SetValid()
|
|
status.PluginConnection.ServiceAccountToken.SetValid()
|
|
} else if res.StatusCode == http.StatusUnauthorized || res.StatusCode == http.StatusForbidden {
|
|
status.PluginConnection.GrafanaURLFromPlugin.SetValid()
|
|
status.PluginConnection.ServiceAccountToken.SetInvalid(fmt.Sprintf("Grafana %s, status code %d", reqURL.String(), res.StatusCode))
|
|
} else {
|
|
status.PluginConnection.GrafanaURLFromPlugin.SetInvalid(fmt.Sprintf("Grafana %s, status code %d", reqURL.String(), res.StatusCode))
|
|
}
|
|
|
|
return status.PluginConnection.ServiceAccountToken.Ok && status.PluginConnection.GrafanaURLFromPlugin.Ok, nil
|
|
}
|
|
|
|
func (a *App) ValidateOnCallConnection(ctx context.Context, status *OnCallStatus, settings *OnCallPluginSettings) error {
|
|
healthStatus, err := a.CheckOnCallApiHealthStatus(settings)
|
|
if err != nil {
|
|
log.DefaultLogger.Error("Error checking OnCall API health", "error", err)
|
|
status.PluginConnection.OnCallAPIURL = OnCallPluginConnectionEntry{
|
|
Ok: false,
|
|
Error: fmt.Sprintf("Error checking OnCall API health. %v. Status code: %d", err, healthStatus),
|
|
}
|
|
return nil
|
|
}
|
|
|
|
statusURL, err := url.JoinPath(settings.OnCallAPIURL, "api/internal/v1/plugin/v2/status")
|
|
if err != nil {
|
|
return fmt.Errorf("error joining path: %v", err)
|
|
}
|
|
|
|
parsedStatusURL, err := url.Parse(statusURL)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing path: %v", err)
|
|
}
|
|
|
|
statusReq, err := http.NewRequest("GET", parsedStatusURL.String(), nil)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating request: %v", err)
|
|
}
|
|
|
|
statusReq.Header.Set("Content-Type", "application/json")
|
|
err = a.SetupRequestHeadersForOnCallWithUser(ctx, settings, statusReq)
|
|
if err != nil {
|
|
return fmt.Errorf("error setting up request headers: %v ", err)
|
|
}
|
|
|
|
res, err := a.httpClient.Do(statusReq)
|
|
if err != nil {
|
|
return fmt.Errorf("error request to oncall: %v ", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
if res.StatusCode == http.StatusUnauthorized || res.StatusCode == http.StatusForbidden {
|
|
status.PluginConnection.OnCallToken = OnCallPluginConnectionEntry{
|
|
Ok: false,
|
|
Error: fmt.Sprintf("Unauthorized/Forbidden while accessing OnCall engine: %s, status code: %d, check token", statusReq.URL.Path, res.StatusCode),
|
|
}
|
|
} else {
|
|
status.PluginConnection.OnCallAPIURL = OnCallPluginConnectionEntry{
|
|
Ok: false,
|
|
Error: fmt.Sprintf("Unable to connect to OnCall engine: %s, status code: %d", statusReq.URL.Path, res.StatusCode),
|
|
}
|
|
}
|
|
} else {
|
|
status.PluginConnection.OnCallAPIURL.SetValid()
|
|
status.PluginConnection.OnCallToken.SetValid()
|
|
|
|
statusBody, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading response body: %v", err)
|
|
}
|
|
|
|
var engineStatus OnCallEngineStatus
|
|
err = json.Unmarshal(statusBody, &engineStatus)
|
|
if err != nil {
|
|
return fmt.Errorf("error unmarshalling OnCallStatus: %v", err)
|
|
}
|
|
|
|
if engineStatus.ConnectionToGrafana.Connected {
|
|
status.PluginConnection.GrafanaURLFromEngine.SetValid()
|
|
} else {
|
|
status.PluginConnection.GrafanaURLFromPlugin.SetInvalid(fmt.Sprintf("While contacting Grafana: %s from Engine: %s, received status: %d, additional: %s",
|
|
engineStatus.ConnectionToGrafana.GrafanaURL,
|
|
settings.OnCallAPIURL,
|
|
engineStatus.ConnectionToGrafana.StatusCode,
|
|
engineStatus.ConnectionToGrafana.Message))
|
|
}
|
|
|
|
status.APIURL = engineStatus.APIURL
|
|
status.License = engineStatus.License
|
|
status.CurrentlyUndergoingMaintenanceMessage = engineStatus.CurrentlyUndergoingMaintenanceMessage
|
|
status.Version = engineStatus.Version
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) ValidateOnCallStatus(ctx context.Context, settings *OnCallPluginSettings) (*OnCallStatus, error) {
|
|
status := OnCallStatus{
|
|
PluginConnection: DefaultPluginConnection(),
|
|
}
|
|
|
|
if !status.PluginConnection.ValidateOnCallPluginSettings(settings) {
|
|
return &status, nil
|
|
}
|
|
|
|
err := a.ValidateOnCallConnection(ctx, &status, settings)
|
|
if err != nil {
|
|
return &status, err
|
|
}
|
|
|
|
grafanaOK, err := a.ValidateGrafanaConnectionFromPlugin(&status, settings)
|
|
if err != nil {
|
|
return &status, err
|
|
} else if !grafanaOK {
|
|
return &status, nil
|
|
}
|
|
|
|
return &status, nil
|
|
}
|
|
|
|
func (a *App) handleStatus(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
onCallPluginSettings, err := a.OnCallSettingsFromContext(req.Context())
|
|
if err != nil {
|
|
log.DefaultLogger.Error("Error getting settings from context", "error", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
status, err := a.ValidateOnCallStatus(req.Context(), onCallPluginSettings)
|
|
if err != nil {
|
|
log.DefaultLogger.Error("Error validating oncall plugin settings", "error", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(status); err != nil {
|
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
if status.AllOk() {
|
|
a.doSync(req.Context(), false)
|
|
}
|
|
}
|