Skip to content

Commit

Permalink
Updated the UI with routing and support for consuming the backend.
Browse files Browse the repository at this point in the history
  • Loading branch information
kristofferahl committed Jan 21, 2024
1 parent 6321a06 commit d111a26
Show file tree
Hide file tree
Showing 11 changed files with 1,024 additions and 246 deletions.
237 changes: 224 additions & 13 deletions internal/command/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ import (
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"

"github.com/dotnetmentor/racoon/internal/api"
"github.com/dotnetmentor/racoon/internal/backend"
"github.com/dotnetmentor/racoon/internal/config"
api "github.com/dotnetmentor/racoon/internal/httpapi"
"github.com/dotnetmentor/racoon/internal/httpapi"
"github.com/dotnetmentor/racoon/internal/io"
"github.com/dotnetmentor/racoon/internal/utils"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
Expand All @@ -33,7 +38,7 @@ func UI(metadata config.AppMetadata, fs embed.FS) *cli.Command {

ctx.Log.Infof("starting UI server")

s := api.NewServer(api.Config{
s := httpapi.NewServer(httpapi.Config{
Log: ctx.Log.WithFields(logrus.Fields{"component": "server"}),
BasicAuth: configureBasicAuth(),
})
Expand All @@ -42,10 +47,12 @@ func UI(metadata config.AppMetadata, fs embed.FS) *cli.Command {
ctx,
fs,
))
s.Router.Post("/api/compare", compareHandler(
s.Router.Post("/api/command/compare", compareCommandHandler(
ctx,
ctx.Manifest.Filepath(),
))
s.Router.Post("/api/command/config/decrypt", decryptConfigCommandHandler(ctx))
s.Router.Get("/api/query/config", configQueryHandler(ctx))

err = s.RunAndBlock()
if err != nil {
Expand All @@ -56,13 +63,13 @@ func UI(metadata config.AppMetadata, fs embed.FS) *cli.Command {
}
}

func configureBasicAuth() *api.BasicAuth {
var auth *api.BasicAuth
func configureBasicAuth() *httpapi.BasicAuth {
var auth *httpapi.BasicAuth
baUsername := os.Getenv("CENTRY_SERVE_USERNAME")
baPassword := os.Getenv("CENTRY_SERVE_PASSWORD")

if baUsername != "" && baPassword != "" {
auth = &api.BasicAuth{
auth = &httpapi.BasicAuth{
Username: baUsername,
Password: baPassword,
}
Expand Down Expand Up @@ -92,7 +99,211 @@ func indexHandler(ctx config.AppContext, embedded embed.FS) http.HandlerFunc {
})
}

func compareHandler(ctx config.AppContext, manifestPath string) func(w http.ResponseWriter, r *http.Request) {
func configQueryHandler(ctx config.AppContext) func(w http.ResponseWriter, r *http.Request) {
backend, err := backend.New(ctx.Context, ctx.Manifest.Backend)
if err != nil {
ctx.Log.Fatal(err)
}

return func(w http.ResponseWriter, r *http.Request) {
ctx.Log.Infof("querying configs")
statusCode := http.StatusOK
response := httpapi.ConfigQueryResponse{
Items: make([]httpapi.ConfigQueryItem, 0),
}

if files, err := backend.Store().List(); err != nil {
response.Error = fmt.Sprintf("error listing configs: %v", err)
statusCode = http.StatusInternalServerError
} else {
download := false
filters := r.URL.Query()["f"]

if r.URL.Query().Get("download") == "true" {
download = true
}

configs := make([]httpapi.ConfigQueryItem, 0)
for _, file := range files {
configs = append(configs, httpapi.ConfigQueryItem{
Path: file,
Encrypted: true,
})
}
if download {
n := len(configs)
configs = filterConfigs(configs, filters)
ctx.Log.Infof("applying filters, yielded %d config(s), was %d", len(configs), n)
}

sort.Slice(configs, func(i, j int) bool {
return configs[i].Matches > configs[j].Matches
})

response.Total = len(configs)
for _, f := range filters {
response.Filters = append(response.Filters, strings.ReplaceAll(f, "/", "="))
}

if download {
startAt := 0
if r.URL.Query().Get("startAt") != "" {
pv, err := strconv.Atoi(r.URL.Query().Get("startAt"))
if err == nil {
startAt = pv
}
}
configs = utils.SliceSkip(configs, startAt)
ctx.Log.Infof("skip %d, yielded %d config(s)", startAt, len(configs))

beforeTake := len(configs)
take := 6 // TODO: Make this configurable
configs = utils.SliceTake(configs, take)
response.More = beforeTake > len(configs)
ctx.Log.Infof("take %d, yielded %d config(s)", take, len(configs))

for i, c := range configs {
ctx.Log.Debugf("downloading config %s", c.Path)
encrypted, err := backend.Store().Download(c.Path)
if err != nil {
response.Error = fmt.Sprintf("error downloading config %s: %v", c.Path, err)
statusCode = http.StatusInternalServerError
break
}
configs[i].Data = encrypted
ctx.Log.Infof("downloaded config %s", c.Path)
}
}

response.Items = configs
}

if response.Error != "" {
ctx.Log.Error(response.Error)
}

if err := jsonRespone(w, statusCode, response); err != nil {
ctx.Log.Errorf("error writing response: %v", err)
}
}
}

func filterConfigs(configs []httpapi.ConfigQueryItem, filters []string) (filtered []httpapi.ConfigQueryItem) {
for _, c := range configs {
matchForKey := make(map[string]bool)
filtersByGroup := make(map[string][]string)
for _, f := range filters {
kv := strings.Split(f, "/")
key := kv[0]
value := kv[1]
if key == "name" {
key = value
value = "racoon.config"
}
filtersByGroup[key] = append(filtersByGroup[key], value)
}

for key, values := range filtersByGroup {
match := false
for _, v := range values {
if strings.Contains(c.Path, fmt.Sprintf("%s/%s", key, v)) {
match = true
c.Matches++
}
}
matchForKey[key] = match
}

matchesAllGroups := true
for _, ok := range matchForKey {
if !ok {
matchesAllGroups = false
break
}
}

if matchesAllGroups {
filtered = append(filtered, c)
}
}
return
}

func decryptConfigCommandHandler(ctx config.AppContext) func(w http.ResponseWriter, r *http.Request) {
backend, err := backend.New(ctx.Context, ctx.Manifest.Backend)
if err != nil {
ctx.Log.Fatal(err)
}

return func(w http.ResponseWriter, r *http.Request) {
ctx.Log.Infof("decrypting config")
statusCode := http.StatusOK
response := httpapi.ConfigDecryptCommandResponse{}

var body httpapi.ConfigDecryptCommand

decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&body)
if err != nil {
ctx.Log.Errorf("error decoding request body: %v", err)
statusCode = http.StatusBadRequest
} else {
ctx.Log.Debugf("downloading config %s", body.Path)
encrypted, err := backend.Store().Download(body.Path)
if err != nil {
response.Error = fmt.Sprintf("error downloading config %s: %v", body.Path, err)
statusCode = http.StatusInternalServerError
} else {
ctx.Log.Infof("downloaded config %s", body.Path)

ctx.Log.Debugf("unmarshalling config %s", body.Path)
encconf := api.EncryptedConfig{}
if err := json.Unmarshal(encrypted, &encconf); err != nil {
response.Error = fmt.Sprintf("error unmarshalling config %s: %v", body.Path, err)
statusCode = http.StatusInternalServerError
}

for i, p := range encconf.Properties {
if p.Sensitive && p.Value != nil && len(*p.Value) > 0 {
ctx.Log.Infof("decrypting property %s", p.Name)
ev := *p.Value
dv, err := backend.Encryption().Decrypt([]byte(ev))
if err != nil {
response.Error = fmt.Sprintf("error decrypting property %s: %v", p.Name, err)
statusCode = http.StatusInternalServerError
ctx.Log.Error(response.Error)
break
}
dsv := string(dv)
encconf.Properties[i].Value = &dsv
}
}

if response.Error == "" {
ctx.Log.Debugf("marshalling config %s", body.Path)
decrypted, err := json.Marshal(encconf)
if err != nil {
response.Error = fmt.Sprintf("error marshalling config %s: %v", body.Path, err)
statusCode = http.StatusInternalServerError
ctx.Log.Error(response.Error)
} else {
response.Data = decrypted
}
}
}
}

if response.Error != "" {
ctx.Log.Error(response.Error)
}

if err := jsonRespone(w, statusCode, response); err != nil {
ctx.Log.Errorf("error writing response: %v", err)
}
}
}

func compareCommandHandler(ctx config.AppContext, manifestPath string) func(w http.ResponseWriter, r *http.Request) {
runCommand := func(command, args string) (output string, logOutput string, err error) {
io, stdout, stderr := io.Buffered(os.Stdin)
execArgs := strings.Fields(fmt.Sprintf(
Expand Down Expand Up @@ -122,9 +333,9 @@ func compareHandler(ctx config.AppContext, manifestPath string) func(w http.Resp
return func(w http.ResponseWriter, r *http.Request) {
ctx.Log.Infof("comparing command output")
statusCode := http.StatusOK
response := api.CompareResponse{}
response := httpapi.CompareCommandResponse{}

var body api.CompareRequest
var body httpapi.CompareCommand

decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&body)
Expand All @@ -141,7 +352,7 @@ func compareHandler(ctx config.AppContext, manifestPath string) func(w http.Resp
if err != nil {
ctx.Log.Errorf("error executing compare left: %v", err)
}
response.Left = &api.ExecutionResult{
response.Left = &httpapi.ExecutionResult{
Logs: stderr,
Result: stdout,
}
Expand All @@ -153,13 +364,13 @@ func compareHandler(ctx config.AppContext, manifestPath string) func(w http.Resp
if err != nil {
ctx.Log.Errorf("error executing compare right: %v", err)
}
response.Right = &api.ExecutionResult{
response.Right = &httpapi.ExecutionResult{
Logs: stderr,
Result: stdout,
}
}

if err := JsonRespone(w, statusCode, response); err != nil {
if err := jsonRespone(w, statusCode, response); err != nil {
ctx.Log.Errorf("error writing response: %v", err)
}
}
Expand Down Expand Up @@ -200,7 +411,7 @@ func createRedirector(ctx config.AppContext, fsys fs.FS) http.HandlerFunc {
}
}

func JsonRespone(w http.ResponseWriter, statusCode int, response interface{}) error {
func jsonRespone(w http.ResponseWriter, statusCode int, response interface{}) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)

Expand Down
34 changes: 30 additions & 4 deletions internal/httpapi/contract.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package httpapi

// CompareRequest defines an HTTP response object
type CompareRequest struct {
import "encoding/json"

// CompareCommand defines an HTTP response object
type CompareCommand struct {
Command string `json:"command"`
Left string `json:"left"`
Right string `json:"right"`
}

// CompareResponse defines an HTTP response object
type CompareResponse struct {
// CompareCommandResponse defines an HTTP response object
type CompareCommandResponse struct {
Left *ExecutionResult `json:"left"`
Right *ExecutionResult `json:"right"`
}
Expand All @@ -17,3 +19,27 @@ type ExecutionResult struct {
Logs string `json:"logs"`
Result string `json:"result"`
}

type ConfigQueryResponse struct {
Error string `json:"error,omitempty"`
Items []ConfigQueryItem `json:"items"`
Total int `json:"total"`
More bool `json:"more"`
Filters []string `json:"filters"`
}

type ConfigQueryItem struct {
Matches int `json:"matches"`
Path string `json:"path"`
Encrypted bool `json:"encrypted"`
Data json.RawMessage `json:"data"`
}

type ConfigDecryptCommand struct {
Path string `json:"path"`
}

type ConfigDecryptCommandResponse struct {
Error string `json:"error,omitempty"`
Data json.RawMessage `json:"data"`
}
Loading

0 comments on commit d111a26

Please sign in to comment.