Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
# Better Go Playground Docker image deployment workflow
#
# Builds and deploys image to Docker hub when a new release created.
# Uses DOCKER_USER, DOCKER_PASS and GTAG env vars from GitHub repo secrets.
# Uses DOCKER_USER and DOCKER_PASS env vars from GitHub repo secrets.
#
# see: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
#

name: Deploy To Docker Hub
name: Build Docker Image
on:
release:
types:
- created
repository_dispatch:
types: manual-deploy
types: manual-build

jobs:
deploy:
build:
runs-on: ubuntu-latest
steps:
- id: go-cache-paths
Expand All @@ -42,8 +42,8 @@ jobs:
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/v}" >> $GITHUB_ENV
- name: Build Docker image
run: |
make docker-image TAG=${{ env.RELEASE_VERSION }} GTAG=${{ secrets.GTAG }}
- name: Deploy image to Docker hub
make docker-image TAG=${{ env.RELEASE_VERSION }}
- name: Push image to Docker hub
run: |
make docker-push-image TAG=${{ env.RELEASE_VERSION }} \
DOCKER_USER=${{ secrets.DOCKER_USER }} \
Expand Down
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ include docker.mk

.PHONY:run
run:
@GOROOT=$(GOROOT) $(GO) run $(PKG) -f ./data/packages.json -static-dir="$(UI)/build" -debug=$(DEBUG) -addr $(LISTEN_ADDR)
@GOROOT=$(GOROOT) $(GO) run $(PKG) \
-f ./data/packages.json \
-static-dir="$(UI)/build" \
-gtag-id="$(GTAG)" \
-debug=$(DEBUG) \
-addr $(LISTEN_ADDR)

.PHONY:ui
ui:
@cd $(UI) && REACT_APP_LANG_SERVER=http://$(LISTEN_ADDR) REACT_APP_GTAG=$(GTAG) REACT_APP_VERSION=testing yarn start
@cd $(UI) && REACT_APP_LANG_SERVER='//$(LISTEN_ADDR)' REACT_APP_VERSION=testing yarn start

.PHONY: cover
cover:
Expand Down
16 changes: 11 additions & 5 deletions build/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ FROM node:17-alpine as ui-build
COPY web /tmp/web
WORKDIR /tmp/web
ARG APP_VERSION=1.0.0
ARG APP_GTAG=""
ARG GITHUB_URL=https://github.com/x1unix/go-playground
ARG GITHUB_URL='https://github.com/x1unix/go-playground'
RUN yarn install --silent && \
REACT_APP_VERSION=$APP_VERSION REACT_APP_GITHUB_URL=$GITHUB_URL REACT_APP_GTAG=$APP_GTAG yarn build
REACT_APP_VERSION=$APP_VERSION NODE_ENV=production REACT_APP_GITHUB_URL=$GITHUB_URL yarn build

FROM golang:1.17-alpine as build
WORKDIR /tmp/playground
Expand All @@ -26,11 +25,18 @@ ENV APP_CLEAN_INTERVAL=10m
ENV APP_DEBUG=false
ENV APP_PLAYGROUND_URL='https://play.golang.org'
ENV APP_GOTIP_URL='https://gotipplay.golang.org'
ENV APP_GTAG_ID=''
COPY data ./data
COPY --from=ui-build /tmp/web/build ./public
COPY --from=build /tmp/playground/server .
COPY --from=build /tmp/playground/worker.wasm ./public
COPY --from=build /tmp/playground/wasm_exec.js ./public
EXPOSE 8000
ENTRYPOINT /opt/playground/server -f=/opt/playground/data/packages.json -addr=:8000 \
-clean-interval=${APP_CLEAN_INTERVAL} -debug=${APP_DEBUG} -playground-url=${APP_PLAYGROUND_URL} -gotip-url=${APP_GOTIP_URL}
ENTRYPOINT /opt/playground/server \
-f='/opt/playground/data/packages.json' \
-clean-interval="${APP_CLEAN_INTERVAL}" \
-debug="${APP_DEBUG}" \
-playground-url="${APP_PLAYGROUND_URL}" \
-gotip-url="${APP_GOTIP_URL}" \
-gtag-id="${APP_GTAG_ID}" \
-addr=:8000
21 changes: 18 additions & 3 deletions cmd/playground/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"flag"
"fmt"
"github.com/x1unix/go-playground/pkg/langserver/webutil"
"net/http"
"os"
"path/filepath"
Expand Down Expand Up @@ -33,6 +34,7 @@ type appArgs struct {
cleanupInterval string
assetsDirectory string
connectTimeout time.Duration
googleAnalyticsID string
}

func (a appArgs) getCleanDuration() (time.Duration, error) {
Expand All @@ -56,6 +58,7 @@ func main() {
flag.BoolVar(&args.debug, "debug", false, "Enable debug mode")
flag.StringVar(&args.assetsDirectory, "static-dir", filepath.Join(wd, "public"), "Path to web page assets (HTML, JS, etc)")
flag.DurationVar(&args.connectTimeout, "timeout", 15*time.Second, "Go Playground server connect timeout")
flag.StringVar(&args.googleAnalyticsID, "gtag-id", "", "Google Analytics tag ID (optional)")

l := getLogger(args.debug)
defer l.Sync() //nolint:errcheck
Expand Down Expand Up @@ -123,12 +126,24 @@ func start(goRoot string, args appArgs) error {
GoTip: goTipClient,
}
// API routes
langserver.New(Version, clients, packages, compiler.NewBuildService(zap.S(), store)).
svcCfg := langserver.ServiceConfig{
Version: Version,
}
langserver.New(svcCfg, clients, packages, compiler.NewBuildService(zap.S(), store)).
Mount(r.PathPrefix("/api").Subrouter())

// Web UI routes
indexHandler := langserver.NewIndexFileServer(args.assetsDirectory)
spaHandler := langserver.NewSpaFileServer(args.assetsDirectory)
tplVars := langserver.TemplateArguments{
GoogleTagID: args.googleAnalyticsID,
}
if err := webutil.ValidateGTag(tplVars.GoogleTagID); err != nil {
zap.L().Error("invalid GTag ID value, parameter will be ignored",
zap.String("gtag", tplVars.GoogleTagID), zap.Error(err))
tplVars.GoogleTagID = ""
}

indexHandler := langserver.NewTemplateFileServer(zap.L(), filepath.Join(args.assetsDirectory, langserver.IndexFileName), tplVars)
spaHandler := langserver.NewSpaFileServer(args.assetsDirectory, tplVars)
r.Path("/").
Handler(indexHandler)
r.Path("/snippet/{snippetID:[A-Za-z0-9_-]+}").
Expand Down
2 changes: 1 addition & 1 deletion docker.mk
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ docker-image:
@echo ":: Building '$(IMG_NAME):latest' $(TAG)..."
docker image build -t $(IMG_NAME):latest -t $(IMG_NAME):$(TAG) -f $(DOCKERFILE) \
--build-arg APP_VERSION=$(TAG) \
--build-arg APP_GTAG=$(GTAG) .
.
12 changes: 8 additions & 4 deletions pkg/langserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,23 @@ type PlaygroundServices struct {

// Service is language server service
type Service struct {
version string
config ServiceConfig
log *zap.SugaredLogger
index analyzer.PackageIndex
compiler compiler.BuildService
playgrounds *PlaygroundServices
limiter *rate.Limiter
}

type ServiceConfig struct {
Version string
}

// New is Service constructor
func New(version string, playgrounds *PlaygroundServices, packages []*analyzer.Package, builder compiler.BuildService) *Service {
func New(cfg ServiceConfig, playgrounds *PlaygroundServices, packages []*analyzer.Package, builder compiler.BuildService) *Service {
return &Service{
config: cfg,
compiler: builder,
version: version,
playgrounds: playgrounds,
log: zap.S().Named("langserver"),
index: analyzer.BuildPackageIndex(packages),
Expand Down Expand Up @@ -131,7 +135,7 @@ func (s *Service) provideSuggestion(req SuggestionRequest) (*SuggestionsResponse

// HandleGetVersion handles /api/version
func (s *Service) HandleGetVersion(w http.ResponseWriter, _ *http.Request) error {
WriteJSON(w, VersionResponse{Version: s.version})
WriteJSON(w, VersionResponse{Version: s.config.Version})
return nil
}

Expand Down
19 changes: 3 additions & 16 deletions pkg/langserver/spa.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,11 @@ func (i httpStatusInterceptor) WriteHeader(_ int) {
i.ResponseWriter.WriteHeader(i.desiredStatus)
}

type IndexFileServer struct {
indexFilePath string
}

// NewIndexFileServer returns handler which serves index.html page from root.
func NewIndexFileServer(root string) *IndexFileServer {
return &IndexFileServer{
indexFilePath: filepath.Join(string(root), IndexFileName),
}
}

func (fs IndexFileServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
http.ServeFile(rw, r, fs.indexFilePath)
}

// SpaFileServer is a wrapper around http.FileServer for serving SPA contents.
type SpaFileServer struct {
root string
NotFoundHandler http.Handler
templateVars TemplateArguments
}

// ServeHTTP implements http.Handler
Expand Down Expand Up @@ -93,11 +79,12 @@ func containsDotDot(v string) bool {
func isSlashRune(r rune) bool { return r == '/' || r == '\\' }

// NewSpaFileServer returns SPA handler
func NewSpaFileServer(root string) *SpaFileServer {
func NewSpaFileServer(root string, tplVars TemplateArguments) *SpaFileServer {
notFoundHandler := NewFileServerWithStatus(filepath.Join(root, NotFoundFileName), http.StatusNotFound)
return &SpaFileServer{
NotFoundHandler: notFoundHandler,
root: root,
templateVars: tplVars,
}
}

Expand Down
94 changes: 94 additions & 0 deletions pkg/langserver/tmphandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package langserver

import (
"bytes"
"io"
"net/http"
"os"
"path/filepath"
"sync"
"text/template"
"time"

"go.uber.org/zap"
)

type nopRSeeker struct {
size int64
io.Reader
}

func newNopRSeeker(size int, r io.Reader) *nopRSeeker {
return &nopRSeeker{
size: int64(size),
Reader: r,
}
}

type TemplateArguments struct {
GoogleTagID string
}

type TemplateFileServer struct {
log *zap.Logger
filePath string
templateVars TemplateArguments
once *sync.Once
buffer io.ReadSeeker
modTime time.Time
}

// Seek implements io.Seeker
func (n nopRSeeker) Seek(_ int64, whence int) (int64, error) {
switch whence {
case io.SeekStart:
return 0, nil
default:
return n.size, nil
}
}

// NewTemplateFileServer returns handler which compiles and serves HTML page template.
func NewTemplateFileServer(logger *zap.Logger, filePath string, tplVars TemplateArguments) *TemplateFileServer {
return &TemplateFileServer{
log: logger,
once: new(sync.Once),
filePath: filePath,
templateVars: tplVars,
}
}

func (fs *TemplateFileServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
fs.once.Do(fs.precompileTemplate)

if fs.buffer == nil {
http.ServeFile(rw, r, fs.filePath)
return
}

http.ServeContent(rw, r, fs.filePath, fs.modTime, fs.buffer)
}

func (fs *TemplateFileServer) precompileTemplate() {
stat, err := os.Stat(fs.filePath)
if err != nil {
fs.log.Error("failed to read template file", zap.Error(err), zap.String("filePath", fs.filePath))
return
}

tpl, err := template.New(filepath.Base(fs.filePath)).ParseFiles(fs.filePath)
if err != nil {
fs.log.Error("failed to parse page template", zap.Error(err), zap.String("filePath", fs.filePath))
return
}

buff := bytes.NewBuffer(make([]byte, 0, stat.Size()))
if err := tpl.Execute(buff, fs.templateVars); err != nil {
fs.log.Error("failed to execute page template", zap.Error(err), zap.String("filePath", fs.filePath))
return
}

fs.log.Info("successfully compiled page template", zap.String("filePath", fs.filePath))
fs.buffer = newNopRSeeker(buff.Len(), buff)
fs.modTime = time.Now()
}
16 changes: 16 additions & 0 deletions pkg/langserver/webutil/gtag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package webutil

import (
"errors"
"regexp"
)

var gtagRegExp = regexp.MustCompile(`(?i)^[A-Z]{2}-[A-Z0-9\-\+]+$`)

// ValidateGTag validates Google Analytics tag ID
func ValidateGTag(gtag string) error {
if !gtagRegExp.MatchString(gtag) {
return errors.New("invalid GTag ID value")
}
return nil
}
26 changes: 26 additions & 0 deletions pkg/langserver/webutil/gtag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package webutil

import (
"testing"

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

func TestValidateGTag(t *testing.T) {
cases := map[string]bool{
"EN-123456789-0": true,
`EN-123456789-0"/>`: false,
`EN-12345/foobar.js`: false,
`foo.js`: false,
}
for input, expect := range cases {
t.Run(input, func(t *testing.T) {
err := ValidateGTag(input)
if !expect {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
3 changes: 2 additions & 1 deletion tools/cover.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
./pkg/compiler/...
./pkg/analyzer/...
./pkg/goplay/...
./pkg/goplay/...
./pkg/langserver/webutil
Loading