Manager: More work on Shaman support

This introduces some more conceptual changes to Shaman. The most important
one is that there is no longer a "checkout ID", but a "checkout path".
The Shaman client can request any subpath of the checkout directory,
so that it can handle things like project- or scene-specific prefixes.
This commit is contained in:
Sybren A. Stüvel 2022-03-22 18:11:14 +01:00
parent 2b0d154a07
commit b2288e7f28
21 changed files with 261 additions and 243 deletions

View File

@ -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.

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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()

View File

@ -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)

View File

@ -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,

View File

@ -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,
// },

View File

@ -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:

View File

@ -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...)
}

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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")
}
}
}()

View File

@ -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)
}

View File

@ -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
}

View File

@ -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{

View File

@ -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 {

View File

@ -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

View File

@ -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