Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating few REST API endpoints #119

Merged
merged 30 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
214b702
Using Chi router for apiserver.
shahariaazam Jan 1, 2024
27db9af
Re-organized codebase
shahariaazam Jan 1, 2024
88755fa
`GET /api/v1/resource` endpoint created
shahariaazam Jan 1, 2024
1a95956
`GET /api/v1/resource` endpoint created
shahariaazam Jan 2, 2024
918038c
Fixed some tests and query to get all the resources
shahariaazam Jan 2, 2024
6f63b5b
Added pagination during getting all resource list
shahariaazam Jan 2, 2024
5f931cc
Fix linting
shahariaazam Jan 2, 2024
2fe6ff3
Added some tests for discover
shahariaazam Jan 2, 2024
7b0692c
Added some tests for discover
shahariaazam Jan 2, 2024
43f5f67
Updated some tests
shahariaazam Jan 2, 2024
974e2f8
Updated some tests
shahariaazam Jan 2, 2024
c842216
Updated some tests
shahariaazam Jan 2, 2024
97a084d
Updated some tests
shahariaazam Jan 2, 2024
e0aec5a
Minor re-factoring
shahariaazam Jan 2, 2024
8702497
Filtering resources now support filtering by multiple metadata key an…
shahariaazam Jan 2, 2024
eeb010d
Minor fix
shahariaazam Jan 2, 2024
28a4518
Debugging integration test failure
shahariaazam Jan 2, 2024
46a2258
Fix query
shahariaazam Jan 2, 2024
e781680
Added few more test
shahariaazam Jan 2, 2024
29e0e0f
Re-factoring large method
shahariaazam Jan 2, 2024
d4cae97
OpenAPI schema added for website
shahariaazam Jan 2, 2024
07fb876
Added API reference link in the navbar
shahariaazam Jan 2, 2024
4752499
Fixing some linting
shahariaazam Jan 2, 2024
b48a478
Frontend html page created
shahariaazam Jan 2, 2024
1d800b0
lint fixing
shahariaazam Jan 2, 2024
5c13400
Frontend html page created
shahariaazam Jan 3, 2024
5065866
Frontend html page created
shahariaazam Jan 3, 2024
50cf476
title change for static page
shahariaazam Jan 3, 2024
8de088c
Merge branch 'master' into terediX-118-create-REST-api-endpoing
shahariaazam Jan 28, 2024
a4a163f
Few linting error fixed
shahariaazam Jan 28, 2024
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
141 changes: 109 additions & 32 deletions cmd/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ package cmd

import (
"context"
"embed"
"errors"
"io/fs"
"net/http"
"time"

"github.com/go-chi/chi"
"github.com/go-chi/cors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/shaharia-lab/teredix/pkg/api"
"github.com/shaharia-lab/teredix/pkg/config"
"github.com/shaharia-lab/teredix/pkg/metrics"
"github.com/shaharia-lab/teredix/pkg/processor"
Expand All @@ -19,6 +24,12 @@ import (
"github.com/spf13/cobra"
)

// Embed the entire static directory. frontend/static directory is located under `cmd` directory
// because go:embed doesn't support relative path
//
//go:embed frontend/static/*
var webStaticFiles embed.FS

// NewDiscoverCommand build "discover" command
func NewDiscoverCommand() *cobra.Command {
var cfgFile string
Expand Down Expand Up @@ -46,6 +57,97 @@ func NewDiscoverCommand() *cobra.Command {
return &cmd
}

// Server represent server
type Server struct {
apiServer *http.Server
promMetricsServer *http.Server
logger *logrus.Logger
storage storage.Storage
}

// NewServer instantiate new server
func NewServer(logger *logrus.Logger, storage storage.Storage) *Server {
return &Server{
logger: logger,
storage: storage,
}
}

func (s *Server) setupAPIServer(port string) {
r := chi.NewRouter()

// Set up CORS
crs := cors.New(cors.Options{
AllowedOrigins: []string{"*"}, // Allow all origins
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
AllowCredentials: true,
MaxAge: 300, // Maximum age to cache preflight request
})
r.Use(crs.Handler)

// Redirect the home page to /app/index.html
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/app/index.html", http.StatusMovedPermanently)
})

// Create a new router group
r.Route("/api", func(r chi.Router) {
r.Route("/v1", func(r chi.Router) {
r.Get("/resources", api.GetAllResources(s.storage))
})
})

// Serve static files
r.Get("/app/*", func(w http.ResponseWriter, r *http.Request) {
// Create a subdirectory for the embedded files
staticFS, err := fs.Sub(webStaticFiles, "frontend/static")
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}

// Strip the "/app" prefix and serve the files
http.StripPrefix("/app", http.FileServer(http.FS(staticFS))).ServeHTTP(w, r)
})

r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})

s.apiServer = &http.Server{
Addr: ":" + port,
Handler: r,
}
}

func (s *Server) setupPromMetricsServer() {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())

s.promMetricsServer = &http.Server{
Addr: ":2112",
Handler: mux,
}
}

func (s *Server) startServer(server *http.Server, serverName string) {
go func() {
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
s.logger.WithError(err).Errorf("failed to start %s server", serverName)
}
}()
}

func (s *Server) shutdownServer(ctx context.Context, server *http.Server, serverName string) error {
if err := server.Shutdown(ctx); err != nil {
s.logger.WithError(err).Errorf("failed to shutdown %s gracefully", serverName)
return err
}
s.apiServer = nil
return nil
}

func run(ctx context.Context, appConfig *config.AppConfig, logger *logrus.Logger) error {
st, err := storage.BuildStorage(appConfig)
if err != nil {
Expand Down Expand Up @@ -73,35 +175,12 @@ func run(ctx context.Context, appConfig *config.AppConfig, logger *logrus.Logger

logger.Info("started processing scheduled jobs")

// Set up your handler
http.Handle("/metrics", promhttp.Handler())

// Use http.Server directly to gain control over its lifecycle
promMetricsServer := &http.Server{
Addr: ":2112",
}
s := NewServer(logger, st)
s.setupAPIServer("8080")
s.setupPromMetricsServer()

// Start server in a separate goroutine so it doesn't block
go func() {
if err := promMetricsServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.WithError(err).Error("failed to start http server")
}
}()

// Start another HTTP server for the API server
apiServer := &http.Server{
Addr: ":8080",
}

http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})

go func() {
if err := apiServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.WithError(err).Error("failed to start API server")
}
}()
s.startServer(s.promMetricsServer, "metrics")
s.startServer(s.apiServer, "api")

// Wait for context cancellation (in your case, the timeout)
<-ctx.Done()
Expand All @@ -110,13 +189,11 @@ func run(ctx context.Context, appConfig *config.AppConfig, logger *logrus.Logger
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := promMetricsServer.Shutdown(shutdownCtx); err != nil {
logger.WithError(err).Error("failed to shutdown Prometheus metrics server gracefully")
if err := s.shutdownServer(shutdownCtx, s.apiServer, "API server"); err != nil {
return err
}

if err := apiServer.Shutdown(shutdownCtx); err != nil {
logger.WithError(err).Error("failed to shutdown API server gracefully")
if err := s.shutdownServer(shutdownCtx, s.promMetricsServer, "Prometheus metrics server"); err != nil {
return err
}

Expand Down
123 changes: 123 additions & 0 deletions cmd/discover_unit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package cmd

import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/go-chi/chi"
"github.com/shaharia-lab/teredix/pkg/resource"
"github.com/shaharia-lab/teredix/pkg/storage"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func TestServer_SetupAPIServer(t *testing.T) {
logger := logrus.New()
st := new(storage.Mock)

s := NewServer(logger, st)

s.setupAPIServer("8080")

assert.NotNil(t, s.apiServer)
assert.Equal(t, ":8080", s.apiServer.Addr)
}

func TestServer_APIEndpoint(t *testing.T) {
logger := logrus.New()
st := new(storage.Mock)

r1 := resource.NewResource("vm", "test", "test", "test", 1)
st.On("Find", mock.Anything).Return([]resource.Resource{r1}, nil)

s := NewServer(logger, st)
s.setupAPIServer("8080")

r := chi.NewRouter()
r.Mount("/", s.apiServer.Handler)

ts := httptest.NewServer(r)
defer ts.Close()

res, err := http.Get(ts.URL + "/api/v1/resources?page=1&per_page=10")
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)

res, err = http.Get(ts.URL + "/api/v1/resources?page=1&per_page=300")
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)

res, err = http.Get(ts.URL + "/api/v1/resources")
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
}

func TestServer_SetupPromMetricsServer(t *testing.T) {
logger := logrus.New()
s := NewServer(logger, nil)

s.setupPromMetricsServer()

assert.NotNil(t, s.promMetricsServer)
assert.Equal(t, ":2112", s.promMetricsServer.Addr)
}

func TestServer_PromMetricsEndpoint(t *testing.T) {
logger := logrus.New()
s := NewServer(logger, nil)
s.setupPromMetricsServer()

ts := httptest.NewServer(s.promMetricsServer.Handler)
defer ts.Close()

res, err := http.Get(ts.URL + "/metrics")
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
}

func TestServer_StartAndShutdownServer(t *testing.T) {
// Arrange
logger := logrus.New()
s := NewServer(logger, nil)
s.setupAPIServer("0")

// Act
// Start the server in a separate goroutine so it doesn't block
go s.startServer(s.apiServer, "API server")

// Create a context with a timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Wait for the server to start
time.Sleep(100 * time.Millisecond)

// Send a request to the server
ts := httptest.NewServer(s.apiServer.Handler)
defer ts.Close()

resp, err := http.Get(ts.URL + "/ping")

// Assert
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)

// Act
// Shutdown the server
err = s.shutdownServer(ctx, s.apiServer, "API server")

// Assert
assert.NoError(t, err)

// Wait for the server to shutdown
time.Sleep(100 * time.Millisecond)

// Check if the server is still running
if s.apiServer != nil {
t.Errorf("Server is still running after shutdown")
}
}
Loading
Loading