Skip to content

Commit

Permalink
ROX-24492: Add emailsender openAPI docs (#2110)
Browse files Browse the repository at this point in the history
* Add emailsender openAPI docs

* Add openAPI endpoint

* Revert podman redundant change
  • Loading branch information
kurlov authored Dec 6, 2024
1 parent d760158 commit 772734e
Show file tree
Hide file tree
Showing 28 changed files with 1,937 additions and 37 deletions.
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ run:
- internal/dinosaur/pkg/api/private
- internal/dinosaur/pkg/api/admin/private
- pkg/client/redhatsso/api
- emailsender/pkg/client/openapi
skip-files:
- ".*_moq.go"
# timeout for analysis, e.g. 30s, 5m, default is 1m
Expand Down
5 changes: 5 additions & 0 deletions .openapi-generator-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,8 @@ pkg/client/redhatsso/api/docs/**.md
pkg/client/redhatsso/api/**all_of.go
pkg/client/redhatsso/api/go.mod
pkg/client/redhatsso/api/go.sum
emailsender/pkg/client/openapi/README.md
emailsender/pkg/client/openapi/docs/**.md
emailsender/pkg/client/openapi/**all_of.go
emailsender/pkg/client/openapi/go.mod
emailsender/pkg/client/openapi/go.sum
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ repos:
- id: trailing-whitespace
args: ["--markdown-linebreak-ext=md"]
- id: end-of-file-fixer
exclude: '^(?:secrets/db.*|internal/dinosaur/pkg/api/(admin|private|public)/.*|pkg/client/redhatsso/api/.*)$' # Matches either secrets/db.* files or the generated files under internal/dinosaur/pkg/api/(admin|private|public) and pkg/client/redhatsso/client/api.
exclude: '^(?:secrets/db.*|internal/dinosaur/pkg/api/(admin|private|public)/.*|pkg/client/redhatsso/api/.*|emailsender/pkg/client/openapi/.*)$' # Matches either secrets/db.* files or the generated files under internal/dinosaur/pkg/api/(admin|private|public) and pkg/client/redhatsso/client/api.
- id: check-json
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.5
hooks:
- id: shellcheck
# Ignore scripts generated by openapi-generator.
exclude: '^(?:internal/dinosaur/pkg/api/.*|pkg/client/redhatsso/api/.*)$'
exclude: '^(?:internal/dinosaur/pkg/api/.*|pkg/client/redhatsso/api/.*|emailsender/pkg/client/openapi/.*)$'
- repo: local
hooks:
- id: code/check
Expand Down
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -395,10 +395,11 @@ openapi/validate: openapi-generator
$(OPENAPI_GENERATOR) validate -i openapi/fleet-manager.yaml
$(OPENAPI_GENERATOR) validate -i openapi/fleet-manager-private.yaml
$(OPENAPI_GENERATOR) validate -i openapi/fleet-manager-private-admin.yaml
$(OPENAPI_GENERATOR) validate -i openapi/emailsender.yaml
.PHONY: openapi/validate

# generate the openapi schema and generated package
openapi/generate: openapi/generate/public openapi/generate/private openapi/generate/admin openapi/generate/rhsso
openapi/generate: openapi/generate/public openapi/generate/private openapi/generate/admin openapi/generate/rhsso openapi/generate/emailsender
.PHONY: openapi/generate

openapi/generate/public: $(GOBINDATA_BIN) openapi-generator
Expand Down Expand Up @@ -435,6 +436,13 @@ openapi/generate/rhsso: $(GOBINDATA_BIN) openapi-generator
$(GOFMT) -w pkg/client/redhatsso/api
.PHONY: openapi/generate/rhsso

openapi/generate/emailsender: $(GOBINDATA_BIN) openapi-generator
rm -rf emailsender/pkg/client/openapi
$(OPENAPI_GENERATOR) validate -i openapi/emailsender.yaml
$(OPENAPI_GENERATOR) generate -i openapi/emailsender.yaml -g go -o emailsender/pkg/client/openapi --package-name openapi -t openapi/templates --ignore-file-override ./.openapi-generator-ignore
$(GOFMT) -w emailsender/pkg/client/openapi
.PHONY: openapi/generate/emailsender

# fail if formatting is required
code/check:
@if ! [ -z "$$(find . -path './vendor' -prune -o -type f -name '*.go' -print0 | xargs -0 $(GOFMT) -l)" ]; then \
Expand All @@ -459,7 +467,8 @@ run/docs:
$(DOCKER) run -u $(shell id -u) --rm --name swagger_ui_docs -d -p 8082:8080 -e URLS="[ \
{ url: \"./openapi/fleet-manager.yaml\", name: \"Public API\" },\
{ url: \"./openapi/fleet-manager-private.yaml\", name: \"Private API\"},\
{ url: \"./openapi/fleet-manager-private-admin.yaml\", name: \"Private Admin API\"}]"\
{ url: \"./openapi/fleet-manager-private-admin.yaml\", name: \"Private Admin API\"},\
{ url: \"./openapi/emailsender.yaml\", name: \"Emailsender API\"}]"\
-v $(PWD)/openapi/:/usr/share/nginx/html/openapi:Z swaggerapi/swagger-ui
@echo "Please open http://localhost:8082/"
.PHONY: run/docs
Expand Down
15 changes: 2 additions & 13 deletions emailsender/pkg/api/emailhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (eh *EmailHandler) SendEmail(w http.ResponseWriter, r *http.Request) {
jsonDecoder.DisallowUnknownFields()

if err := jsonDecoder.Decode(&request); err != nil {
eh.errorResponse(w, "Cannot decode send email request payload", http.StatusBadRequest)
shared.HandleError(r, w, errors.MalformedRequest("failed to decode send email request payload"))
return
}

Expand All @@ -54,7 +54,7 @@ func (eh *EmailHandler) SendEmail(w http.ResponseWriter, r *http.Request) {
}

if err := eh.emailSender.Send(r.Context(), request.To, request.RawMessage, tenantID); err != nil {
eh.errorResponse(w, "Cannot send email", http.StatusInternalServerError)
shared.HandleError(r, w, errors.GeneralError("cannot send email"))
return
}

Expand Down Expand Up @@ -83,14 +83,3 @@ func (eh *EmailHandler) jsonResponse(w http.ResponseWriter, envelop Envelope, st

return nil
}

func (eh *EmailHandler) errorResponse(w http.ResponseWriter, message string, statusCode int) {
envelope := Envelope{
"error": message,
}

if err := eh.jsonResponse(w, envelope, statusCode); err != nil {
glog.Errorf("Failed creating error json response: %v", err)
http.Error(w, "Can not create error json response", http.StatusInternalServerError)
}
}
43 changes: 29 additions & 14 deletions emailsender/pkg/api/emailhandler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ func TestEmailHandler_SendEmail(t *testing.T) {
}

tests := []struct {
name string
emailSender email.Sender
req *http.Request
wantCode int
wantBody string
name string
emailSender email.Sender
req *http.Request
wantCode int
wantBody string
wantErrorReason string
}{
{
name: "should return JSON response with StatusOK to a valid email request",
Expand All @@ -70,11 +71,11 @@ func TestEmailHandler_SendEmail(t *testing.T) {
wantBody: `{"status":"sent"}`,
},
{
name: "should return JSON error with StatusBadRequest when cannot decode request",
emailSender: simpleEmailSender,
req: httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(invalidJsonReq)),
wantCode: http.StatusBadRequest,
wantBody: `{"error":"Cannot decode send email request payload"}`,
name: "should return JSON error with StatusBadRequest when cannot decode request",
emailSender: simpleEmailSender,
req: httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(invalidJsonReq)),
wantCode: http.StatusBadRequest,
wantErrorReason: "failed to decode send email request payload",
},
{
name: "should return JSON error with StatusInternalServerError when cannot send email",
Expand All @@ -83,9 +84,9 @@ func TestEmailHandler_SendEmail(t *testing.T) {
return errors.New("failed to send email")
},
},
req: httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(jsonReq)),
wantCode: http.StatusInternalServerError,
wantBody: `{"error":"Cannot send email"}`,
req: httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(jsonReq)),
wantCode: http.StatusInternalServerError,
wantErrorReason: "cannot send email",
},
}
for _, tt := range tests {
Expand All @@ -103,7 +104,21 @@ func TestEmailHandler_SendEmail(t *testing.T) {
t.Errorf("expected status code %d, got %d", tt.wantCode, resp.Result().StatusCode)
}

if resp.Body.String() != tt.wantBody {
if tt.wantErrorReason != "" {
var respDecoded map[string]string
if err := json.NewDecoder(resp.Body).Decode(&respDecoded); err != nil {
t.Errorf("failed to decoded response body")
}
errorReason, ok := respDecoded["reason"]
if !ok {
t.Errorf("response error body does not have reason key")
}
if errorReason != tt.wantErrorReason {
t.Errorf("expected error reason %s, got %s", tt.wantBody, resp.Body.String())
}
}

if tt.wantBody != "" && resp.Body.String() != tt.wantBody {
t.Errorf("expected body %s, got %s", tt.wantBody, resp.Body.String())
}
})
Expand Down
14 changes: 14 additions & 0 deletions emailsender/pkg/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,21 @@ import (
"net/http"
)

// Represents Emailsender openAPI definition
type openAPIHandler struct {
OpenAPIDefinition string
}

// NewOpenAPIHandler ...
func NewOpenAPIHandler(openAPIDefinition string) *openAPIHandler {
return &openAPIHandler{openAPIDefinition}
}

// HealthCheckHandler returns 200 HTTP status code
func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}

func (h openAPIHandler) Get(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(h.OpenAPIDefinition))
}
6 changes: 3 additions & 3 deletions emailsender/pkg/api/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ func EnsureJSONContentType(next http.Handler) http.Handler {
contentType := r.Header.Get("Content-Type")

if contentType == "" {
http.Error(w, "Empty Content-Type", http.StatusBadRequest)
shared.HandleError(r, w, errors.BadRequest("empty Content-Type"))
return
}
if contentType != "" {
mt, _, err := mime.ParseMediaType(contentType)
if err != nil {
http.Error(w, "Malformed Content-Type header", http.StatusBadRequest)
shared.HandleError(r, w, errors.MalformedRequest("malformed Content-Type header"))
return
}

if mt != "application/json" {
http.Error(w, "Content-Type header must be application/json", http.StatusUnsupportedMediaType)
shared.HandleError(r, w, errors.BadRequest("Content-Type header must be application/json"))
return
}
}
Expand Down
27 changes: 24 additions & 3 deletions emailsender/pkg/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import (
"github.com/pkg/errors"

"github.com/stackrox/acs-fleet-manager/emailsender/config"
"github.com/stackrox/acs-fleet-manager/emailsender/pkg/client"
acscsAPI "github.com/stackrox/acs-fleet-manager/pkg/api"
acscsErrors "github.com/stackrox/acs-fleet-manager/pkg/errors"
acscsHandlers "github.com/stackrox/acs-fleet-manager/pkg/handlers"
loggingMiddleware "github.com/stackrox/acs-fleet-manager/pkg/server/logging"
)

Expand All @@ -30,13 +33,19 @@ func SetupRoutes(authConfig config.AuthConfig, emailHandler *EmailHandler) (http

func setupRoutes(authnHandlerFunc authnHandlerBuilder, authConfig config.AuthConfig, emailHandler *EmailHandler) (http.Handler, error) {
router := mux.NewRouter()
errorsHandler := acscsHandlers.NewErrorsHandler()
openAPIHandler := NewOpenAPIHandler(client.OpenAPIDefinition)

router.NotFoundHandler = http.HandlerFunc(acscsAPI.SendNotFound)
router.MethodNotAllowedHandler = http.HandlerFunc(acscsAPI.SendMethodNotAllowed)

// using a path prefix here to seperate endpoints that should use
// middleware vs. endpoints that shouldn't for instance /health
apiRouter := router.PathPrefix("/api").Subrouter()
apiV1Router := apiRouter.PathPrefix("/v1").Subrouter()

// add middlewares
apiRouter.Use(
apiV1Router.Use(
loggingMiddleware.RequestLoggingMiddleware,
EnsureJSONContentType,
// this middleware is supposed to validate if the client is authorized to do the desired request
Expand All @@ -45,8 +54,18 @@ func setupRoutes(authnHandlerFunc authnHandlerBuilder, authConfig config.AuthCon
emailsenderAuthorizationMiddleware(authConfig),
)

// health endpoint
router.HandleFunc("/health", HealthCheckHandler).Methods("GET")
apiRouter.HandleFunc("/v1/acscsemail", emailHandler.SendEmail).Methods("POST")

// openAPI definiton endpoint
router.HandleFunc("/openapi", openAPIHandler.Get).Methods("GET")

// errors endpoint
router.HandleFunc("/api/v1/acscsemail/errors/{id}", errorsHandler.Get).Methods(http.MethodGet)
router.HandleFunc("/api/v1/acscsemail/errors", errorsHandler.List).Methods(http.MethodGet)

// send email endpoint
apiV1Router.HandleFunc("/acscsemail", emailHandler.SendEmail).Methods("POST")

// this settings are to make sure the middlewares shared with acs-fleet-manager
// print a prefix and href matching to the emailsender application
Expand All @@ -71,7 +90,9 @@ func buildAuthnHandler(router http.Handler, cfg config.AuthConfig) (http.Handler
Error(fmt.Sprint(acscsErrors.ErrorUnauthenticated)).
Service(emailsenderPrefix).
Next(router).
Public("/health")
Public("/health").
Public("/openapi").
Public("/api/v1/acscsemail/errors/?[0-9]*")

for _, keyURL := range cfg.JwksURLs {
authnHandlerBuilder.KeysURL(keyURL)
Expand Down
24 changes: 24 additions & 0 deletions emailsender/pkg/client/openapi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so

# Folders
_obj
_test

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out

*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*

_testmain.go

*.exe
*.test
*.prof
23 changes: 23 additions & 0 deletions emailsender/pkg/client/openapi/.openapi-generator-ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator

# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.

# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs

# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux

# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux

# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md
1 change: 1 addition & 0 deletions emailsender/pkg/client/openapi/.openapi-generator/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4.3.1
8 changes: 8 additions & 0 deletions emailsender/pkg/client/openapi/.travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
language: go

install:
- go get -d -v .

script:
- go build -v ./

Loading

0 comments on commit 772734e

Please sign in to comment.