Skip to content

Commit

Permalink
Merge 39019f9 into b81e9ae
Browse files Browse the repository at this point in the history
  • Loading branch information
rbeuque74 committed Jan 11, 2020
2 parents b81e9ae + 39019f9 commit 768e83f
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 10 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ COPY ./ui /home/node/ui

# dashboard
WORKDIR /home/node/ui/dashboard
RUN BASEHREF=/ui/dashboard/ PREFIX_API_BASE_URL=/ make build-prod
RUN BASEHREF=___UTASK_DASHBOARD_BASEHREF___ PREFIX_API_BASE_URL=___UTASK_DASHBOARD_PREFIXAPIBASEURL___ make build-prod

# editor
WORKDIR /home/node/ui/editor
RUN BASEHREF=/ui/editor/ make build-prod
RUN BASEHREF=___UTASK_EDITOR_BASEHREF___ make build-prod

FROM golang:1.13-buster

Expand Down
48 changes: 48 additions & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import (
"net/url"
"os"
"strconv"
"strings"
"testing"
"time"

"github.com/gin-gonic/gin"
"github.com/juju/errors"
"github.com/loopfz/gadgeto/iffy"
"github.com/loopfz/gadgeto/zesty"
Expand Down Expand Up @@ -65,6 +67,9 @@ func TestMain(m *testing.M) {

srv := api.NewServer()
srv.WithAuth(dumbIdentityProvider)
srv.SetDashboardPathPrefix("")
srv.SetDashboardAPIPathPrefix("")
srv.SetEditorPathPrefix("")

go srv.ListenAndServe()
srvx := &http.Server{Addr: fmt.Sprintf(":%d", utask.FPort)}
Expand Down Expand Up @@ -530,6 +535,49 @@ func blockedHidden(name string, blocked, hidden bool) tasktemplate.TaskTemplate
}
}

func Test_staticMiddleware(t *testing.T) {
ginEngine := gin.Default()
ginEngine.
Group("/", api.StaticFilePatternReplaceMiddleware("static.go", "___test_suite___")).
StaticFS("/", http.Dir("./"))

tester := iffy.NewTester(t, ginEngine)

tester.AddCall("retrieve test folder index and validate replacement OK", http.MethodGet, "/", "").
Headers(adminHeaders).
Checkers(
iffy.ExpectStatus(200),
expectStringNotPresent("static.go"),
expectStringPresent("___test_suite___"),
)

tester.AddCall("unknown static page", http.MethodGet, "/dsqdzdzodkzdzdz", "").
Headers(adminHeaders).
Checkers(
iffy.ExpectStatus(404),
)

tester.Run()
}

func expectStringNotPresent(value string) iffy.Checker {
return func(r *http.Response, body string, respObject interface{}) error {
if strings.Contains(body, value) {
return fmt.Errorf("Response body invalid: should not contains %q, but it does", value)
}
return nil
}
}

func expectStringPresent(value string) iffy.Checker {
return func(r *http.Response, body string, respObject interface{}) error {
if !strings.Contains(body, value) {
return fmt.Errorf("Response body invalid: should contains %q, but it doesn't", value)
}
return nil
}
}

func marshalJSON(t *testing.T, i interface{}) string {
jsonBytes, err := json.Marshal(i)
if err != nil {
Expand Down
69 changes: 64 additions & 5 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"net/http"
"os"
"os/signal"
"path"
"strings"
"syscall"

"github.com/gin-gonic/gin"
Expand All @@ -27,8 +29,11 @@ import (
// Server wraps the http handler that exposes a REST API to control
// the task orchestration engine
type Server struct {
httpHandler *fizz.Fizz
authMiddleware func(*gin.Context)
httpHandler *fizz.Fizz
authMiddleware func(*gin.Context)
dashboardPathPrefix string
dashboardAPIPathPrefix string
editorPathPrefix string
}

// NewServer returns a new Server
Expand All @@ -47,6 +52,35 @@ func (s *Server) WithAuth(authProvider func(*http.Request) (string, error)) {
}
}

// SetDashboardPathPrefix configures the custom path prefix for dashboard static files hosting.
// It doesn't change the path used by utask API to serve the files, it's only used inside UI files
// in order that dashboard can be aware of a ProxyPass configuration.
func (s *Server) SetDashboardPathPrefix(dashboardPathPrefix string) {
if dashboardPathPrefix == "" {
return
}
s.dashboardPathPrefix = dashboardPathPrefix
}

// SetDashboardAPIPathPrefix configures a custom path prefix that UI should use when calling utask API.
// Required when utask API is exposed behind a ProxyPass and UI need to know the absolute URI to call.
func (s *Server) SetDashboardAPIPathPrefix(dashboardAPIPathPrefix string) {
if dashboardAPIPathPrefix == "" {
return
}
s.dashboardAPIPathPrefix = dashboardAPIPathPrefix
}

// SetEditorPathPrefix configures a custom path prefix for editor static files hosting.
// It doesn't change the path used by utask API to serve the files, it's only used inside UI files
// in order that editor can be aware of a ProxyPass configuration.
func (s *Server) SetEditorPathPrefix(editorPathPrefix string) {
if editorPathPrefix == "" {
return
}
s.editorPathPrefix = editorPathPrefix
}

// ListenAndServe launches an http server and stays blocked until
// the server is shut down by a system signal
func (s *Server) ListenAndServe() error {
Expand Down Expand Up @@ -79,14 +113,39 @@ func (s *Server) Handler(ctx context.Context) http.Handler {
return s.httpHandler
}

func generateBaseHref(pathPrefix, uri string) string {
// UI requires to have a trailing slash at the end
return path.Join(pathPrefix, uri) + "/"
}

func generatePathPrefixAPI(pathPrefix string) string {
p := path.Join(pathPrefix, "/")
if p == "." {
p = "/"
} else if !strings.HasSuffix(p, "/") {
p += "/"
}
return p
}

// build registers all routes and their corresponding handlers for the Server's API
func (s *Server) build(ctx context.Context) {
if s.httpHandler == nil {
ginEngine := gin.Default()

ginEngine.
Group("/ui", s.authMiddleware).
StaticFS("/dashboard", http.Dir("./static/dashboard"))
ginEngine.
Group("/", s.authMiddleware,
StaticFilePatternReplaceMiddleware(
"___UTASK_DASHBOARD_BASEHREF___",
generateBaseHref(s.dashboardPathPrefix, "/ui/dashboard"),
"___UTASK_DASHBOARD_PREFIXAPIBASEURL___",
generatePathPrefixAPI(s.dashboardAPIPathPrefix))).
StaticFS("/ui/dashboard", http.Dir("./static/dashboard"))

ginEngine.Group("/",
StaticFilePatternReplaceMiddleware(
"___UTASK_EDITOR_BASEHREF___",
generateBaseHref(s.editorPathPrefix, "/ui/editor"))).
StaticFS("/ui/editor", http.Dir("./static/editor"))

collectMetrics(ctx)
Expand Down
24 changes: 24 additions & 0 deletions api/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package api

import (
"testing"

"github.com/stretchr/testify/assert"
)

func Test_generateBaseHref(t *testing.T) {
assert.Equal(t, "/ui/dashboard/", generateBaseHref("", "/ui/dashboard/"))
assert.Equal(t, "/ui/dashboard/", generateBaseHref("/", "/ui/dashboard/"))
assert.Equal(t, "/ui/dashboard/", generateBaseHref("", "/ui/dashboard"))
assert.Equal(t, "/toto/ui/dashboard/", generateBaseHref("/toto", "/ui/dashboard/"))
assert.Equal(t, "/toto/ui/dashboard/", generateBaseHref("/toto/", "/ui/dashboard/"))
}

func Test_generateAPIPathPrefix(t *testing.T) {
assert.Equal(t, "/", generatePathPrefixAPI(""))
assert.Equal(t, "/", generatePathPrefixAPI("./"))
assert.Equal(t, "/", generatePathPrefixAPI("."))
assert.Equal(t, "/toto/", generatePathPrefixAPI("/toto"))
assert.Equal(t, "/toto/", generatePathPrefixAPI("/toto/"))

}
134 changes: 134 additions & 0 deletions api/static.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package api

import (
"bufio"
"bytes"
"io"
"net"
"net/http"
"strconv"
"strings"

"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)

const (
notWrittenSize = -1
defaultStatus = http.StatusOK
)

// StaticFilePatternReplaceMiddleware is a middleware that modifies the response body with a given replace pattern
// Used inside utask to change response body of static files at flight, to customize path prefixes.
func StaticFilePatternReplaceMiddleware(oldnew ...string) func(c *gin.Context) {
return func(c *gin.Context) {
buffer := &bytes.Buffer{}
originalWriter := c.Writer
wb := &responseWriter{
ResponseWriter: originalWriter,
status: defaultStatus,
size: notWrittenSize,
bufwriter: buffer,
}
wb.replacer = strings.NewReplacer(oldnew...)
c.Writer = wb
c.Next()
str := wb.replacer.Replace(buffer.String())
wb.Header().Set("Content-Length", strconv.FormatInt(int64(len(str)), 10))
wb.WriteHeaderNow()
if _, err := originalWriter.WriteString(str); err != nil {
logrus.WithError(err).Error("unable to respond to static content")
}
}
}

type responseWriter struct {
http.ResponseWriter
size int64
status int
replacer *strings.Replacer
bufwriter *bytes.Buffer
}

// WriteHeader sends an HTTP response header with the provided
// status code.
func (w *responseWriter) WriteHeader(code int) {
if code <= 0 || w.status == code || w.Written() {
return
}

w.status = code
}

// WriteHeaderNow forces to write HTTP headers
func (w *responseWriter) WriteHeaderNow() {
if w.Written() {
return
}

w.size = 0
w.ResponseWriter.WriteHeader(w.status)
}

// Write writes a given bytes array into the response buffer.
func (w *responseWriter) Write(data []byte) (n int, err error) {
n, err = w.bufwriter.Write(data)
if err != nil {
logrus.WithError(err).Error("responseWriter: unable to write data")
}
w.size += int64(n)
return

}

// WriteString writes a given string into the response buffer.
func (w *responseWriter) WriteString(s string) (n int, err error) {
n, err = io.WriteString(w.bufwriter, s)
if err != nil {
logrus.WithError(err).Error("responseWriter: unable to write string")
}
w.size += int64(n)
return
}

// Status returns the HTTP response status code of the current request.
func (w *responseWriter) Status() int {
return w.status
}

// Size returns the number of bytes already written into the response http body.
func (w *responseWriter) Size() int {
return int(w.size)
}

// Writter returns true if the response body was already written.
func (w *responseWriter) Written() bool {
return w.size != notWrittenSize
}

// Hijack implements the http.Hijacker interface.
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if w.size < 0 {
w.size = 0
}
return w.ResponseWriter.(http.Hijacker).Hijack()
}

// CloseNotify implements the http.CloseNotify interface.
func (w *responseWriter) CloseNotify() <-chan bool {
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
}

// Flush implements the http.Flush interface.
func (w *responseWriter) Flush() {
w.WriteHeaderNow()
w.ResponseWriter.(http.Flusher).Flush()
}

// Pusher implements the http.Pusher interface
func (w *responseWriter) Pusher() (pusher http.Pusher) {
if pusher, ok := w.ResponseWriter.(http.Pusher); ok {
return pusher
}
return nil
}
8 changes: 8 additions & 0 deletions cmd/utask/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ var rootCmd = &cobra.Command{
server = api.NewServer()
server.WithAuth(defaultAuthHandler)

cfg, err := utask.Config(store)
if err != nil {
return err
}
server.SetDashboardPathPrefix(cfg.DashboardPathPrefix)
server.SetDashboardAPIPathPrefix(cfg.DashboardAPIPathPrefix)
server.SetEditorPathPrefix(cfg.EditorPathPrefix)

for _, err := range []error{
// register builtin executors
builtin.Register(),
Expand Down
9 changes: 6 additions & 3 deletions utask.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ const (

// This is the key used in Values for a step to refer to itself
This = "this"
)

// UtaskCfgSecretAlias is the key for the config item containing global configuration data
const UtaskCfgSecretAlias = "utask-cfg"
// UtaskCfgSecretAlias is the key for the config item containing global configuration data
UtaskCfgSecretAlias = "utask-cfg"
)

// Cfg holds global configuration data
type Cfg struct {
Expand All @@ -86,6 +86,9 @@ type Cfg struct {
ConcealedSecrets []string `json:"concealed_secrets"`
ResourceLimits map[string]uint `json:"resource_limits"`
MaxConcurrentExecutions uint `json:"max_concurrent_executions"`
DashboardPathPrefix string `json:"dashboard_path_prefix"`
DashboardAPIPathPrefix string `json:"dashboard_api_path_prefix"`
EditorPathPrefix string `json:"editor_path_prefix"`

resourceSemaphores map[string]*semaphore.Weighted
executionSemaphore *semaphore.Weighted
Expand Down

0 comments on commit 768e83f

Please sign in to comment.