commit fbca89e751d0fd6546568a191a06193267e34488 Author: tiff Date: Mon Dec 30 00:10:26 2024 -0500 Initial commit diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..5a3c060 --- /dev/null +++ b/.air.toml @@ -0,0 +1,46 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./main" + cmd = "make build" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5cae497 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with "go test -c" +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +tmp/ + +# IDE specific files +.vscode +.idea + +# .env file +.env + +# Project build +main +*templ.go + +# OS X generated file +.DS_Store + diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..e001bf5 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,42 @@ +version: 2 +before: + hooks: + - go mod tidy + +env: + - PACKAGE_PATH=github.com///cmd + +builds: +- binary: "{{ .ProjectName }}" + main: ./cmd/api + goos: + - darwin + - linux + - windows + goarch: + - amd64 + - arm64 + env: + - CGO_ENABLED=0 + ldflags: + - -s -w -X {{.Env.PACKAGE_PATH}}={{.Version}} +release: + prerelease: auto + +universal_binaries: +- replace: true + +archives: + - name_template: > + {{- .ProjectName }}_{{- .Version }}_{{- title .Os }}_{{- if eq .Arch "amd64" }}x86_64{{- else if eq .Arch "386" }}i386{{- else }}{{ .Arch }}{{ end }}{{- if .Arm }}v{{ .Arm }}{{ end -}} + format_overrides: + - goos: windows + format: zip + builds_info: + group: root + owner: root + files: + - README.md + +checksum: + name_template: 'checksums.txt' diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f91dac6 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +# Simple Makefile for a Go project + +# Build the application +all: build test + +build: + @echo "Building..." + + + @go build -o main cmd/api/main.go + +# Run the application +run: + @go run cmd/api/main.go + +# Test the application +test: + @echo "Testing..." + @go test ./... -v + +# Clean the binary +clean: + @echo "Cleaning..." + @rm -f main + +# Live Reload +watch: + @if command -v air > /dev/null; then \ + air; \ + echo "Watching...";\ + else \ + read -p "Go's 'air' is not installed on your machine. Do you want to install it? [Y/n] " choice; \ + if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \ + go install github.com/air-verse/air@latest; \ + air; \ + echo "Watching...";\ + else \ + echo "You chose not to install air. Exiting..."; \ + exit 1; \ + fi; \ + fi + +.PHONY: all build run test clean watch diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd924db --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Project goload + +One Paragraph of project description goes here + +## Getting Started + +These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system. + +## MakeFile + +Run build make command with tests +```bash +make all +``` + +Build the application +```bash +make build +``` + +Run the application +```bash +make run +``` + +Live reload the application: +```bash +make watch +``` + +Run the test suite: +```bash +make test +``` + +Clean up binary from the last build: +```bash +make clean +``` diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..4531107 --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os/signal" + "syscall" + "time" + + "goload/internal/server" +) + +func gracefulShutdown(apiServer *http.Server, done chan bool) { + // Create context that listens for the interrupt signal from the OS. + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + // Listen for the interrupt signal. + <-ctx.Done() + + log.Println("shutting down gracefully, press Ctrl+C again to force") + + // The context is used to inform the server it has 5 seconds to finish + // the request it is currently handling + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := apiServer.Shutdown(ctx); err != nil { + log.Printf("Server forced to shutdown with error: %v", err) + } + + log.Println("Server exiting") + + // Notify the main goroutine that the shutdown is complete + done <- true +} + +func main() { + + server := server.NewServer() + + // Create a done channel to signal when the shutdown is complete + done := make(chan bool, 1) + + // Run graceful shutdown in a separate goroutine + go gracefulShutdown(server, done) + + err := server.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + panic(fmt.Sprintf("http server error: %s", err)) + } + + // Wait for the graceful shutdown to complete + <-done + log.Println("Graceful shutdown complete.") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..725bf0d --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module goload + +go 1.23.2 + +require github.com/joho/godotenv v1.5.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d61b19e --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/internal/server/routes.go b/internal/server/routes.go new file mode 100644 index 0000000..a93638a --- /dev/null +++ b/internal/server/routes.go @@ -0,0 +1,49 @@ +package server + +import ( + "encoding/json" + "log" + "net/http" +) + +func (s *Server) RegisterRoutes() http.Handler { + mux := http.NewServeMux() + + // Register routes + mux.HandleFunc("/", s.HelloWorldHandler) + + // Wrap the mux with CORS middleware + return s.corsMiddleware(mux) +} + +func (s *Server) corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Set CORS headers + w.Header().Set("Access-Control-Allow-Origin", "*") // Replace "*" with specific origins if needed + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH") + w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type, X-CSRF-Token") + w.Header().Set("Access-Control-Allow-Credentials", "false") // Set to "true" if credentials are required + + // Handle preflight OPTIONS requests + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + + // Proceed with the next handler + next.ServeHTTP(w, r) + }) +} + +func (s *Server) HelloWorldHandler(w http.ResponseWriter, r *http.Request) { + resp := map[string]string{"message": "Hello World"} + jsonResp, err := json.Marshal(resp) + if err != nil { + http.Error(w, "Failed to marshal response", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write(jsonResp); err != nil { + log.Printf("Failed to write response: %v", err) + } +} diff --git a/internal/server/routes_test.go b/internal/server/routes_test.go new file mode 100644 index 0000000..602ae81 --- /dev/null +++ b/internal/server/routes_test.go @@ -0,0 +1,31 @@ +package server + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHandler(t *testing.T) { + s := &Server{} + server := httptest.NewServer(http.HandlerFunc(s.HelloWorldHandler)) + defer server.Close() + resp, err := http.Get(server.URL) + if err != nil { + t.Fatalf("error making request to server. Err: %v", err) + } + defer resp.Body.Close() + // Assertions + if resp.StatusCode != http.StatusOK { + t.Errorf("expected status OK; got %v", resp.Status) + } + expected := "{\"message\":\"Hello World\"}" + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("error reading response body. Err: %v", err) + } + if expected != string(body) { + t.Errorf("expected response body to be %v; got %v", expected, string(body)) + } +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..dfdc252 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,36 @@ +package server + +import ( + "fmt" + "net/http" + "net/http/httputil" + "os" + // "strconv" + "sync" + "time" + + _ "github.com/joho/godotenv/autoload" +) +// +// type Server struct { +// port int +// } +// +type Backend struct { + URL *url.URL + Alive bool + mux sync.RWMutex + ReverseProxy *httputil.ReverseProxy +} + +type SeverPool struct { + backends []*Backend + current uint64 +} + +u, _ := url.Parse("http://localhost:8080") +rp := httputil.NewSingleHostReverseProxy(u) + +http.HandlerFunc(rp.ServeHTTP) + +