Skip to content

Commit 5e93b2e

Browse files
authored
refactor: routes as pointer receivers for server (#38)
* chore: remove routes package * refactor: add routes as receiver funcs from server Having these routes hang off from the `State` didn't really make sense. They're for the HTTP server so should be one-level up, they still have access to the `State` because this is part of the `PlaygroundServer` already. Doing this removes the need of passing the `State` into every function, as it can be accessed internally. * fix: kubecfg version hack script * chore: use logger for frontend This was missed during the initial slog pass, tacking it on here for ease * chore: remove log package
1 parent febaa1c commit 5e93b2e

File tree

6 files changed

+71
-72
lines changed

6 files changed

+71
-72
lines changed

cmd/server/cmd.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func main() {
5757
state := state.NewWithLogger(bindAddress, shareAddress, logger)
5858
playground := server.New(state)
5959

60-
slog.Info("Listening on", "address", bindAddress)
60+
logger.Info("Listening on", "address", bindAddress)
6161
go func() {
6262
if err := playground.Serve(); err != http.ErrServerClosed {
6363
slog.Error("Unexpected shutdown", slog.Any("error", err))
@@ -66,14 +66,14 @@ func main() {
6666

6767
<-ctx.Done()
6868
stop()
69-
slog.Info("Shutting down, use Ctrl+C again to force")
69+
logger.Info("Shutting down, use Ctrl+C again to force")
7070

7171
// Inform the server that it had 5 seconds to handle connections and shutdown
7272
timeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
7373
defer cancel()
7474
if err := playground.Server.Shutdown(timeoutCtx); err != nil {
75-
slog.Error("Server forced shutdown", slog.Any("error", err))
75+
logger.Error("Server forced shutdown", slog.Any("error", err))
7676
}
7777

78-
slog.Info("Server shutdown")
78+
logger.Info("Server shutdown")
7979
}

internal/server/routes/backend.go renamed to internal/server/backend_routes.go

+38-39
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package routes
1+
package server
22

33
import (
44
"encoding/hex"
@@ -7,7 +7,6 @@ import (
77
"regexp"
88

99
"github.com/google/go-jsonnet"
10-
"github.com/jdockerty/jsonnet-playground/internal/server/state"
1110
)
1211

1312
var (
@@ -23,33 +22,33 @@ var (
2322
)
2423

2524
// Health indicates whether the server is running.
26-
func Health(state *state.State) http.HandlerFunc {
25+
func (srv *PlaygroundServer) Health() http.HandlerFunc {
2726
return func(w http.ResponseWriter, r *http.Request) {
28-
state.Logger.Debug("health")
27+
srv.State.Logger.Debug("health")
2928
_, _ = w.Write([]byte("OK"))
3029
}
3130
}
3231

3332
// HandleRun receives Jsonnet input via text and evaluates it.
34-
func HandleRun(state *state.State) http.HandlerFunc {
33+
func (srv *PlaygroundServer) HandleRun() http.HandlerFunc {
3534
return func(w http.ResponseWriter, r *http.Request) {
3635
if r.Method != http.MethodPost {
37-
state.Logger.Error("incorrect method to run handler", "method", r.Method)
36+
srv.State.Logger.Error("incorrect method to run handler", "method", r.Method)
3837
http.Error(w, "must be POST", http.StatusBadRequest)
3938
return
4039
}
4140

4241
incomingJsonnet := r.FormValue("jsonnet-input")
43-
state.Logger.Info("run triggered", "jsonnet", incomingJsonnet)
44-
evaluated, err := state.EvaluateSnippet(incomingJsonnet)
42+
srv.State.Logger.Info("run triggered", "jsonnet", incomingJsonnet)
43+
evaluated, err := srv.State.EvaluateSnippet(incomingJsonnet)
4544
if err != nil {
46-
state.Logger.Error("invalid snippet", "jsonnet", incomingJsonnet)
45+
srv.State.Logger.Error("invalid snippet", "jsonnet", incomingJsonnet)
4746
// TODO: display an error for the bad req rather than using a 200
4847
_, _ = w.Write([]byte(err.Error()))
4948
return
5049
}
5150

52-
state.Logger.Info("evaluated", "jsonnet", evaluated)
51+
srv.State.Logger.Info("evaluated", "jsonnet", evaluated)
5352
_, _ = w.Write([]byte(evaluated))
5453
}
5554
}
@@ -59,89 +58,89 @@ func HandleRun(state *state.State) http.HandlerFunc {
5958
// store - this storage mechanism is ephemeral.
6059
//
6160
// At a later date, this will include a persistence layer.
62-
func HandleCreateShare(state *state.State) http.HandlerFunc {
61+
func (srv *PlaygroundServer) HandleCreateShare() http.HandlerFunc {
6362
return func(w http.ResponseWriter, r *http.Request) {
6463
if r.Method != http.MethodPost {
65-
state.Logger.Error("incorrect method to create share handler", "method", r.Method)
64+
srv.State.Logger.Error("incorrect method to create share handler", "method", r.Method)
6665
http.Error(w, "must be POST", http.StatusBadRequest)
6766
return
6867
}
6968

7069
incomingJsonnet := r.FormValue("jsonnet-input")
71-
_, err := state.EvaluateSnippet(incomingJsonnet)
70+
_, err := srv.State.EvaluateSnippet(incomingJsonnet)
7271
if err != nil {
73-
state.Logger.Error("invalid share", "jsonnet", incomingJsonnet)
72+
srv.State.Logger.Error("invalid share", "jsonnet", incomingJsonnet)
7473
// TODO: display an error for the bad req rather than using a 200
7574
_, _ = w.Write([]byte("Share is not available for invalid Jsonnet. Run your snippet to see the result."))
7675
return
7776
}
7877

79-
snippetHash := hex.EncodeToString(state.Hasher.Sum([]byte(incomingJsonnet)))[:15]
80-
if _, ok := state.Store[snippetHash]; !ok {
81-
state.Logger.Info("store creation", "hash", snippetHash)
82-
state.Store[snippetHash] = incomingJsonnet
78+
snippetHash := hex.EncodeToString(srv.State.Hasher.Sum([]byte(incomingJsonnet)))[:15]
79+
if _, ok := srv.State.Store[snippetHash]; !ok {
80+
srv.State.Logger.Info("store creation", "hash", snippetHash)
81+
srv.State.Store[snippetHash] = incomingJsonnet
8382
} else {
84-
state.Logger.Info("store update", "hash", snippetHash)
85-
state.Store[snippetHash] = incomingJsonnet
83+
srv.State.Logger.Info("store update", "hash", snippetHash)
84+
srv.State.Store[snippetHash] = incomingJsonnet
8685
}
87-
shareMsg := fmt.Sprintf("%s/share/%s", state.Config.ShareDomain, snippetHash)
88-
state.Logger.Debug("created share link", "link", shareMsg)
86+
shareMsg := fmt.Sprintf("%s/share/%s", srv.State.Config.ShareDomain, snippetHash)
87+
srv.State.Logger.Debug("created share link", "link", shareMsg)
8988
_, _ = w.Write([]byte("Link: " + shareMsg))
9089
}
9190
}
9291

9392
// HandleGetShare attempts to retrieve a shared snippet hash from the internal
9493
// store. If this does not exist, an error is displayed.
95-
func HandleGetShare(state *state.State) http.HandlerFunc {
94+
func (srv *PlaygroundServer) HandleGetShare() http.HandlerFunc {
9695
return func(w http.ResponseWriter, r *http.Request) {
9796
if r.Method != http.MethodGet {
98-
state.Logger.Error("incorrect method to get share handler", "method", r.Method)
97+
srv.State.Logger.Error("incorrect method to get share handler", "method", r.Method)
9998
http.Error(w, "must be GET", http.StatusBadRequest)
10099
return
101100
}
102101
shareHash := r.PathValue("shareHash")
103-
state.Logger.Info("attempting to load shared snippet", "hash", shareHash)
102+
srv.State.Logger.Info("attempting to load shared snippet", "hash", shareHash)
104103

105-
snippet, ok := state.Store[shareHash]
104+
snippet, ok := srv.State.Store[shareHash]
106105
if !ok {
107-
state.Logger.Warn("no share snippet exists", "hash", shareHash)
106+
srv.State.Logger.Warn("no share snippet exists", "hash", shareHash)
108107
errMsg := fmt.Errorf("No share snippet exists for %s, it might have expired.\n", shareHash)
109108
_, _ = w.Write([]byte(errMsg.Error()))
110109
return
111110
}
112-
state.Logger.Info("loaded shared snippet", "hash", shareHash)
111+
srv.State.Logger.Info("loaded shared snippet", "hash", shareHash)
113112
_, _ = w.Write([]byte(snippet))
114113
}
115114
}
116115

117116
// Format the input Jsonnet according to the standard jsonnetfmt rules.
118-
func HandleFormat(state *state.State) http.HandlerFunc {
117+
func (srv *PlaygroundServer) HandleFormat() http.HandlerFunc {
119118
return func(w http.ResponseWriter, r *http.Request) {
120119
if r.Method != http.MethodPost {
121-
state.Logger.Error("incorrect method to format handler", "method", r.Method)
120+
srv.State.Logger.Error("incorrect method to format handler", "method", r.Method)
122121
http.Error(w, "must be POST", http.StatusBadRequest)
123122
return
124123
}
125124

126125
incomingJsonnet := r.FormValue("jsonnet-input")
127-
state.Logger.Info("attempting to format", "jsonnet", incomingJsonnet)
128-
formattedJsonnet, err := state.FormatSnippet(incomingJsonnet)
126+
srv.State.Logger.Info("attempting to format", "jsonnet", incomingJsonnet)
127+
formattedJsonnet, err := srv.State.FormatSnippet(incomingJsonnet)
129128
if err != nil {
130-
state.Logger.Warn("cannot format invalid jsonnet")
129+
srv.State.Logger.Warn("cannot format invalid jsonnet")
131130
http.Error(w, "Format is not available for invalid Jsonnet. Run your snippet to see the result.", http.StatusBadRequest)
132131
return
133132
}
134-
state.Logger.Info("formatted", "jsonnet", formattedJsonnet)
133+
srv.State.Logger.Info("formatted", "jsonnet", formattedJsonnet)
135134
_, _ = w.Write([]byte(formattedJsonnet))
136135
}
137136
}
138137

139138
// Retrieve the current version of Jsonnet/Kubecfg in use for the running application.
140139
// This is purely informational for the frontend.
141-
func HandleVersions(state *state.State) http.HandlerFunc {
140+
func (srv *PlaygroundServer) HandleVersions() http.HandlerFunc {
142141
return func(w http.ResponseWriter, r *http.Request) {
143142
if r.Method != http.MethodGet {
144-
state.Logger.Error("incorrect method to versions handler", "method", r.Method)
143+
srv.State.Logger.Error("incorrect method to versions handler", "method", r.Method)
145144
http.Error(w, "must be POST", http.StatusBadRequest)
146145
return
147146
}
@@ -152,17 +151,17 @@ func HandleVersions(state *state.State) http.HandlerFunc {
152151
// Middleware to stop Jsonnet snippets which contain file:///, typically paired
153152
// with an import, being used and becoming shareable. These are rejected before
154153
// running through the Jsonnet VM and a generic error is displayed.
155-
func DisableFileImports(state *state.State, next http.Handler) http.HandlerFunc {
154+
func DisableFileImports(srv *PlaygroundServer, next http.Handler) http.HandlerFunc {
156155
return func(w http.ResponseWriter, r *http.Request) {
157156
err := r.ParseForm()
158157
if err != nil {
159-
state.Logger.Error("unable to parse form")
158+
srv.State.Logger.Error("unable to parse form")
160159
http.Error(w, "unable to parse form", http.StatusBadRequest)
161160
return
162161
}
163162
incomingJsonnet := r.FormValue("jsonnet-input")
164163
if ok := disallowFileImports.Match([]byte(incomingJsonnet)); ok {
165-
state.Logger.Warn("attempt to import file", "jsonnet", incomingJsonnet)
164+
srv.State.Logger.Warn("attempt to import file", "jsonnet", incomingJsonnet)
166165
_, _ = w.Write([]byte("File imports are disabled."))
167166
return
168167
}

internal/server/routes/backend_test.go renamed to internal/server/backend_routes_test.go

+16-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package routes_test
1+
package server_test
22

33
import (
44
"crypto/sha512"
@@ -11,7 +11,7 @@ import (
1111
"testing"
1212

1313
"github.com/google/go-jsonnet"
14-
"github.com/jdockerty/jsonnet-playground/internal/server/routes"
14+
"github.com/jdockerty/jsonnet-playground/internal/server"
1515
"github.com/jdockerty/jsonnet-playground/internal/server/state"
1616
"github.com/kubecfg/kubecfg/pkg/kubecfg"
1717
"github.com/stretchr/testify/assert"
@@ -33,9 +33,9 @@ func TestHandleRun(t *testing.T) {
3333
expected string
3434
shouldFail bool
3535
}{
36-
{name: "hello-world", input: "{hello: 'world'}", expected: "../../../testdata/hello-world.json", shouldFail: false},
37-
{name: "blank", input: "{}", expected: "../../../testdata/blank.json", shouldFail: false},
38-
{name: "kubecfg", input: "local kubecfg = import 'internal:///kubecfg.libsonnet';\n{myVeryNestedObj:: { foo: { bar: { baz: { qux: 'some-val' }}}}, hasValue: kubecfg.objectHasPathAll($.myVeryNestedObj, 'foo.bar.baz.qux')}", expected: "../../../testdata/kubecfg.json", shouldFail: false},
36+
{name: "hello-world", input: "{hello: 'world'}", expected: "../../testdata/hello-world.json", shouldFail: false},
37+
{name: "blank", input: "{}", expected: "../../testdata/blank.json", shouldFail: false},
38+
{name: "kubecfg", input: "local kubecfg = import 'internal:///kubecfg.libsonnet';\n{myVeryNestedObj:: { foo: { bar: { baz: { qux: 'some-val' }}}}, hasValue: kubecfg.objectHasPathAll($.myVeryNestedObj, 'foo.bar.baz.qux')}", expected: "../../testdata/kubecfg.json", shouldFail: false},
3939
{name: "invalid-jsonnet", input: "{", expected: "Invalid Jsonnet", shouldFail: true},
4040
{name: "invalid-jsonnet-2", input: "{hello:}", expected: "Invalid Jsonnet", shouldFail: true},
4141
{name: "file-import-jsonnet", input: "local f = import 'file:///proc/self/environ'; error 'test' + f", expected: "File imports are disabled", shouldFail: true},
@@ -49,7 +49,8 @@ func TestHandleRun(t *testing.T) {
4949
req := httptest.NewRequest("POST", "/api/run", nil)
5050
req.PostForm = data
5151

52-
handler := routes.HandleRun(state.New("127.0.0.1", "https://example.com"))
52+
srv := server.New(state.New("127.0.0.1", "https://example.com"))
53+
handler := srv.HandleRun()
5354
handler.ServeHTTP(rec, req)
5455

5556
if tc.shouldFail {
@@ -87,7 +88,8 @@ func TestHandleCreateShare(t *testing.T) {
8788
req := httptest.NewRequest("POST", "/api/share", nil)
8889
req.PostForm = data
8990

90-
handler := routes.HandleCreateShare(state.New("127.0.0.1", "https://example.com"))
91+
srv := server.New(state.New("127.0.0.1", "https://example.com"))
92+
handler := srv.HandleCreateShare()
9193
handler.ServeHTTP(rec, req)
9294

9395
if tc.shouldFail {
@@ -107,7 +109,8 @@ func TestHandleGetShare(t *testing.T) {
107109
snippetHash := hex.EncodeToString(sha512.New().Sum([]byte(snippet)))[:15]
108110

109111
// Get non-existent snippet
110-
handler := routes.HandleGetShare(s)
112+
srv := server.New(s)
113+
handler := srv.HandleGetShare()
111114
path := fmt.Sprintf("/api/share/%s", snippetHash)
112115
rec := httptest.NewRecorder()
113116
req := httptest.NewRequest("GET", path, nil)
@@ -117,10 +120,10 @@ func TestHandleGetShare(t *testing.T) {
117120

118121
// Add snippet to store
119122
evaluated, _ := vm.EvaluateAnonymousSnippet("", snippet)
120-
s.Store[snippetHash] = evaluated
123+
srv.State.Store[snippetHash] = evaluated
121124

122125
// Get snippet which has been added
123-
handler = routes.HandleGetShare(s)
126+
handler = srv.HandleGetShare()
124127
rec = httptest.NewRecorder()
125128
req = httptest.NewRequest("GET", fmt.Sprintf("/api/share/%s", snippetHash), nil)
126129
req.SetPathValue("shareHash", snippetHash)
@@ -134,10 +137,11 @@ func TestHandleVersions(t *testing.T) {
134137
assert := assert.New(t)
135138
s := state.New("127.0.0.1", "https://example.com")
136139

137-
handler := routes.HandleVersions(s)
140+
srv := server.New(s)
141+
handler := srv.HandleVersions()
138142
rec := httptest.NewRecorder()
139143
req := httptest.NewRequest("GET", "/api/versions", nil)
140144
handler.ServeHTTP(rec, req)
141145

142-
assert.Contains(rec.Body.String(), string(routes.VersionResponse))
146+
assert.Contains(rec.Body.String(), string(server.VersionResponse))
143147
}

internal/server/routes/frontend.go renamed to internal/server/frontend_routes.go

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
package routes
1+
package server
22

33
import (
44
"context"
5-
"log"
65
"net/http"
76

87
"github.com/jdockerty/jsonnet-playground/internal/components"
9-
"github.com/jdockerty/jsonnet-playground/internal/server/state"
108
)
119

1210
// HandleAssets wires up the static asset handling for the server.
@@ -15,17 +13,16 @@ func HandleAssets(pattern string, fsHandler http.Handler) http.Handler {
1513
}
1614

1715
// HandleShare is the rendering of the shared snippet view.
18-
func HandleShare(state *state.State) http.HandlerFunc {
16+
func (srv *PlaygroundServer) HandleShare() http.HandlerFunc {
1917
return func(w http.ResponseWriter, r *http.Request) {
2018
shareHash := r.PathValue("shareHash")
21-
log.Printf("Incoming share view for %+v\n", shareHash)
19+
srv.State.Logger.Info("share view loading", "shareHash", shareHash)
2220

2321
if shareHash == "" {
24-
log.Println("Browsed to share with no hash, rendering root page")
22+
srv.State.Logger.Debug("browse to share with no hash, rendering root page")
2523
_ = components.RootPage("").Render(context.Background(), w)
2624
return
2725
}
28-
log.Println("Rendering share page")
2926
sharePage := components.RootPage(shareHash)
3027
_ = sharePage.Render(context.Background(), w)
3128
}

internal/server/server.go

+8-9
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77

88
"github.com/a-h/templ"
99
"github.com/jdockerty/jsonnet-playground/internal/components"
10-
"github.com/jdockerty/jsonnet-playground/internal/server/routes"
1110
"github.com/jdockerty/jsonnet-playground/internal/server/state"
1211
)
1312

@@ -37,17 +36,17 @@ func (srv *PlaygroundServer) Routes() error {
3736
// Frontend routes
3837
rootPage := components.RootPage("")
3938
fs := http.FileServer(http.Dir(path))
40-
http.Handle("/assets/", routes.HandleAssets("/assets/", fs))
39+
http.Handle("/assets/", HandleAssets("/assets/", fs))
4140
http.Handle("/", templ.Handler(rootPage))
42-
http.HandleFunc("/share/{shareHash}", routes.HandleShare(srv.State))
41+
http.HandleFunc("/share/{shareHash}", srv.HandleShare())
4342

4443
// Backend/API routes
45-
http.HandleFunc("/api/health", routes.Health(srv.State))
46-
http.HandleFunc("/api/run", routes.DisableFileImports(srv.State, routes.HandleRun(srv.State)))
47-
http.HandleFunc("/api/format", routes.DisableFileImports(srv.State, routes.HandleFormat(srv.State)))
48-
http.HandleFunc("/api/share", routes.DisableFileImports(srv.State, routes.HandleCreateShare(srv.State)))
49-
http.HandleFunc("/api/share/{shareHash}", routes.DisableFileImports(srv.State, routes.HandleGetShare(srv.State)))
50-
http.HandleFunc("/api/versions", routes.HandleVersions(srv.State))
44+
http.HandleFunc("/api/health", srv.Health())
45+
http.HandleFunc("/api/run", DisableFileImports(srv, srv.HandleRun()))
46+
http.HandleFunc("/api/format", DisableFileImports(srv, srv.HandleFormat()))
47+
http.HandleFunc("/api/share", DisableFileImports(srv, srv.HandleCreateShare()))
48+
http.HandleFunc("/api/share/{shareHash}", DisableFileImports(srv, srv.HandleGetShare()))
49+
http.HandleFunc("/api/versions", srv.HandleVersions())
5150
return nil
5251
}
5352

scripts/kubecfg_hack.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/sh
22

33
ACTUAL_KUBECFG_VERSION=$(go mod edit -json | jq -r '.Require[] | select(.Path | contains("kubecfg/kubecfg")).Version' | tr -d " ")
4-
WRITTEN_KUBECFG_VERSION=$(grep "KubecfgVersion " internal/server/routes/backend.go | cut -d "=" -f 2 | tr -d '" ')
4+
WRITTEN_KUBECFG_VERSION=$(grep "KubecfgVersion " internal/server/backend_routes.go | cut -d "=" -f 2 | tr -d '" ')
55

66
if [ "${ACTUAL_KUBECFG_VERSION}" != "${WRITTEN_KUBECFG_VERSION}" ]; then
77
echo "Written kubecfg version (${WRITTEN_KUBECFG_VERSION}) does not match go.mod file version (${ACTUAL_KUBECFG_VERSION})"

0 commit comments

Comments
 (0)