mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			133 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			133 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			Go
		
	
	
	
| package api
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"sort"
 | |
| 	"strconv"
 | |
| 
 | |
| 	"github.com/grafana/grafana/pkg/api/response"
 | |
| 	"github.com/grafana/grafana/pkg/infra/log"
 | |
| 	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
 | |
| 	"github.com/grafana/grafana/pkg/services/featuremgmt"
 | |
| 	"github.com/grafana/grafana/pkg/setting"
 | |
| 	"github.com/grafana/grafana/pkg/web"
 | |
| )
 | |
| 
 | |
| func (hs *HTTPServer) GetFeatureToggles(ctx *contextmodel.ReqContext) response.Response {
 | |
| 	// object being returned
 | |
| 	dtos := make([]featuremgmt.FeatureToggleDTO, 0)
 | |
| 
 | |
| 	// loop through features an add features that should be visible to dtos
 | |
| 	for _, ft := range hs.featureManager.GetFlags() {
 | |
| 		flag := ft.Name
 | |
| 		if hs.featureManager.IsHiddenFromAdminPage(flag, false) {
 | |
| 			continue
 | |
| 		}
 | |
| 		dto := featuremgmt.FeatureToggleDTO{
 | |
| 			Name:        flag,
 | |
| 			Description: ft.Description,
 | |
| 			Enabled:     hs.featureManager.IsEnabled(ctx.Req.Context(), flag),
 | |
| 			ReadOnly:    !hs.featureManager.IsEditableFromAdminPage(flag),
 | |
| 		}
 | |
| 
 | |
| 		dtos = append(dtos, dto)
 | |
| 		sort.Slice(dtos, func(i, j int) bool {
 | |
| 			return dtos[i].Name < dtos[j].Name
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	return response.JSON(http.StatusOK, dtos)
 | |
| }
 | |
| 
 | |
| func (hs *HTTPServer) UpdateFeatureToggle(ctx *contextmodel.ReqContext) response.Response {
 | |
| 	featureMgmtCfg := hs.featureManager.Settings
 | |
| 	if !featureMgmtCfg.AllowEditing {
 | |
| 		return response.Error(http.StatusForbidden, "feature toggles are read-only", fmt.Errorf("feature toggles are configured to be read-only"))
 | |
| 	}
 | |
| 
 | |
| 	if featureMgmtCfg.UpdateWebhook == "" {
 | |
| 		return response.Error(http.StatusInternalServerError, "feature toggles service is misconfigured", fmt.Errorf("[feature_management]update_webhook is not set"))
 | |
| 	}
 | |
| 
 | |
| 	cmd := featuremgmt.UpdateFeatureTogglesCommand{}
 | |
| 	if err := web.Bind(ctx.Req, &cmd); err != nil {
 | |
| 		return response.Error(http.StatusBadRequest, "bad request data", err)
 | |
| 	}
 | |
| 
 | |
| 	payload := UpdatePayload{
 | |
| 		FeatureToggles: make(map[string]string, len(cmd.FeatureToggles)),
 | |
| 		User:           ctx.SignedInUser.Email,
 | |
| 	}
 | |
| 
 | |
| 	for _, t := range cmd.FeatureToggles {
 | |
| 		// make sure flag exists, and only continue if flag is writeable
 | |
| 		if hs.featureManager.IsEditableFromAdminPage(t.Name) {
 | |
| 			hs.log.Info("UpdateFeatureToggle: updating toggle", "toggle_name", t.Name, "enabled", t.Enabled, "username", ctx.SignedInUser.Login)
 | |
| 			payload.FeatureToggles[t.Name] = strconv.FormatBool(t.Enabled)
 | |
| 		} else {
 | |
| 			hs.log.Warn("UpdateFeatureToggle: invalid toggle passed in", "toggle_name", t.Name)
 | |
| 			return response.Error(http.StatusBadRequest, "invalid toggle passed in", fmt.Errorf("invalid toggle passed in: %s", t.Name))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	err := sendWebhookUpdate(featureMgmtCfg, payload, hs.log)
 | |
| 	if err != nil {
 | |
| 		hs.log.Error("UpdateFeatureToggle: Failed to perform webhook request", "error", err)
 | |
| 		return response.Respond(http.StatusBadRequest, "Failed to perform webhook request")
 | |
| 	}
 | |
| 
 | |
| 	hs.featureManager.SetRestartRequired()
 | |
| 
 | |
| 	return response.Respond(http.StatusOK, "feature toggles updated successfully")
 | |
| }
 | |
| 
 | |
| func (hs *HTTPServer) GetFeatureMgmtState(ctx *contextmodel.ReqContext) response.Response {
 | |
| 	fmState := hs.featureManager.GetState()
 | |
| 	return response.Respond(http.StatusOK, fmState)
 | |
| }
 | |
| 
 | |
| type UpdatePayload struct {
 | |
| 	FeatureToggles map[string]string `json:"feature_toggles"`
 | |
| 	User           string            `json:"user"`
 | |
| }
 | |
| 
 | |
| func sendWebhookUpdate(cfg setting.FeatureMgmtSettings, payload UpdatePayload, logger log.Logger) error {
 | |
| 	data, err := json.Marshal(payload)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	req, err := http.NewRequest(http.MethodPost, cfg.UpdateWebhook, bytes.NewBuffer(data))
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	req.Header.Set("Authorization", "Bearer "+cfg.UpdateWebhookToken)
 | |
| 
 | |
| 	client := &http.Client{}
 | |
| 	resp, err := client.Do(req)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	defer func() {
 | |
| 		if err := resp.Body.Close(); err != nil {
 | |
| 			logger.Warn("Failed to close response body", "err", err)
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	if resp.StatusCode >= http.StatusBadRequest {
 | |
| 		if body, err := io.ReadAll(resp.Body); err != nil {
 | |
| 			return fmt.Errorf("SendWebhookUpdate failed with status=%d, error: %s", resp.StatusCode, string(body))
 | |
| 		} else {
 | |
| 			return fmt.Errorf("SendWebhookUpdate failed with status=%d, error: %w", resp.StatusCode, err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 |