diff --git a/FEATURES.md b/FEATURES.md index b89e4b97..79d13ba2 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -76,3 +76,4 @@ Note that list is **not** in any specific order. - [ ] Notification system to push "job done" messages to. Ideally would be in a form/shape that allows sending a message to Rocket.Chat, Matrix, Telegram, Discord, email, webbrowser, push URL-encoded/JSON/XML to some URL, stuff like that. Idea by Dan McLaughlin. - [ ] Notification client inside Blender itself, so that you get a message when your job is done. +- [ ] Separate the OpenAPI definition of Shaman from the rest of Flamenco Manager. That way a part of BAT can also use the code generator. It also is the first step towards running Shaman as a standalone service. diff --git a/cmd/flamenco-manager/main.go b/cmd/flamenco-manager/main.go index d120f085..3b04ea4a 100644 --- a/cmd/flamenco-manager/main.go +++ b/cmd/flamenco-manager/main.go @@ -34,6 +34,7 @@ import ( "git.blender.org/flamenco/internal/own_url" "git.blender.org/flamenco/internal/upnp_ssdp" "git.blender.org/flamenco/pkg/api" + "git.blender.org/flamenco/pkg/shaman" ) var cliArgs struct { @@ -137,7 +138,8 @@ func buildFlamencoAPI(configService *config.Service, persist *persistence.DB) ap } logStorage := task_logs.NewStorage(configService.Get().TaskLogsPath) taskStateMachine := task_state_machine.NewStateMachine(persist) - flamenco := api_impl.NewFlamenco(compiler, persist, logStorage, configService, taskStateMachine) + shamanServer := shaman.NewServer(configService.Get().Shaman, nil) + flamenco := api_impl.NewFlamenco(compiler, persist, logStorage, configService, taskStateMachine, shamanServer) return flamenco } diff --git a/internal/manager/api_impl/api_impl.go b/internal/manager/api_impl/api_impl.go index d076d41c..82f7a306 100644 --- a/internal/manager/api_impl/api_impl.go +++ b/internal/manager/api_impl/api_impl.go @@ -14,6 +14,7 @@ import ( "git.blender.org/flamenco/internal/manager/persistence" "git.blender.org/flamenco/internal/manager/task_state_machine" "git.blender.org/flamenco/pkg/api" + "git.blender.org/flamenco/pkg/shaman" "github.com/labstack/echo/v4" "github.com/rs/zerolog" ) @@ -80,16 +81,19 @@ type ConfigService interface { } type Shaman interface { + // IsEnabled returns whether this Shaman service is enabled or not. + IsEnabled() bool + // Checkout creates a directory, and symlinks the required files into it. The // files must all have been uploaded to Shaman before calling this. - Checkout(ctx context.Context, checkoutID string, checkout api.ShamanCheckout) error + Checkout(ctx context.Context, checkout api.ShamanCheckout) error // Requirements checks a Shaman Requirements file, and returns the subset // containing the unknown files. Requirements(ctx context.Context, requirements api.ShamanRequirementsRequest) (api.ShamanRequirementsResponse, error) // Check the status of a file on the Shaman server. - FileStoreCheck(ctx context.Context, checksum string, filesize int64) (api.ShamanFileStatus, error) + FileStoreCheck(ctx context.Context, checksum string, filesize int64) api.ShamanFileStatus // Store a new file on the Shaman server. Note that the Shaman server can // return early when another client finishes uploading the exact same file, to @@ -97,6 +101,8 @@ type Shaman interface { FileStore(ctx context.Context, file io.ReadCloser, checksum string, filesize int64, canDefer bool, originalFilename string) error } +var _ Shaman = (*shaman.Server)(nil) + // NewFlamenco creates a new Flamenco service. func NewFlamenco( jc JobCompiler, @@ -104,6 +110,7 @@ func NewFlamenco( ls LogStorage, cs ConfigService, sm TaskStateMachine, + sha Shaman, ) *Flamenco { return &Flamenco{ jobCompiler: jc, @@ -111,6 +118,7 @@ func NewFlamenco( logStorage: ls, config: cs, stateMachine: sm, + shaman: sha, } } diff --git a/internal/manager/api_impl/mocks/api_impl_mock.gen.go b/internal/manager/api_impl/mocks/api_impl_mock.gen.go index 325ff785..c74a8b9f 100644 --- a/internal/manager/api_impl/mocks/api_impl_mock.gen.go +++ b/internal/manager/api_impl/mocks/api_impl_mock.gen.go @@ -412,17 +412,17 @@ func (m *MockShaman) EXPECT() *MockShamanMockRecorder { } // Checkout mocks base method. -func (m *MockShaman) Checkout(arg0 context.Context, arg1 string, arg2 api.ShamanCheckout) error { +func (m *MockShaman) Checkout(arg0 context.Context, arg1 api.ShamanCheckout) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Checkout", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "Checkout", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // Checkout indicates an expected call of Checkout. -func (mr *MockShamanMockRecorder) Checkout(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockShamanMockRecorder) Checkout(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Checkout", reflect.TypeOf((*MockShaman)(nil).Checkout), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Checkout", reflect.TypeOf((*MockShaman)(nil).Checkout), arg0, arg1) } // FileStore mocks base method. @@ -440,12 +440,11 @@ func (mr *MockShamanMockRecorder) FileStore(arg0, arg1, arg2, arg3, arg4, arg5 i } // FileStoreCheck mocks base method. -func (m *MockShaman) FileStoreCheck(arg0 context.Context, arg1 string, arg2 int64) (api.ShamanFileStatus, error) { +func (m *MockShaman) FileStoreCheck(arg0 context.Context, arg1 string, arg2 int64) api.ShamanFileStatus { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FileStoreCheck", arg0, arg1, arg2) ret0, _ := ret[0].(api.ShamanFileStatus) - ret1, _ := ret[1].(error) - return ret0, ret1 + return ret0 } // FileStoreCheck indicates an expected call of FileStoreCheck. @@ -454,6 +453,20 @@ func (mr *MockShamanMockRecorder) FileStoreCheck(arg0, arg1, arg2 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FileStoreCheck", reflect.TypeOf((*MockShaman)(nil).FileStoreCheck), arg0, arg1, arg2) } +// IsEnabled mocks base method. +func (m *MockShaman) IsEnabled() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsEnabled") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsEnabled indicates an expected call of IsEnabled. +func (mr *MockShamanMockRecorder) IsEnabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEnabled", reflect.TypeOf((*MockShaman)(nil).IsEnabled)) +} + // Requirements mocks base method. func (m *MockShaman) Requirements(arg0 context.Context, arg1 api.ShamanRequirementsRequest) (api.ShamanRequirementsResponse, error) { m.ctrl.T.Helper() diff --git a/internal/manager/api_impl/shaman.go b/internal/manager/api_impl/shaman.go index 6a3e83ab..41953a0a 100644 --- a/internal/manager/api_impl/shaman.go +++ b/internal/manager/api_impl/shaman.go @@ -11,12 +11,18 @@ import ( "git.blender.org/flamenco/pkg/shaman/fileserver" ) +func (f *Flamenco) isShamanEnabled() bool { + return f.shaman.IsEnabled() +} + // Create a directory, and symlink the required files into it. The files must all have been uploaded to Shaman before calling this endpoint. // (POST /shaman/checkout/create/{checkoutID}) -func (f *Flamenco) ShamanCheckout(e echo.Context, checkoutID string) error { - logger := requestLogger(e).With(). - Str("checkoutID", checkoutID). - Logger() +func (f *Flamenco) ShamanCheckout(e echo.Context) error { + logger := requestLogger(e) + if !f.isShamanEnabled() { + logger.Error().Msg("shaman server not active, unable to serve request") + return sendAPIError(e, http.StatusServiceUnavailable, "shaman server not active") + } var reqBody api.ShamanCheckoutJSONBody err := e.Bind(&reqBody) @@ -25,7 +31,7 @@ func (f *Flamenco) ShamanCheckout(e echo.Context, checkoutID string) error { return sendAPIError(e, http.StatusBadRequest, "invalid format") } - err = f.shaman.Checkout(e.Request().Context(), checkoutID, api.ShamanCheckout(reqBody)) + err = f.shaman.Checkout(e.Request().Context(), api.ShamanCheckout(reqBody)) if err != nil { // TODO: return 409 when checkout already exists. logger.Warn().Err(err).Msg("Shaman: creating checkout") @@ -39,6 +45,10 @@ func (f *Flamenco) ShamanCheckout(e echo.Context, checkoutID string) error { // (POST /shaman/checkout/requirements) func (f *Flamenco) ShamanCheckoutRequirements(e echo.Context) error { logger := requestLogger(e) + if !f.isShamanEnabled() { + logger.Error().Msg("shaman server not active, unable to serve request") + return sendAPIError(e, http.StatusServiceUnavailable, "shaman server not active") + } var reqBody api.ShamanCheckoutRequirementsJSONBody err := e.Bind(&reqBody) @@ -62,13 +72,14 @@ func (f *Flamenco) ShamanFileStoreCheck(e echo.Context, checksum string, filesiz logger := requestLogger(e).With(). Str("checksum", checksum).Int("filesize", filesize). Logger() - - status, err := f.shaman.FileStoreCheck(e.Request().Context(), checksum, int64(filesize)) - if err != nil { - logger.Warn().Err(err).Msg("Shaman: checking stored file") - return sendAPIError(e, http.StatusInternalServerError, "unexpected error: %v", err) + if !f.isShamanEnabled() { + logger.Error().Msg("shaman server not active, unable to serve request") + return sendAPIError(e, http.StatusServiceUnavailable, "shaman server not active") } + logger.Debug().Msg("shaman: checking file") + status := f.shaman.FileStoreCheck(e.Request().Context(), checksum, int64(filesize)) + // TODO: actually switch over the actual statuses, see the TODO in the Shaman interface. switch status { case api.ShamanFileStatusStored: @@ -94,6 +105,13 @@ func (f *Flamenco) ShamanFileStore(e echo.Context, checksum string, filesize int logCtx := requestLogger(e).With(). Str("checksum", checksum).Int("filesize", filesize) + + if !f.isShamanEnabled() { + logger := logCtx.Logger() + logger.Error().Msg("shaman server not active, unable to serve request") + return sendAPIError(e, http.StatusServiceUnavailable, "shaman server not active") + } + if params.XShamanCanDeferUpload != nil { canDefer = *params.XShamanCanDeferUpload logCtx = logCtx.Bool("canDefer", canDefer) diff --git a/internal/manager/api_impl/support_test.go b/internal/manager/api_impl/support_test.go index dabf4df5..eb928c12 100644 --- a/internal/manager/api_impl/support_test.go +++ b/internal/manager/api_impl/support_test.go @@ -25,6 +25,7 @@ type mockedFlamenco struct { logStorage *mocks.MockLogStorage config *mocks.MockConfigService stateMachine *mocks.MockTaskStateMachine + shaman *mocks.MockShaman } func newMockedFlamenco(mockCtrl *gomock.Controller) mockedFlamenco { @@ -33,7 +34,8 @@ func newMockedFlamenco(mockCtrl *gomock.Controller) mockedFlamenco { ls := mocks.NewMockLogStorage(mockCtrl) cs := mocks.NewMockConfigService(mockCtrl) sm := mocks.NewMockTaskStateMachine(mockCtrl) - f := NewFlamenco(jc, ps, ls, cs, sm) + sha := mocks.NewMockShaman(mockCtrl) + f := NewFlamenco(jc, ps, ls, cs, sm, sha) return mockedFlamenco{ flamenco: f, diff --git a/internal/manager/config/defaults.go b/internal/manager/config/defaults.go index c11ce894..29c8e295 100644 --- a/internal/manager/config/defaults.go +++ b/internal/manager/config/defaults.go @@ -1,5 +1,11 @@ package config +import ( + "time" + + shaman_config "git.blender.org/flamenco/pkg/shaman/config" +) + // SPDX-License-Identifier: GPL-3.0-or-later // The default configuration, use DefaultConfig() to obtain a copy. @@ -14,6 +20,17 @@ var defaultConfig = Conf{ TaskLogsPath: "./task-logs", SSDPDiscovery: true, + Shaman: shaman_config.Config{ + Enabled: true, + FileStorePath: "./shaman-file-storage/file-store", + CheckoutPath: "./shaman-file-storage/checkout", + GarbageCollect: shaman_config.GarbageCollect{ + Period: 24 * time.Hour, + MaxAge: 31 * 24 * time.Hour, + ExtraCheckoutDirs: []string{}, + }, + }, + // ActiveTaskTimeoutInterval: 10 * time.Minute, // ActiveWorkerTimeoutInterval: 1 * time.Minute, @@ -33,16 +50,6 @@ var defaultConfig = Conf{ // }, // }, - // Shaman: ShamanConfig{ - // Enabled: true, - // FileStorePath: defaultShamanFilestorePath, - // GarbageCollect: ShamanGarbageCollect{ - // Period: 24 * time.Hour, - // MaxAge: 31 * 24 * time.Hour, - // ExtraCheckoutDirs: []string{}, - // }, - // }, - // JWT: jwtauth.Config{ // DownloadKeysInterval: 1 * time.Hour, // }, diff --git a/internal/manager/config/settings.go b/internal/manager/config/settings.go index 2cb2b00c..c0441638 100644 --- a/internal/manager/config/settings.go +++ b/internal/manager/config/settings.go @@ -12,10 +12,12 @@ import ( "strings" "time" - "git.blender.org/flamenco/internal/appinfo" "github.com/rs/zerolog" "github.com/rs/zerolog/log" yaml "gopkg.in/yaml.v2" + + "git.blender.org/flamenco/internal/appinfo" + shaman_config "git.blender.org/flamenco/pkg/shaman/config" ) const ( @@ -25,9 +27,6 @@ const ( // // relative to the Flamenco Server Base URL: // jwtPublicKeysRelativeURL = "api/flamenco/jwt/public-keys" - - defaultShamanFilestorePath = "/shared/flamenco/file-store" - defaultJobStorage = "/shared/flamenco/jobs" ) var ( @@ -97,7 +96,7 @@ type Base struct { // TestTasks TestTasks `yaml:"test_tasks"` // Shaman configuration settings. - // Shaman ShamanConfig `yaml:"shaman"` + Shaman shaman_config.Config `yaml:"shaman"` // Authentication settings. // JWT jwtauth.Config `yaml:"user_authentication"` @@ -109,12 +108,6 @@ type Base struct { // Websetup *WebsetupConf `yaml:"websetup,omitempty"` } -type ShamanConfig struct { - Enabled bool `yaml:"enabled"` - FileStorePath string `yaml:"fileStorePath"` - GarbageCollect ShamanGarbageCollect `yaml:"garbageCollect"` -} - // GarbageCollect contains the config options for the GC. type ShamanGarbageCollect struct { // How frequently garbage collection is performed on the file store: diff --git a/internal/worker/mocks/client.gen.go b/internal/worker/mocks/client.gen.go index 4c311c5a..960e6f3c 100644 --- a/internal/worker/mocks/client.gen.go +++ b/internal/worker/mocks/client.gen.go @@ -197,10 +197,10 @@ func (mr *MockFlamencoClientMockRecorder) ShamanCheckoutRequirementsWithResponse } // ShamanCheckoutWithBodyWithResponse mocks base method. -func (m *MockFlamencoClient) ShamanCheckoutWithBodyWithResponse(arg0 context.Context, arg1, arg2 string, arg3 io.Reader, arg4 ...api.RequestEditorFn) (*api.ShamanCheckoutResponse, error) { +func (m *MockFlamencoClient) ShamanCheckoutWithBodyWithResponse(arg0 context.Context, arg1 string, arg2 io.Reader, arg3 ...api.RequestEditorFn) (*api.ShamanCheckoutResponse, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1, arg2, arg3} - for _, a := range arg4 { + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "ShamanCheckoutWithBodyWithResponse", varargs...) @@ -210,17 +210,17 @@ func (m *MockFlamencoClient) ShamanCheckoutWithBodyWithResponse(arg0 context.Con } // ShamanCheckoutWithBodyWithResponse indicates an expected call of ShamanCheckoutWithBodyWithResponse. -func (mr *MockFlamencoClientMockRecorder) ShamanCheckoutWithBodyWithResponse(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call { +func (mr *MockFlamencoClientMockRecorder) ShamanCheckoutWithBodyWithResponse(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...) + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShamanCheckoutWithBodyWithResponse", reflect.TypeOf((*MockFlamencoClient)(nil).ShamanCheckoutWithBodyWithResponse), varargs...) } // ShamanCheckoutWithResponse mocks base method. -func (m *MockFlamencoClient) ShamanCheckoutWithResponse(arg0 context.Context, arg1 string, arg2 api.ShamanCheckoutJSONRequestBody, arg3 ...api.RequestEditorFn) (*api.ShamanCheckoutResponse, error) { +func (m *MockFlamencoClient) ShamanCheckoutWithResponse(arg0 context.Context, arg1 api.ShamanCheckoutJSONRequestBody, arg2 ...api.RequestEditorFn) (*api.ShamanCheckoutResponse, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1, arg2} - for _, a := range arg3 { + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "ShamanCheckoutWithResponse", varargs...) @@ -230,9 +230,9 @@ func (m *MockFlamencoClient) ShamanCheckoutWithResponse(arg0 context.Context, ar } // ShamanCheckoutWithResponse indicates an expected call of ShamanCheckoutWithResponse. -func (mr *MockFlamencoClientMockRecorder) ShamanCheckoutWithResponse(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { +func (mr *MockFlamencoClientMockRecorder) ShamanCheckoutWithResponse(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + varargs := append([]interface{}{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShamanCheckoutWithResponse", reflect.TypeOf((*MockFlamencoClient)(nil).ShamanCheckoutWithResponse), varargs...) } diff --git a/pkg/api/flamenco-manager.yaml b/pkg/api/flamenco-manager.yaml index 5b2bddd5..e7acdf21 100644 --- a/pkg/api/flamenco-manager.yaml +++ b/pkg/api/flamenco-manager.yaml @@ -286,17 +286,12 @@ paths: schema: $ref: '#/components/schemas/Error' - /shaman/checkout/create/{checkoutID}: + /shaman/checkout/create: summary: Symlink a set of files into the checkout area. post: operationId: shamanCheckout summary: Create a directory, and symlink the required files into it. The files must all have been uploaded to Shaman before calling this endpoint. tags: [shaman] - parameters: - - name: checkoutID - in: path - required: true - schema: {type: string} requestBody: description: Set of files to check out. required: true @@ -749,7 +744,14 @@ components: "files": type: array items: {$ref: "#/components/schemas/ShamanFileSpecWithPath"} - required: [files] + "checkoutPath": + type: string + description: > + Path where the Manager should create this checkout, It is relative + to the Shaman checkout path as configured on the Manager. In older + versions of the Shaman this was just the "checkout ID", but in this + version it can be a path like `project-slug/scene-name/unique-ID`. + required: [files, checkoutPath] example: files: - sha: 35b0491c27b0333d1fb45fc0789a12ca06b1d640d2569780b807de504d7029e0 diff --git a/pkg/api/openapi_client.gen.go b/pkg/api/openapi_client.gen.go index afee6945..e1d8da4a 100644 --- a/pkg/api/openapi_client.gen.go +++ b/pkg/api/openapi_client.gen.go @@ -134,9 +134,9 @@ type ClientInterface interface { TaskUpdate(ctx context.Context, taskId string, body TaskUpdateJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) // ShamanCheckout request with any body - ShamanCheckoutWithBody(ctx context.Context, checkoutID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + ShamanCheckoutWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - ShamanCheckout(ctx context.Context, checkoutID string, body ShamanCheckoutJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + ShamanCheckout(ctx context.Context, body ShamanCheckoutJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) // ShamanCheckoutRequirements request with any body ShamanCheckoutRequirementsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -342,8 +342,8 @@ func (c *Client) TaskUpdate(ctx context.Context, taskId string, body TaskUpdateJ return c.Client.Do(req) } -func (c *Client) ShamanCheckoutWithBody(ctx context.Context, checkoutID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewShamanCheckoutRequestWithBody(c.Server, checkoutID, contentType, body) +func (c *Client) ShamanCheckoutWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewShamanCheckoutRequestWithBody(c.Server, contentType, body) if err != nil { return nil, err } @@ -354,8 +354,8 @@ func (c *Client) ShamanCheckoutWithBody(ctx context.Context, checkoutID string, return c.Client.Do(req) } -func (c *Client) ShamanCheckout(ctx context.Context, checkoutID string, body ShamanCheckoutJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewShamanCheckoutRequest(c.Server, checkoutID, body) +func (c *Client) ShamanCheckout(ctx context.Context, body ShamanCheckoutJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewShamanCheckoutRequest(c.Server, body) if err != nil { return nil, err } @@ -791,33 +791,26 @@ func NewTaskUpdateRequestWithBody(server string, taskId string, contentType stri } // NewShamanCheckoutRequest calls the generic ShamanCheckout builder with application/json body -func NewShamanCheckoutRequest(server string, checkoutID string, body ShamanCheckoutJSONRequestBody) (*http.Request, error) { +func NewShamanCheckoutRequest(server string, body ShamanCheckoutJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader buf, err := json.Marshal(body) if err != nil { return nil, err } bodyReader = bytes.NewReader(buf) - return NewShamanCheckoutRequestWithBody(server, checkoutID, "application/json", bodyReader) + return NewShamanCheckoutRequestWithBody(server, "application/json", bodyReader) } // NewShamanCheckoutRequestWithBody generates requests for ShamanCheckout with any type of body -func NewShamanCheckoutRequestWithBody(server string, checkoutID string, contentType string, body io.Reader) (*http.Request, error) { +func NewShamanCheckoutRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { var err error - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "checkoutID", runtime.ParamLocationPath, checkoutID) - if err != nil { - return nil, err - } - serverURL, err := url.Parse(server) if err != nil { return nil, err } - operationPath := fmt.Sprintf("/shaman/checkout/create/%s", pathParam0) + operationPath := fmt.Sprintf("/shaman/checkout/create") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -1070,9 +1063,9 @@ type ClientWithResponsesInterface interface { TaskUpdateWithResponse(ctx context.Context, taskId string, body TaskUpdateJSONRequestBody, reqEditors ...RequestEditorFn) (*TaskUpdateResponse, error) // ShamanCheckout request with any body - ShamanCheckoutWithBodyWithResponse(ctx context.Context, checkoutID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ShamanCheckoutResponse, error) + ShamanCheckoutWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ShamanCheckoutResponse, error) - ShamanCheckoutWithResponse(ctx context.Context, checkoutID string, body ShamanCheckoutJSONRequestBody, reqEditors ...RequestEditorFn) (*ShamanCheckoutResponse, error) + ShamanCheckoutWithResponse(ctx context.Context, body ShamanCheckoutJSONRequestBody, reqEditors ...RequestEditorFn) (*ShamanCheckoutResponse, error) // ShamanCheckoutRequirements request with any body ShamanCheckoutRequirementsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ShamanCheckoutRequirementsResponse, error) @@ -1568,16 +1561,16 @@ func (c *ClientWithResponses) TaskUpdateWithResponse(ctx context.Context, taskId } // ShamanCheckoutWithBodyWithResponse request with arbitrary body returning *ShamanCheckoutResponse -func (c *ClientWithResponses) ShamanCheckoutWithBodyWithResponse(ctx context.Context, checkoutID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ShamanCheckoutResponse, error) { - rsp, err := c.ShamanCheckoutWithBody(ctx, checkoutID, contentType, body, reqEditors...) +func (c *ClientWithResponses) ShamanCheckoutWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ShamanCheckoutResponse, error) { + rsp, err := c.ShamanCheckoutWithBody(ctx, contentType, body, reqEditors...) if err != nil { return nil, err } return ParseShamanCheckoutResponse(rsp) } -func (c *ClientWithResponses) ShamanCheckoutWithResponse(ctx context.Context, checkoutID string, body ShamanCheckoutJSONRequestBody, reqEditors ...RequestEditorFn) (*ShamanCheckoutResponse, error) { - rsp, err := c.ShamanCheckout(ctx, checkoutID, body, reqEditors...) +func (c *ClientWithResponses) ShamanCheckoutWithResponse(ctx context.Context, body ShamanCheckoutJSONRequestBody, reqEditors ...RequestEditorFn) (*ShamanCheckoutResponse, error) { + rsp, err := c.ShamanCheckout(ctx, body, reqEditors...) if err != nil { return nil, err } diff --git a/pkg/api/openapi_server.gen.go b/pkg/api/openapi_server.gen.go index d42a4c96..6a86f808 100644 --- a/pkg/api/openapi_server.gen.go +++ b/pkg/api/openapi_server.gen.go @@ -47,8 +47,8 @@ type ServerInterface interface { // (POST /api/worker/task/{task_id}) TaskUpdate(ctx echo.Context, taskId string) error // Create a directory, and symlink the required files into it. The files must all have been uploaded to Shaman before calling this endpoint. - // (POST /shaman/checkout/create/{checkoutID}) - ShamanCheckout(ctx echo.Context, checkoutID string) error + // (POST /shaman/checkout/create) + ShamanCheckout(ctx echo.Context) error // Checks a Shaman Requirements file, and reports which files are unknown. // (POST /shaman/checkout/requirements) ShamanCheckoutRequirements(ctx echo.Context) error @@ -193,16 +193,9 @@ func (w *ServerInterfaceWrapper) TaskUpdate(ctx echo.Context) error { // ShamanCheckout converts echo context to params. func (w *ServerInterfaceWrapper) ShamanCheckout(ctx echo.Context) error { var err error - // ------------- Path parameter "checkoutID" ------------- - var checkoutID string - - err = runtime.BindStyledParameterWithLocation("simple", false, "checkoutID", runtime.ParamLocationPath, ctx.Param("checkoutID"), &checkoutID) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter checkoutID: %s", err)) - } // Invoke the callback with all the unmarshalled arguments - err = w.Handler.ShamanCheckout(ctx, checkoutID) + err = w.Handler.ShamanCheckout(ctx) return err } @@ -337,7 +330,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.POST(baseURL+"/api/worker/state-changed", wrapper.WorkerStateChanged) router.POST(baseURL+"/api/worker/task", wrapper.ScheduleTask) router.POST(baseURL+"/api/worker/task/:task_id", wrapper.TaskUpdate) - router.POST(baseURL+"/shaman/checkout/create/:checkoutID", wrapper.ShamanCheckout) + router.POST(baseURL+"/shaman/checkout/create", wrapper.ShamanCheckout) router.POST(baseURL+"/shaman/checkout/requirements", wrapper.ShamanCheckoutRequirements) router.OPTIONS(baseURL+"/shaman/files/:checksum/:filesize", wrapper.ShamanFileStoreCheck) router.POST(baseURL+"/shaman/files/:checksum/:filesize", wrapper.ShamanFileStore) diff --git a/pkg/api/openapi_spec.gen.go b/pkg/api/openapi_spec.gen.go index 33798a93..db12af9d 100644 --- a/pkg/api/openapi_spec.gen.go +++ b/pkg/api/openapi_spec.gen.go @@ -18,90 +18,92 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "4NAxUwAA", + "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", + "AP//RfyB4BlUAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/api/openapi_types.gen.go b/pkg/api/openapi_types.gen.go index f00ead26..fbc49818 100644 --- a/pkg/api/openapi_types.gen.go +++ b/pkg/api/openapi_types.gen.go @@ -244,7 +244,9 @@ type SecurityError struct { // Set of files with their SHA256 checksum, size in bytes, and desired location in the checkout directory. type ShamanCheckout struct { - Files []ShamanFileSpecWithPath `json:"files"` + // Path where the Manager should create this checkout, It is relative to the Shaman checkout path as configured on the Manager. In older versions of the Shaman this was just the "checkout ID", but in this version it can be a path like `project-slug/scene-name/unique-ID`. + CheckoutPath string `json:"checkoutPath"` + Files []ShamanFileSpecWithPath `json:"files"` } // Specification of a file in the Shaman storage. diff --git a/pkg/shaman/checkout/checkout.go b/pkg/shaman/checkout/checkout.go index bf861803..ad14dcba 100644 --- a/pkg/shaman/checkout/checkout.go +++ b/pkg/shaman/checkout/checkout.go @@ -16,12 +16,13 @@ var ( ErrMissingFiles = errors.New("unknown files requested in checkout") ) -func (m *Manager) Checkout(ctx context.Context, checkoutID string, checkout api.ShamanCheckout) error { - logger := *zerolog.Ctx(ctx) +func (m *Manager) Checkout(ctx context.Context, checkout api.ShamanCheckout) error { + logger := (*zerolog.Ctx(ctx)).With(). + Str("checkoutPath", checkout.CheckoutPath).Logger() logger.Debug().Msg("shaman: user requested checkout creation") // Actually create the checkout. - resolvedCheckoutInfo, err := m.PrepareCheckout(checkoutID) + resolvedCheckoutInfo, err := m.PrepareCheckout(checkout.CheckoutPath) if err != nil { return err } @@ -30,7 +31,10 @@ func (m *Manager) Checkout(ctx context.Context, checkoutID string, checkout api. var checkoutOK bool defer func() { if !checkoutOK { - m.EraseCheckout(checkoutID) + err := m.EraseCheckout(checkout.CheckoutPath) + if err != nil { + logger.Error().Err(err).Msg("shaman: error erasing checkout directory") + } } }() diff --git a/pkg/shaman/checkout/checkout_id.go b/pkg/shaman/checkout/checkout_id.go index eea85e2c..fc91c8fd 100644 --- a/pkg/shaman/checkout/checkout_id.go +++ b/pkg/shaman/checkout/checkout_id.go @@ -24,8 +24,8 @@ package checkout import "regexp" -var validCheckoutRegexp = regexp.MustCompile("^[a-zA-Z0-9_]+$") +var validCheckoutRegexp = regexp.MustCompile("^[a-zA-Z0-9_ /]+$") -func isValidCheckoutID(checkoutID string) bool { +func isValidCheckoutPath(checkoutID string) bool { return validCheckoutRegexp.MatchString(checkoutID) } diff --git a/pkg/shaman/checkout/manager.go b/pkg/shaman/checkout/manager.go index 5a4f58d1..fbe68aac 100644 --- a/pkg/shaman/checkout/manager.go +++ b/pkg/shaman/checkout/manager.go @@ -40,7 +40,7 @@ import ( // Manager creates checkouts and provides info about missing files. type Manager struct { checkoutBasePath string - fileStore filestore.Storage + fileStore *filestore.Store wg sync.WaitGroup } @@ -49,8 +49,8 @@ type Manager struct { type ResolvedCheckoutInfo struct { // The absolute path on our filesystem. absolutePath string - // The path relative to the Manager.checkoutBasePath. This is what is - // sent back to the client. + // The path relative to the Manager.checkoutBasePath. This is what was + // received from the client. RelativePath string } @@ -61,7 +61,7 @@ var ( ) // NewManager creates and returns a new Checkout Manager. -func NewManager(conf config.Config, fileStore filestore.Storage) *Manager { +func NewManager(conf config.Config, fileStore *filestore.Store) *Manager { logger := log.With().Str("checkoutDir", conf.CheckoutPath).Logger() logger.Info().Msg("opening checkout directory") @@ -79,36 +79,28 @@ func (m *Manager) Close() { m.wg.Wait() } -func (m *Manager) pathForCheckoutID(checkoutID string) (ResolvedCheckoutInfo, error) { - if !isValidCheckoutID(checkoutID) { +func (m *Manager) pathForCheckout(requestedCheckoutPath string) (ResolvedCheckoutInfo, error) { + if !isValidCheckoutPath(requestedCheckoutPath) { return ResolvedCheckoutInfo{}, ErrInvalidCheckoutID } - // When changing the number of path components the checkout ID is turned into, - // be sure to also update the EraseCheckout() function for this. - - // We're expecting ObjectIDs as checkoutIDs, which means most variation - // is in the last characters. - lastBitIndex := len(checkoutID) - 2 - relativePath := path.Join(checkoutID[lastBitIndex:], checkoutID) - return ResolvedCheckoutInfo{ - absolutePath: path.Join(m.checkoutBasePath, relativePath), - RelativePath: relativePath, + absolutePath: filepath.Join(m.checkoutBasePath, requestedCheckoutPath), + RelativePath: requestedCheckoutPath, }, nil } // PrepareCheckout creates the root directory for a specific checkout. // Returns the path relative to the checkout root directory. -func (m *Manager) PrepareCheckout(checkoutID string) (ResolvedCheckoutInfo, error) { - checkoutPaths, err := m.pathForCheckoutID(checkoutID) +func (m *Manager) PrepareCheckout(checkoutPath string) (ResolvedCheckoutInfo, error) { + checkoutPaths, err := m.pathForCheckout(checkoutPath) if err != nil { return ResolvedCheckoutInfo{}, err } logger := log.With(). - Str("checkoutPath", checkoutPaths.absolutePath). - Str("checkoutID", checkoutID). + Str("absolutePath", checkoutPaths.absolutePath). + Str("checkoutPath", checkoutPath). Logger() if stat, err := os.Stat(checkoutPaths.absolutePath); !os.IsNotExist(err) { @@ -136,7 +128,7 @@ func (m *Manager) PrepareCheckout(checkoutID string) (ResolvedCheckoutInfo, erro // EraseCheckout removes the checkout directory structure identified by the ID. func (m *Manager) EraseCheckout(checkoutID string) error { - checkoutPaths, err := m.pathForCheckoutID(checkoutID) + checkoutPaths, err := m.pathForCheckout(checkoutID) if err != nil { return err } diff --git a/pkg/shaman/fileserver/fileserver.go b/pkg/shaman/fileserver/fileserver.go index ad2d9887..18319eca 100644 --- a/pkg/shaman/fileserver/fileserver.go +++ b/pkg/shaman/fileserver/fileserver.go @@ -33,7 +33,7 @@ type receiverChannel chan struct{} // FileServer deals with receiving and serving of uploaded files. type FileServer struct { - fileStore filestore.Storage + fileStore *filestore.Store receiverMutex sync.Mutex receiverChannels map[string]receiverChannel @@ -44,7 +44,7 @@ type FileServer struct { } // New creates a new File Server and starts a monitoring goroutine. -func New(fileStore filestore.Storage) *FileServer { +func New(fileStore *filestore.Store) *FileServer { ctx, ctxCancel := context.WithCancel(context.Background()) fs := &FileServer{ diff --git a/pkg/shaman/fileserver/receivefile.go b/pkg/shaman/fileserver/receivefile.go index bdd08524..a80da3d1 100644 --- a/pkg/shaman/fileserver/receivefile.go +++ b/pkg/shaman/fileserver/receivefile.go @@ -65,20 +65,25 @@ func (fs *FileServer) ReceiveFile( defer bodyReader.Close() localPath, status := fs.fileStore.ResolveFile(checksum, filesize, filestore.ResolveEverything) - logger = logger.With().Str("path", localPath).Logger() + logger = logger.With(). + Str("path", localPath). + Str("checksum", checksum). + Int64("filesize", filesize). + Str("status", status.String()). + Logger() switch status { case filestore.StatusStored: - logger.Info().Msg("uploaded file already exists") + logger.Info().Msg("shaman: uploaded file already exists") return ErrFileAlreadyExists case filestore.StatusUploading: if canDefer { - logger.Info().Msg("someone is uploading this file and client can defer") + logger.Info().Msg("shaman: someone is uploading this file and client can defer") return ErrFileAlreadyExists } } - logger.Info().Msg("receiving file") + logger.Info().Msg("shaman: receiving file") streamTo, err := fs.fileStore.OpenForUpload(checksum, filesize) if err != nil { diff --git a/pkg/shaman/filestore/interface.go b/pkg/shaman/filestore/interface.go index ab9e2a6e..a1e71bc9 100644 --- a/pkg/shaman/filestore/interface.go +++ b/pkg/shaman/filestore/interface.go @@ -25,36 +25,8 @@ package filestore import ( "errors" "fmt" - "os" ) -// Storage is the interface for Shaman file stores. -type Storage interface { - // ResolveFile checks the status of the file in the store and returns the actual path. - ResolveFile(checksum string, filesize int64, storedOnly StoredOnly) (string, FileStatus) - - // OpenForUpload returns a file pointer suitable to stream an uploaded file to. - OpenForUpload(checksum string, filesize int64) (*os.File, error) - - // BasePath returns the directory path of the storage. - // This is the directory containing the 'stored' and 'uploading' directories. - BasePath() string - - // StoragePath returns the directory path of the 'stored' storage bin. - StoragePath() string - - // MoveToStored moves a file from 'uploading' storage to the actual 'stored' storage. - MoveToStored(checksum string, filesize int64, uploadedFilePath string) error - - // RemoveUploadedFile removes a file from the 'uploading' storage. - // This is intended to clean up files for which upload was aborted for some reason. - RemoveUploadedFile(filePath string) - - // RemoveStoredFile removes a file from the 'stored' storage bin. - // This is intended to garbage collect old, unused files. - RemoveStoredFile(filePath string) error -} - // FileStatus represents the status of a file in the store. type FileStatus int diff --git a/pkg/shaman/server.go b/pkg/shaman/server.go index 7612bd0c..95c60d8c 100644 --- a/pkg/shaman/server.go +++ b/pkg/shaman/server.go @@ -41,7 +41,7 @@ type Server struct { config config.Config auther jwtauth.Authenticator - fileStore filestore.Storage + fileStore *filestore.Store fileServer *fileserver.FileServer checkoutMan *checkout.Manager @@ -52,7 +52,12 @@ type Server struct { // NewServer creates a new Shaman server. func NewServer(conf config.Config, auther jwtauth.Authenticator) *Server { if !conf.Enabled { - log.Info().Msg("Shaman server is disabled") + log.Info().Msg("shaman server is disabled") + return nil + } + + if conf.CheckoutPath == "" { + log.Error().Interface("config", conf).Msg("shaman: no checkout path configured, unable to start") return nil } @@ -101,10 +106,14 @@ func (s *Server) Close() { s.wg.Wait() } +func (s *Server) IsEnabled() bool { + return s != nil && s.config.Enabled +} + // Checkout creates a directory, and symlinks the required files into it. The // files must all have been uploaded to Shaman before calling this. -func (s *Server) Checkout(ctx context.Context, checkoutID string, checkout api.ShamanCheckout) error { - return s.checkoutMan.Checkout(ctx, checkoutID, checkout) +func (s *Server) Checkout(ctx context.Context, checkout api.ShamanCheckout) error { + return s.checkoutMan.Checkout(ctx, checkout) } // Requirements checks a Shaman Requirements file, and returns the subset