Skip to content

Commit

Permalink
WIP: Add current-job api
Browse files Browse the repository at this point in the history
  • Loading branch information
moskyb committed Feb 7, 2023
1 parent 13ba54d commit d948e81
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ require (
github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220812150832-b6b31c6eeeaf
github.com/buildkite/roko v1.0.3-0.20221121010703-599521c80157
github.com/gliderlabs/ssh v0.3.5
github.com/go-chi/chi/v5 v5.0.0
github.com/google/go-cmp v0.5.9
go.opentelemetry.io/contrib/propagators/aws v1.12.0
go.opentelemetry.io/contrib/propagators/b3 v1.12.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
github.com/go-chi/chi/v5 v5.0.0 h1:DBPx88FjZJH3FsICfDAfIfnb7XxKIYVGG6lOPlhENAg=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
Expand Down
3 changes: 3 additions & 0 deletions jobapi/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// jobapi provides an API with which to interact with and mutate the currently executing job
// Pronunciation: /ˈdʒɑbe̞pi/ /joh-bah-pee/
package jobapi
49 changes: 49 additions & 0 deletions jobapi/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package jobapi

import (
"encoding/json"
"net/http"
"strings"
)

func (s *Server) authMdlw(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer next.ServeHTTP(w, r)

enc := json.NewEncoder(w)

auth := r.Header.Get("Authorization")
if auth == "" {
w.WriteHeader(http.StatusUnauthorized)
enc.Encode(ErrorResponse{Error: "authorization header is required"})
return
}

authType, token, found := strings.Cut(auth, " ")
if !found {
w.WriteHeader(http.StatusUnauthorized)
enc.Encode(ErrorResponse{Error: "invalid authorization header: must be in the form `Bearer <token>`"})
return
}

if authType != "Bearer" {
w.WriteHeader(http.StatusUnauthorized)
enc.Encode(ErrorResponse{Error: "invalid authorization header: type must be Bearer"})
return
}

if token != s.token {
w.WriteHeader(http.StatusUnauthorized)
enc.Encode(ErrorResponse{Error: "invalid authorization token"})
return
}
})
}

func (s *Server) headersMdlw(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer next.ServeHTTP(w, r)

w.Header().Set("Content-Type", "application/json")
})
}
5 changes: 5 additions & 0 deletions jobapi/payloads.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package jobapi

type ErrorResponse struct {
Error string `json:"error"`
}
46 changes: 46 additions & 0 deletions jobapi/routes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package jobapi

import (
"net/http"

"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)

func (s *Server) router() chi.Router {
r := chi.NewRouter()
r.Use(
middleware.Recoverer,
s.authMdlw,
s.headersMdlw,
)

r.Route("/api/current-job/v1", func(r chi.Router) {
r.Get("/env", s.getEnv())
r.Patch("/env", s.patchEnv())
r.Delete("/env", s.deleteEnv())
})

return r
}

func (s *Server) getEnv() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// STUB
w.WriteHeader(http.StatusOK)
}
}

func (s *Server) patchEnv() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// STUB
w.WriteHeader(http.StatusOK)
}
}

func (s *Server) deleteEnv() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// STUB
w.WriteHeader(http.StatusOK)
}
}
94 changes: 94 additions & 0 deletions jobapi/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package jobapi

import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
"time"
)

type Server struct {
SocketPath string
token string
httpSvr *http.Server
}

func NewServer(socketPath string) (server *Server, token string, err error) {
exists, err := socketExists(socketPath)
if err != nil {
return nil, "", err
}

if exists {
err = os.RemoveAll(socketPath)
if err != nil {
return nil, "", fmt.Errorf("removing existing socket: %w", err)
}
}

token, err = generateToken(32)
if err != nil {
return nil, "", fmt.Errorf("generating token: %w", err)
}

return &Server{
SocketPath: socketPath,
token: token,
}, token, nil
}

func (s *Server) Start() error {
r := s.router()
l, err := net.Listen("unix", s.SocketPath)
if err != nil {
return fmt.Errorf("listening on socket: %w", err)
}

s.httpSvr = &http.Server{Handler: r}
go func() {
_ = s.httpSvr.Serve(l)
}()

return nil
}

func (s *Server) Stop() error {
serverCtx, serverStopCtx := context.WithCancel(context.Background())
defer serverStopCtx()

// REVIEW: Should we capture signals here?

// Shutdown signal with grace period of 30 seconds
shutdownCtx, _ := context.WithTimeout(serverCtx, 10*time.Second)

go func() {
<-shutdownCtx.Done()
if shutdownCtx.Err() == context.DeadlineExceeded {
// REVIEW: What should we do in this situation? Force a return? Log something?
}
}()

// Trigger graceful shutdown
err := s.httpSvr.Shutdown(shutdownCtx)
if err != nil {
return fmt.Errorf("shutting down Job API server: %w", err)
}

return nil
}

func socketExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}

if errors.Is(err, os.ErrNotExist) {
return false, nil
}

return false, fmt.Errorf("checking if socket exists: %w", err)
}
23 changes: 23 additions & 0 deletions jobapi/tokens.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package jobapi

import (
"crypto/rand"
"fmt"
"math/big"
)

const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func generateToken(length int) (string, error) {
lettersLen := len(letters)
bigLettersLen := big.NewInt(int64(lettersLen))
b := make([]byte, length)
for i := range b {
bigRand, err := rand.Int(rand.Reader, bigLettersLen)
if err != nil {
return "", fmt.Errorf("generating crypto-random big.Int: %w", err)
}
b[i] = letters[bigRand.Int64()%int64(lettersLen)] // mod letterslen just in case rand.Int returns a number larger than lettersLen, don't want to have a panic
}
return string(b), nil
}

0 comments on commit d948e81

Please sign in to comment.