Skip to content

Commit

Permalink
Merge branch 'refactor/sparrow-api' into refactor/api
Browse files Browse the repository at this point in the history
  • Loading branch information
lvlcn-t committed Jan 27, 2024
2 parents 9717641 + 55616be commit e9b711f
Show file tree
Hide file tree
Showing 9 changed files with 362 additions and 304 deletions.
171 changes: 60 additions & 111 deletions pkg/sparrow/api.go → pkg/api/api.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// sparrow
// (C) 2023, Deutsche Telekom IT GmbH
// (C) 2024, Deutsche Telekom IT GmbH
//
// Deutsche Telekom IT GmbH and all other contributors /
// copyright owners license this file to you under the Apache
Expand All @@ -16,67 +16,53 @@
// specific language governing permissions and limitations
// under the License.

package sparrow
package api

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"

"github.com/caas-team/sparrow/internal/logger"
"github.com/caas-team/sparrow/pkg/checks"
"github.com/caas-team/sparrow/pkg/config"
"github.com/getkin/kin-openapi/openapi3"
"github.com/go-chi/chi/v5"
"github.com/prometheus/client_golang/prometheus/promhttp"
"gopkg.in/yaml.v3"
)

type encoder interface {
Encode(v any) error
type API struct {
server *http.Server
router chi.Router
}

const (
urlParamCheckName = "checkName"
readHeaderTimeout = time.Second * 5
readHeaderTimeout = 5 * time.Second
shutdownTimeout = 30 * time.Second
)

var ErrCreateOpenapiSchema = errors.New("failed to get schema for check")

func (s *Sparrow) register(ctx context.Context) {
s.router.Use(logger.Middleware(ctx))

// Handles OpenApi spec
s.router.Get("/openapi", s.getOpenapi)
// Handles public user facing json api
s.router.Get(fmt.Sprintf("/v1/metrics/{%s}", urlParamCheckName), s.getCheckMetrics)

// Handles requests with simple http ok
// Required for global targets in checks
s.router.Handle("/", okHandler(ctx))

// Handles prometheus metrics
s.router.Handle("/metrics",
promhttp.HandlerFor(
s.metrics.GetRegistry(),
promhttp.HandlerOpts{Registry: s.metrics.GetRegistry()},
))
func New(cfg *config.ApiConfig) *API {
r := chi.NewRouter()
return &API{
server: &http.Server{Addr: cfg.ListeningAddress, Handler: r, ReadHeaderTimeout: readHeaderTimeout},
router: r,
}
}

// Serves the data api.
//
// Run serves the data api
// Blocks until context is done
func (s *Sparrow) api(ctx context.Context) error {
func (a *API) Run(ctx context.Context) error {
log := logger.FromContext(ctx)
cErr := make(chan error, 1)
s.register(ctx)

// run http server in goroutine
go func(cErr chan error) {
defer close(cErr)
log.Info("Serving Api", "addr", s.cfg.Api.ListeningAddress)
if err := s.server.ListenAndServe(); err != nil {
log.Info("Serving Api", "addr", a.server.Addr)
if err := a.server.ListenAndServe(); err != nil {
log.Error("Failed to serve api", "error", err)
cErr <- err
}
Expand All @@ -95,22 +81,58 @@ func (s *Sparrow) api(ctx context.Context) error {
}
}

// shutdownAPI gracefully shuts down the api server
// Shutdown gracefully shuts down the api server
// Returns an error if an error is present in the context
// or if the server cannot be shut down
func (s *Sparrow) shutdownAPI(ctx context.Context) error {
func (a *API) Shutdown(ctx context.Context) error {
errC := ctx.Err()
log := logger.FromContext(ctx)
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
err := s.server.Shutdown(shutdownCtx)
err := a.server.Shutdown(shutdownCtx)
if err != nil {
log.Error("Failed to shutdown api server", "error", err)
return fmt.Errorf("failed shutting down API: %w", errors.Join(errC, err))
}
return errC
}

type Route struct {
Path string
Method string
Handler http.HandlerFunc
}

func (a *API) RegisterRoutes(ctx context.Context, routes ...Route) error {
a.router.Use(logger.Middleware(ctx))
for _, route := range routes {
switch route.Method {
case http.MethodGet:
a.router.Get(route.Path, route.Handler)
case http.MethodPost:
a.router.Post(route.Path, route.Handler)
case http.MethodPut:
a.router.Put(route.Path, route.Handler)
case http.MethodDelete:
a.router.Delete(route.Path, route.Handler)
case http.MethodPatch:
a.router.Patch(route.Path, route.Handler)
case "Handle":
a.router.Handle(route.Path, route.Handler)
case "HandleFunc":
a.router.HandleFunc(route.Path, route.Handler)
default:
return fmt.Errorf("unsupported method: %s", route.Method)
}
}

// Handles requests with simple http ok
// Required for global tarMan in checks
a.router.Handle("/", okHandler(ctx))

return nil
}

// okHandler returns a handler that will serve status ok
func okHandler(ctx context.Context) http.Handler {
log := logger.FromContext(ctx)
Expand Down Expand Up @@ -144,10 +166,10 @@ var oapiBoilerplate = openapi3.T{
Servers: openapi3.Servers{},
}

func (s *Sparrow) Openapi(ctx context.Context) (openapi3.T, error) {
func OpenAPI(ctx context.Context, cks map[string]checks.Check) (openapi3.T, error) {
log := logger.FromContext(ctx)
doc := oapiBoilerplate
for name, c := range s.checks {
for name, c := range cks {
ref, err := c.Schema()
if err != nil {
log.Error("failed to get schema for check", "error", err)
Expand Down Expand Up @@ -175,76 +197,3 @@ func (s *Sparrow) Openapi(ctx context.Context) (openapi3.T, error) {

return doc, nil
}

func (s *Sparrow) getCheckMetrics(w http.ResponseWriter, r *http.Request) {
log := logger.FromContext(r.Context())
name := chi.URLParam(r, urlParamCheckName)
if name == "" {
w.WriteHeader(http.StatusBadRequest)
_, err := w.Write([]byte(http.StatusText(http.StatusBadRequest)))
if err != nil {
log.Error("Failed to write response", "error", err)
}
return
}
res, ok := s.db.Get(name)
if !ok {
w.WriteHeader(http.StatusNotFound)
_, err := w.Write([]byte(http.StatusText(http.StatusNotFound)))
if err != nil {
log.Error("Failed to write response", "error", err)
}
return
}

enc := json.NewEncoder(w)
enc.SetIndent("", " ")

if err := enc.Encode(res); err != nil {
log.Error("failed to encode response", "error", err)
w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write([]byte(http.StatusText(http.StatusInternalServerError)))
if err != nil {
log.Error("Failed to write response", "error", err)
}
return
}
w.Header().Add("Content-Type", "application/json")
}

func (s *Sparrow) getOpenapi(w http.ResponseWriter, r *http.Request) {
log := logger.FromContext(r.Context())
oapi, err := s.Openapi(r.Context())
if err != nil {
log.Error("failed to create openapi", "error", err)
w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write([]byte(http.StatusText(http.StatusInternalServerError)))
if err != nil {
log.Error("Failed to write response", "error", err)
}
return
}

mime := r.Header.Get("Accept")

var marshaler encoder
switch mime {
case "application/json":
marshaler = json.NewEncoder(w)
w.Header().Add("Content-Type", "application/json")
default:
marshaler = yaml.NewEncoder(w)
w.Header().Add("Content-Type", "text/yaml")
}

err = marshaler.Encode(oapi)
if err != nil {
log.Error("failed to marshal openapi", "error", err)
w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write([]byte(http.StatusText(http.StatusInternalServerError)))
if err != nil {
log.Error("Failed to write response", "error", err)
}
return
}
}
70 changes: 70 additions & 0 deletions pkg/api/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// sparrow
// (C) 2024, Deutsche Telekom IT GmbH
//
// Deutsche Telekom IT GmbH and all other contributors /
// copyright owners license this file to you under the Apache
// License, Version 2.0 (the "License"); you may not use this
// file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package api

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

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

func TestAPI_RegisterRoutes(_ *testing.T) {
}

func TestAPI_shutdownWhenContextCanceled(t *testing.T) {
a := API{
router: chi.NewRouter(),
server: &http.Server{}, //nolint:gosec
}
ctx, cancel := context.WithCancel(context.Background())
cancel()

if err := a.Run(ctx); !errors.Is(err, context.Canceled) {
t.Error("Expected ErrApiContext")
}
}

func Test_okHandler(t *testing.T) {
ctx := context.Background()

req, err := http.NewRequestWithContext(ctx, "GET", "/okHandler", http.NoBody)
if err != nil {
t.Fatal(err)
}

rr := httptest.NewRecorder()
handler := okHandler(ctx)

handler.ServeHTTP(rr, req)

if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}

expected := "ok"
if rr.Body.String() != expected {
t.Errorf("Handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}
4 changes: 2 additions & 2 deletions pkg/sparrow/metrics.go → pkg/metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// sparrow
// (C) 2023, Deutsche Telekom IT GmbH
// (C) 2024, Deutsche Telekom IT GmbH
//
// Deutsche Telekom IT GmbH and all other contributors /
// copyright owners license this file to you under the Apache
Expand All @@ -16,7 +16,7 @@
// specific language governing permissions and limitations
// under the License.

package sparrow
package metrics

import (
"github.com/prometheus/client_golang/prometheus"
Expand Down
2 changes: 1 addition & 1 deletion pkg/sparrow/metrics_moq.go → pkg/metrics/metrics_moq.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pkg/sparrow/metrics_test.go → pkg/metrics/metrics_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// sparrow
// (C) 2023, Deutsche Telekom IT GmbH
// (C) 2024, Deutsche Telekom IT GmbH
//
// Deutsche Telekom IT GmbH and all other contributors /
// copyright owners license this file to you under the Apache
Expand All @@ -16,7 +16,7 @@
// specific language governing permissions and limitations
// under the License.

package sparrow
package metrics

import (
"reflect"
Expand Down
Loading

0 comments on commit e9b711f

Please sign in to comment.