Skip to content

Commit

Permalink
Add about service/endpoint to API
Browse files Browse the repository at this point in the history
Add a new service and endpoint to the API to get system information.
Include short version, preservation system, and preprocessing and
poststorage child workflows configuration.
  • Loading branch information
jraddaoui committed Nov 22, 2024
1 parent c0afd76 commit dfad0eb
Show file tree
Hide file tree
Showing 22 changed files with 2,205 additions and 2 deletions.
21 changes: 19 additions & 2 deletions cmd/enduro/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
temporalsdk_workflow "go.temporal.io/sdk/workflow"
goahttp "goa.design/goa/v3/http"

"github.com/artefactual-sdps/enduro/internal/about"
"github.com/artefactual-sdps/enduro/internal/api"
"github.com/artefactual-sdps/enduro/internal/api/auth"
goahttpstorage "github.com/artefactual-sdps/enduro/internal/api/gen/http/storage/client"
Expand Down Expand Up @@ -274,6 +275,14 @@ func main() {
}
}

aboutsvc := about.NewService(
logger.WithName("about"),
cfg.Preservation.TaskQueue,
cfg.Preprocessing,
cfg.Poststorage,
tokenVerifier,
)

Check warning on line 285 in cmd/enduro/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/enduro/main.go#L278-L285

Added lines #L278 - L285 were not covered by tests
// Set up the watcher service.
var wsvc watcher.Service
{
Expand All @@ -292,7 +301,7 @@ func main() {

g.Add(
func() error {
srv = api.HTTPServer(logger, tp, &cfg.API, pkgsvc, storagesvc)
srv = api.HTTPServer(logger, tp, &cfg.API, pkgsvc, storagesvc, aboutsvc)

Check warning on line 304 in cmd/enduro/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/enduro/main.go#L304

Added line #L304 was not covered by tests
return srv.ListenAndServe()
},
func(err error) {
Expand Down Expand Up @@ -333,11 +342,19 @@ func main() {
os.Exit(1)
}

ias := about.NewService(
logger.WithName("internal-about"),
cfg.Preservation.TaskQueue,
cfg.Preprocessing,
cfg.Poststorage,
&auth.NoopTokenVerifier{},
)

Check warning on line 352 in cmd/enduro/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/enduro/main.go#L345-L352

Added lines #L345 - L352 were not covered by tests
var srv *http.Server

g.Add(
func() error {
srv = api.HTTPServer(logger, tp, &cfg.InternalAPI, ips, iss)
srv = api.HTTPServer(logger, tp, &cfg.InternalAPI, ips, iss, ias)

Check warning on line 357 in cmd/enduro/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/enduro/main.go#L357

Added line #L357 was not covered by tests
return srv.ListenAndServe()
},
func(err error) {
Expand Down
86 changes: 86 additions & 0 deletions internal/about/goa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package about

import (
"context"
"errors"

"github.com/go-logr/logr"
"goa.design/goa/v3/security"

"github.com/artefactual-sdps/enduro/internal/api/auth"
goaabout "github.com/artefactual-sdps/enduro/internal/api/gen/about"
"github.com/artefactual-sdps/enduro/internal/poststorage"
"github.com/artefactual-sdps/enduro/internal/preprocessing"
"github.com/artefactual-sdps/enduro/internal/temporal"
"github.com/artefactual-sdps/enduro/internal/version"
)

type Service struct {
logger logr.Logger
presTaskQueue string
ppConfig preprocessing.Config
psConfig []poststorage.Config
tokenVerifier auth.TokenVerifier
}

var _ goaabout.Service = (*Service)(nil)

var ErrUnauthorized error = goaabout.Unauthorized("Unauthorized")

func NewService(
logger logr.Logger,
presTaskQueue string,
ppConfig preprocessing.Config,
psConfig []poststorage.Config,
tokenVerifier auth.TokenVerifier,
) *Service {
return &Service{
logger: logger,
presTaskQueue: presTaskQueue,
ppConfig: ppConfig,
psConfig: psConfig,
tokenVerifier: tokenVerifier,
}
}

func (s *Service) JWTAuth(ctx context.Context, token string, scheme *security.JWTScheme) (context.Context, error) {
claims, err := s.tokenVerifier.Verify(ctx, token)
if err != nil {
if !errors.Is(err, auth.ErrUnauthorized) {
s.logger.V(1).Info("failed to verify token", "err", err)
}
return ctx, ErrUnauthorized
}

ctx = auth.WithUserClaims(ctx, claims)

return ctx, nil
}

func (s *Service) About(context.Context, *goaabout.AboutPayload) (*goaabout.EnduroAbout, error) {
res := &goaabout.EnduroAbout{
Version: version.Short,
Preprocessing: &goaabout.EnduroPreprocessing{
Enabled: s.ppConfig.Enabled,
WorkflowName: s.ppConfig.Temporal.WorkflowName,
TaskQueue: s.ppConfig.Temporal.TaskQueue,
},
}

res.PreservationSystem = "Unknown"
if s.presTaskQueue == temporal.AmWorkerTaskQueue {
res.PreservationSystem = "Archivematica"
} else if s.presTaskQueue == temporal.A3mWorkerTaskQueue {
res.PreservationSystem = "a3m"
}

res.Poststorage = make([]*goaabout.EnduroPoststorage, len(s.psConfig))
for i, cfg := range s.psConfig {
res.Poststorage[i] = &goaabout.EnduroPoststorage{
WorkflowName: cfg.WorkflowName,
TaskQueue: cfg.TaskQueue,
}
}

return res, nil
}
217 changes: 217 additions & 0 deletions internal/about/goa_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package about_test

import (
"context"
"fmt"
"regexp"
"testing"

"github.com/go-logr/logr"
"github.com/go-logr/logr/funcr"
"github.com/google/go-cmp/cmp/cmpopts"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"

"github.com/artefactual-sdps/enduro/internal/about"
"github.com/artefactual-sdps/enduro/internal/api/auth"
authfake "github.com/artefactual-sdps/enduro/internal/api/auth/fake"
goaabout "github.com/artefactual-sdps/enduro/internal/api/gen/about"
"github.com/artefactual-sdps/enduro/internal/config"
"github.com/artefactual-sdps/enduro/internal/poststorage"
"github.com/artefactual-sdps/enduro/internal/preprocessing"
"github.com/artefactual-sdps/enduro/internal/pres"
)

func TestJWTAuth(t *testing.T) {
t.Parallel()

type test struct {
name string
mock func(tv *authfake.MockTokenVerifier, claims *auth.Claims)
claims *auth.Claims
logged string
wantErr error
}
for _, tt := range []test{
{
name: "Verifies and adds claims to context",
mock: func(tv *authfake.MockTokenVerifier, claims *auth.Claims) {
tv.EXPECT().
Verify(context.Background(), "abc").
Return(claims, nil)
},
claims: &auth.Claims{
Email: "info@artefactual.com",
EmailVerified: true,
Attributes: []string{},
},
},
{
name: "Fails with unauthorized error",
mock: func(tv *authfake.MockTokenVerifier, claims *auth.Claims) {
tv.EXPECT().
Verify(context.Background(), "abc").
Return(nil, auth.ErrUnauthorized)
},
wantErr: about.ErrUnauthorized,
},
{
name: "Fails with unauthorized error (logging)",
mock: func(tv *authfake.MockTokenVerifier, claims *auth.Claims) {
tv.EXPECT().
Verify(context.Background(), "abc").
Return(nil, fmt.Errorf("fail"))
},
logged: `"level"=1 "msg"="failed to verify token" "err"="fail"`,
wantErr: about.ErrUnauthorized,
},
} {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

var logged string
logger := funcr.New(
func(prefix, args string) { logged = args },
funcr.Options{Verbosity: 1},
)

tvMock := authfake.NewMockTokenVerifier(gomock.NewController(t))
tt.mock(tvMock, tt.claims)
srv := about.NewService(
logger,
"",
preprocessing.Config{},
[]poststorage.Config{},
tvMock,
)

ctx, err := srv.JWTAuth(context.Background(), "abc", nil)
assert.Equal(t, logged, tt.logged)
if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr)
return
}
assert.NilError(t, err)
assert.DeepEqual(t, auth.UserClaimsFromContext(ctx), tt.claims)
})
}
}

func TestAbout(t *testing.T) {
t.Parallel()

versionRegExp := regexp.MustCompile(`^\d+\.\d+\.\d+-dev$`)

type test struct {
name string
config config.Configuration
want *goaabout.EnduroAbout
}
for _, tt := range []test{
{
name: "Empty config",
config: config.Configuration{},
want: &goaabout.EnduroAbout{
Version: "",
PreservationSystem: "Unknown",
Preprocessing: &goaabout.EnduroPreprocessing{
Enabled: false,
WorkflowName: "",
TaskQueue: "",
},
Poststorage: goaabout.EnduroPoststorageCollection{},
},
},
{
name: "Preservation system: Archivematica",
config: config.Configuration{Preservation: pres.Config{TaskQueue: "am"}},
want: &goaabout.EnduroAbout{
Version: "",
PreservationSystem: "Archivematica",
Preprocessing: &goaabout.EnduroPreprocessing{
Enabled: false,
WorkflowName: "",
TaskQueue: "",
},
Poststorage: goaabout.EnduroPoststorageCollection{},
},
},
{
name: "Preservation system: a3m",
config: config.Configuration{Preservation: pres.Config{TaskQueue: "a3m"}},
want: &goaabout.EnduroAbout{
Version: "",
PreservationSystem: "a3m",
Preprocessing: &goaabout.EnduroPreprocessing{
Enabled: false,
WorkflowName: "",
TaskQueue: "",
},
Poststorage: goaabout.EnduroPoststorageCollection{},
},
},
{
name: "Full config",
config: config.Configuration{
Preservation: pres.Config{TaskQueue: "a3m"},
Preprocessing: preprocessing.Config{
Enabled: true,
Extract: true,
SharedPath: "/tmp",
Temporal: preprocessing.Temporal{
Namespace: "default",
TaskQueue: "preprocessing",
WorkflowName: "preprocessing",
},
},
Poststorage: []poststorage.Config{
{
Namespace: "default",
TaskQueue: "poststorage",
WorkflowName: "poststorage_1",
},
{
Namespace: "default",
TaskQueue: "poststorage",
WorkflowName: "poststorage_2",
},
},
},
want: &goaabout.EnduroAbout{
Version: "",
PreservationSystem: "a3m",
Preprocessing: &goaabout.EnduroPreprocessing{
Enabled: true,
TaskQueue: "preprocessing",
WorkflowName: "preprocessing",
},
Poststorage: goaabout.EnduroPoststorageCollection{
&goaabout.EnduroPoststorage{
TaskQueue: "poststorage",
WorkflowName: "poststorage_1",
},
&goaabout.EnduroPoststorage{
TaskQueue: "poststorage",
WorkflowName: "poststorage_2",
},
},
},
},
} {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

srv := about.NewService(
logr.Discard(),
tt.config.Preservation.TaskQueue,
tt.config.Preprocessing,
tt.config.Poststorage,
&auth.NoopTokenVerifier{},
)
res, err := srv.About(context.Background(), &goaabout.AboutPayload{})
assert.NilError(t, err)
assert.DeepEqual(t, res, tt.want, cmpopts.IgnoreFields(goaabout.EnduroAbout{}, "Version"))
assert.Assert(t, versionRegExp.MatchString(res.Version))
})
}
}
10 changes: 10 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import (
goahttpmwr "goa.design/goa/v3/http/middleware"
"goa.design/goa/v3/middleware"

intabout "github.com/artefactual-sdps/enduro/internal/about"
"github.com/artefactual-sdps/enduro/internal/api/gen/about"
aaboutsvr "github.com/artefactual-sdps/enduro/internal/api/gen/http/about/server"
packagesvr "github.com/artefactual-sdps/enduro/internal/api/gen/http/package_/server"
storagesvr "github.com/artefactual-sdps/enduro/internal/api/gen/http/storage/server"
swaggersvr "github.com/artefactual-sdps/enduro/internal/api/gen/http/swagger/server"
Expand All @@ -43,6 +46,7 @@ func HTTPServer(
config *Config,
pkgsvc intpkg.Service,
storagesvc intstorage.Service,
aboutsvc *intabout.Service,
) *http.Server {
dec := goahttp.RequestDecoder
enc := goahttp.ResponseEncoder
Expand All @@ -66,6 +70,12 @@ func HTTPServer(
storageServer.Download = writeTimeout(intstorage.Download(storagesvc, mux, dec), 0)
storagesvr.Mount(mux, storageServer)

// About service.
aboutEndpoints := about.NewEndpoints(aboutsvc)
aboutErrorHandler := errorHandler(logger, "About error.")
aboutServer := aaboutsvr.New(aboutEndpoints, mux, dec, enc, aboutErrorHandler, nil)
aaboutsvr.Mount(mux, aboutServer)

Check warning on line 78 in internal/api/api.go

View check run for this annotation

Codecov / codecov/patch

internal/api/api.go#L73-L78

Added lines #L73 - L78 were not covered by tests
// Swagger service.
swaggerService := swaggersvr.New(nil, nil, nil, nil, nil, nil, http.FS(openAPIJSON))
swaggersvr.Mount(mux, swaggerService)
Expand Down
Loading

0 comments on commit dfad0eb

Please sign in to comment.