blob: da2139eca2b2b53c2a2bf7ba3c02c46db671e054 [file] [log] [blame]
package api
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"go.skia.org/infra/go/alogin"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/roles"
"go.skia.org/infra/go/sklog"
sheriffconfig "go.skia.org/infra/perf/go/sheriffconfig/service"
)
// sheriffConfigApi provides a struct for handling Sheriff Config requests.
type sheriffConfigApi struct {
loginProvider alogin.Login
}
// NewSheriffConfigApi returns a new instance of the sheriffConfigApi struct.
func NewSheriffConfigApi(loginProvider alogin.Login) sheriffConfigApi {
return sheriffConfigApi{
loginProvider: loginProvider,
}
}
// RegisterHandlers registers the api handlers for their respective routes.
func (api sheriffConfigApi) RegisterHandlers(router *chi.Mux) {
router.Get("/_/configs/metadata", api.getMetadataHandler)
router.Post("/_/configs/validate", api.validateConfigHandler)
}
type Pattern struct {
ConfigSet string `json:"config_set"`
Path string `json:"path"`
}
type Validation struct {
Patterns []Pattern `json:"patterns"`
Url string `json:"url"`
}
type GetMetadataResponse struct {
Version string `json:"version"`
Validation Validation `json:"validation"`
}
// getMetadataHandler is a GET Method that'll be used by LUCI Config to determine which
// configuration files in infra_internal are owned by us. It'll provide a Url for validation
// for any changes to these files.
func (api sheriffConfigApi) getMetadataHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if !api.loginProvider.HasRole(r, roles.LuciConfig) {
sklog.Infof("User is not authorized: %s", api.loginProvider.LoggedInAs(r).String())
httputils.ReportError(w, nil, "Permission denied", http.StatusForbidden)
return
}
resp := GetMetadataResponse{
Version: "1.0",
Validation: Validation{
Patterns: []Pattern{
{
ConfigSet: "regex:projects/.+",
Path: "regex:skia-sheriff-configs.cfg",
},
},
Url: "https://perf.luci.app/_/configs/validate",
},
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
sklog.Errorf("Failed to write or encode output: %s", err)
}
}
type ValidateConfigRequest struct {
Path string `json:"path"`
Content string `json:"content"`
}
type Message struct {
Path string `json:"path"`
Severity string `json:"severity"`
Text string `json:"text"`
}
type ValidateConfigResponse struct {
Messages []Message `json:"messages"`
}
// validateConfigHandler will receive incoming validation requests from LUCI Config.
// It'll return an empty object and status 200 if the validation is succesful. If the validation
// fails, it'll return status 200 and a formatted error message to LUCI Config.
//
// Example ValidateConfigRequest:
//
// {
// "content": "c3Vic2NyaXB0aW9ucyB7CgluYW1lOiAiYSIKfQ=="
// }
func (api sheriffConfigApi) validateConfigHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if !api.loginProvider.HasRole(r, roles.LuciConfig) {
sklog.Infof("User is not authorized: %s", api.loginProvider.LoggedInAs(r).String())
httputils.ReportError(w, nil, "Permission denied", http.StatusForbidden)
return
}
vcr := ValidateConfigRequest{}
if err := json.NewDecoder(r.Body).Decode(&vcr); err != nil {
httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError)
return
}
err := sheriffconfig.ValidateContent(vcr.Content)
if err != nil {
ret := ValidateConfigResponse{
Messages: []Message{
{
Path: vcr.Path,
Severity: "ERROR",
Text: err.Error(),
},
},
}
if err := json.NewEncoder(w).Encode(ret); err != nil {
sklog.Errorf("Failed to write or encode output: %s", err)
}
}
}