mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-03 10:37:35 +01:00
* s3tables: bind table bucket policy writes to the authorized bucket PutTableBucketPolicy compared only the policy bytes, and DeleteTableBucketPolicy removed the policy unconditionally. A bucket deleted and created again under the same name between authorization and the write took a policy change nobody authorized for it. Both writes now require the bucket's CreatedAt and the policy they were authorized against, answer 409 otherwise and 404 for a bucket that is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3tables: bind bucket tag writes to the authorized bucket TagResource and UntagResource checked the creation time only for a table ARN. On a bucket ARN the merged tags landed on a bucket deleted and created again since authorization. extractResourceOwnerAndBucket now returns the resource's CreatedAt for both kinds, and the write requires it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3tables: bind bucket maintenance writes to the authorized bucket PutTableBucketMaintenanceConfiguration wrote onto whatever bucket held the name at write time. authorizeMaintenanceBucket now returns the bucket's CreatedAt like authorizeMaintenanceTable, and putMaintenanceConfiguration takes the check and creation time from both callers instead of an optional closure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3vectors: bind vector bucket policy writes to the authorized bucket Put/DeleteVectorBucketPolicy authorized inside the compare-and-swap with the owner from an earlier bucket read, so a bucket deleted and created again by another account in between had its policy rewritten by the old owner. The mutation now requires the entry's createdAt to match the record it authorized against. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3vectors: bind DeleteVectorBucket to the authorized bucket DeleteVectorBucket authorized one bucket record, then ran the trusted catalog DeleteTableBucket on whatever bucket held the name by then, so an empty bucket deleted and created again by another account in between was deleted by the old owner. The catalog delete now accepts the authorized bucket's createdAt through an in-process context value and treats any other bucket as gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3vectors: bind CreateIndex and DeleteIndex to the authorized bucket Both authorized the bucket, then ran trusted CreateNamespace, CreateTable or DeleteTable on whatever bucket held the name by then, so an index could be created in, or deleted from, a bucket another account created again in between. Those catalog operations now honor the bucket binding DeleteTableBucket already takes, and the vector handlers pass the authorized bucket's createdAt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
560 lines
21 KiB
Go
560 lines
21 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
)
|
|
|
|
// Maintenance types are scoped: AWS configures unreferenced file removal on the
|
|
// table bucket and compaction and snapshot management on the table.
|
|
var (
|
|
bucketMaintenanceTypes = map[string]bool{
|
|
MaintenanceTypeIcebergUnreferencedFileRemoval: true,
|
|
}
|
|
tableMaintenanceTypes = map[string]bool{
|
|
MaintenanceTypeIcebergCompaction: true,
|
|
MaintenanceTypeIcebergSnapshotManagement: true,
|
|
}
|
|
)
|
|
|
|
// handlePutTableBucketMaintenanceConfiguration sets a maintenance configuration on a table bucket
|
|
func (h *S3TablesHandler) handlePutTableBucketMaintenanceConfiguration(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {
|
|
var req PutTableBucketMaintenanceConfigurationRequest
|
|
if err := h.readRequestBody(r, &req); err != nil {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
|
return err
|
|
}
|
|
|
|
if req.TableBucketARN == "" {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN is required")
|
|
return fmt.Errorf("tableBucketARN is required")
|
|
}
|
|
|
|
bucketName, err := parseBucketNameFromARN(req.TableBucketARN)
|
|
if err != nil {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
|
return err
|
|
}
|
|
|
|
if err := validateMaintenanceValue(req.Type, bucketMaintenanceTypes, req.Value); err != nil {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
|
return err
|
|
}
|
|
|
|
_, createdAt, err := h.authorizeMaintenanceBucket(r, filerClient, "PutTableBucketMaintenanceConfiguration", bucketName)
|
|
if err != nil {
|
|
h.writeMaintenanceError(w, err, ErrCodeNoSuchBucket, fmt.Sprintf("table bucket %s not found", bucketName))
|
|
return err
|
|
}
|
|
|
|
if err := h.putMaintenanceConfiguration(r, filerClient, GetTableBucketPath(bucketName), req.Type, req.Value, requireTableBucketCreatedAt, createdAt); err != nil {
|
|
h.writeMaintenanceError(w, err, ErrCodeNoSuchBucket, fmt.Sprintf("table bucket %s not found", bucketName))
|
|
return err
|
|
}
|
|
|
|
h.writeJSON(w, http.StatusOK, nil)
|
|
return nil
|
|
}
|
|
|
|
// handleGetTableBucketMaintenanceConfiguration gets the maintenance configuration of a table bucket
|
|
func (h *S3TablesHandler) handleGetTableBucketMaintenanceConfiguration(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {
|
|
var req GetTableBucketMaintenanceConfigurationRequest
|
|
if err := h.readRequestBody(r, &req); err != nil {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
|
return err
|
|
}
|
|
|
|
if req.TableBucketARN == "" {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN is required")
|
|
return fmt.Errorf("tableBucketARN is required")
|
|
}
|
|
|
|
bucketName, err := parseBucketNameFromARN(req.TableBucketARN)
|
|
if err != nil {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
|
return err
|
|
}
|
|
|
|
bucketARN, _, err := h.authorizeMaintenanceBucket(r, filerClient, "GetTableBucketMaintenanceConfiguration", bucketName)
|
|
if err != nil {
|
|
h.writeMaintenanceError(w, err, ErrCodeNoSuchBucket, fmt.Sprintf("table bucket %s not found", bucketName))
|
|
return err
|
|
}
|
|
|
|
config, err := h.readMaintenanceConfiguration(r, filerClient, GetTableBucketPath(bucketName))
|
|
if err != nil {
|
|
h.writeMaintenanceError(w, err, ErrCodeNoSuchBucket, fmt.Sprintf("table bucket %s not found", bucketName))
|
|
return err
|
|
}
|
|
|
|
h.writeJSON(w, http.StatusOK, &GetTableBucketMaintenanceConfigurationResponse{
|
|
TableBucketARN: bucketARN,
|
|
Configuration: config,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// handlePutTableMaintenanceConfiguration sets a maintenance configuration on a table
|
|
func (h *S3TablesHandler) handlePutTableMaintenanceConfiguration(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {
|
|
var req PutTableMaintenanceConfigurationRequest
|
|
if err := h.readRequestBody(r, &req); err != nil {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
|
return err
|
|
}
|
|
|
|
bucketName, namespaceName, tableName, err := h.parseTableTarget(req.TableBucketARN, req.Namespace, req.Name)
|
|
if err != nil {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
|
return err
|
|
}
|
|
|
|
if err := validateMaintenanceValue(req.Type, tableMaintenanceTypes, req.Value); err != nil {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
|
return err
|
|
}
|
|
|
|
_, createdAt, err := h.authorizeMaintenanceTable(r, filerClient, "PutTableMaintenanceConfiguration", bucketName, namespaceName, tableName)
|
|
if err != nil {
|
|
h.writeMaintenanceError(w, err, ErrCodeNoSuchTable, fmt.Sprintf("table %s not found", tableName))
|
|
return err
|
|
}
|
|
|
|
tablePath := GetTablePath(bucketName, namespaceName, tableName)
|
|
if err := h.putMaintenanceConfiguration(r, filerClient, tablePath, req.Type, req.Value, requireTableCreatedAt, createdAt); err != nil {
|
|
h.writeMaintenanceError(w, err, ErrCodeNoSuchTable, fmt.Sprintf("table %s not found", tableName))
|
|
return err
|
|
}
|
|
|
|
h.writeJSON(w, http.StatusOK, nil)
|
|
return nil
|
|
}
|
|
|
|
// handleGetTableMaintenanceConfiguration gets the maintenance configuration of a table
|
|
func (h *S3TablesHandler) handleGetTableMaintenanceConfiguration(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {
|
|
var req GetTableMaintenanceConfigurationRequest
|
|
if err := h.readRequestBody(r, &req); err != nil {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
|
return err
|
|
}
|
|
|
|
bucketName, namespaceName, tableName, err := h.parseTableTarget(req.TableBucketARN, req.Namespace, req.Name)
|
|
if err != nil {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
|
return err
|
|
}
|
|
|
|
tableARN, _, err := h.authorizeMaintenanceTable(r, filerClient, "GetTableMaintenanceConfiguration", bucketName, namespaceName, tableName)
|
|
if err != nil {
|
|
h.writeMaintenanceError(w, err, ErrCodeNoSuchTable, fmt.Sprintf("table %s not found", tableName))
|
|
return err
|
|
}
|
|
|
|
tablePath := GetTablePath(bucketName, namespaceName, tableName)
|
|
config, err := h.readMaintenanceConfiguration(r, filerClient, tablePath)
|
|
if err != nil {
|
|
h.writeMaintenanceError(w, err, ErrCodeNoSuchTable, fmt.Sprintf("table %s not found", tableName))
|
|
return err
|
|
}
|
|
|
|
h.writeJSON(w, http.StatusOK, &GetTableMaintenanceConfigurationResponse{
|
|
TableARN: tableARN,
|
|
Namespace: []string{namespaceName},
|
|
Name: tableName,
|
|
Configuration: config,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// handleGetTableMaintenanceJobStatus reports the outcome of the last maintenance run on a table
|
|
func (h *S3TablesHandler) handleGetTableMaintenanceJobStatus(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {
|
|
var req GetTableMaintenanceJobStatusRequest
|
|
if err := h.readRequestBody(r, &req); err != nil {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
|
return err
|
|
}
|
|
|
|
bucketName, namespaceName, tableName, err := h.parseTableTarget(req.TableBucketARN, req.Namespace, req.Name)
|
|
if err != nil {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
|
return err
|
|
}
|
|
|
|
tableARN, _, err := h.authorizeMaintenanceTable(r, filerClient, "GetTableMaintenanceJobStatus", bucketName, namespaceName, tableName)
|
|
if err != nil {
|
|
h.writeMaintenanceError(w, err, ErrCodeNoSuchTable, fmt.Sprintf("table %s not found", tableName))
|
|
return err
|
|
}
|
|
|
|
tablePath := GetTablePath(bucketName, namespaceName, tableName)
|
|
|
|
// Unreferenced file removal is configured on the bucket, so a bucket-level
|
|
// disable has to show up here as Disabled rather than a stale table status.
|
|
bucketConfig, err := h.readMaintenanceConfiguration(r, filerClient, GetTableBucketPath(bucketName))
|
|
if err != nil {
|
|
h.writeMaintenanceError(w, err, ErrCodeNoSuchBucket, fmt.Sprintf("table bucket %s not found", bucketName))
|
|
return err
|
|
}
|
|
tableConfig, err := h.readMaintenanceConfiguration(r, filerClient, tablePath)
|
|
if err != nil {
|
|
h.writeMaintenanceError(w, err, ErrCodeNoSuchTable, fmt.Sprintf("table %s not found", tableName))
|
|
return err
|
|
}
|
|
config := MergeMaintenanceConfiguration(bucketConfig, tableConfig)
|
|
|
|
recorded := MaintenanceJobStatus{}
|
|
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
data, err := h.getExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyMaintenanceStatus)
|
|
if err != nil {
|
|
if errors.Is(err, ErrAttributeNotFound) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
return json.Unmarshal(data, &recorded)
|
|
})
|
|
if err != nil {
|
|
h.writeMaintenanceError(w, err, ErrCodeNoSuchTable, fmt.Sprintf("table %s not found", tableName))
|
|
return err
|
|
}
|
|
|
|
h.writeJSON(w, http.StatusOK, &GetTableMaintenanceJobStatusResponse{
|
|
TableARN: tableARN,
|
|
Status: buildJobStatusResponse(recorded, config),
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// buildJobStatusResponse reports every job type: what the worker recorded, or
|
|
// Disabled when the configuration switched the type off, or Not_Yet_Run.
|
|
func buildJobStatusResponse(recorded MaintenanceJobStatus, config MaintenanceConfiguration) MaintenanceJobStatus {
|
|
jobTypes := []string{
|
|
MaintenanceTypeIcebergCompaction,
|
|
MaintenanceTypeIcebergSnapshotManagement,
|
|
MaintenanceTypeIcebergUnreferencedFileRemoval,
|
|
}
|
|
|
|
status := make(MaintenanceJobStatus, len(jobTypes))
|
|
for _, jobType := range jobTypes {
|
|
if value, ok := config[jobType]; ok && value != nil && value.Status == MaintenanceStatusDisabled {
|
|
status[jobType] = &MaintenanceJobStatusValue{Status: MaintenanceJobStatusDisabled}
|
|
continue
|
|
}
|
|
if value, ok := recorded[jobType]; ok && value != nil {
|
|
status[jobType] = value
|
|
continue
|
|
}
|
|
status[jobType] = &MaintenanceJobStatusValue{Status: MaintenanceJobStatusNotYetRun}
|
|
}
|
|
return status
|
|
}
|
|
|
|
// putMaintenanceConfiguration merges one maintenance type into the stored
|
|
// configuration, leaving the other types alone. The write checks the catalog
|
|
// identity in the same conditional mutation, so it cannot land on a name a
|
|
// concurrent rename or delete has already soft-deleted. sameRecord refuses a
|
|
// record other than the one created at createdAt, which the caller was
|
|
// authorized against.
|
|
func (h *S3TablesHandler) putMaintenanceConfiguration(
|
|
r *http.Request,
|
|
filerClient FilerClient,
|
|
resourcePath, maintenanceType string,
|
|
value *MaintenanceConfigurationValue,
|
|
sameRecord func(extended map[string][]byte, path string, createdAt time.Time) error,
|
|
createdAt time.Time,
|
|
) error {
|
|
return filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
return h.mutateCatalogRecord(r.Context(), client, resourcePath, func(extended map[string][]byte) error {
|
|
if len(extended[ExtendedKeyMetadata]) == 0 {
|
|
return fmt.Errorf("%w: %s", filer_pb.ErrNotFound, resourcePath)
|
|
}
|
|
if err := sameRecord(extended, resourcePath, createdAt); err != nil {
|
|
return err
|
|
}
|
|
|
|
config := MaintenanceConfiguration{}
|
|
if current := extended[ExtendedKeyMaintenance]; len(current) > 0 {
|
|
if err := json.Unmarshal(current, &config); err != nil {
|
|
return fmt.Errorf("failed to unmarshal maintenance configuration: %w", err)
|
|
}
|
|
}
|
|
config[maintenanceType] = value
|
|
|
|
data, err := json.Marshal(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
extended[ExtendedKeyMaintenance] = data
|
|
return nil
|
|
})
|
|
})
|
|
}
|
|
|
|
// readMaintenanceConfiguration returns the stored configuration, or an empty
|
|
// one when the resource has never been configured.
|
|
func (h *S3TablesHandler) readMaintenanceConfiguration(r *http.Request, filerClient FilerClient, resourcePath string) (MaintenanceConfiguration, error) {
|
|
config := MaintenanceConfiguration{}
|
|
|
|
err := filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
data, err := h.getExtendedAttribute(r.Context(), client, resourcePath, ExtendedKeyMaintenance)
|
|
if err != nil {
|
|
if errors.Is(err, ErrAttributeNotFound) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
return json.Unmarshal(data, &config)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// parseTableTarget validates the bucket/namespace/table triple every
|
|
// table-scoped maintenance request carries.
|
|
func (h *S3TablesHandler) parseTableTarget(tableBucketARN string, namespace []string, name string) (bucketName, namespaceName, tableName string, err error) {
|
|
if tableBucketARN == "" || len(namespace) == 0 || name == "" {
|
|
return "", "", "", fmt.Errorf("tableBucketARN, namespace, and name are required")
|
|
}
|
|
if namespaceName, err = validateNamespace(namespace); err != nil {
|
|
return "", "", "", err
|
|
}
|
|
if bucketName, err = parseBucketNameFromARN(tableBucketARN); err != nil {
|
|
return "", "", "", err
|
|
}
|
|
if tableName, err = validateTableName(name); err != nil {
|
|
return "", "", "", err
|
|
}
|
|
return bucketName, namespaceName, tableName, nil
|
|
}
|
|
|
|
// authorizeMaintenanceBucket checks the caller may perform operation on the
|
|
// table bucket, returning the bucket ARN and the creation time of the bucket
|
|
// record the check read.
|
|
func (h *S3TablesHandler) authorizeMaintenanceBucket(r *http.Request, filerClient FilerClient, operation, bucketName string) (string, time.Time, error) {
|
|
bucketPath := GetTableBucketPath(bucketName)
|
|
|
|
var bucketMetadata tableBucketMetadata
|
|
var bucketPolicy string
|
|
err := filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
data, err := h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := json.Unmarshal(data, &bucketMetadata); err != nil {
|
|
return fmt.Errorf("failed to unmarshal bucket metadata: %w", err)
|
|
}
|
|
bucketPolicy, err = h.readPolicy(r, client, bucketPath)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return "", time.Time{}, err
|
|
}
|
|
|
|
bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
|
|
principal := h.getAccountID(r)
|
|
if !CheckPermissionWithContext(operation, principal, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
|
TrustedIssuers: h.trustedIssuers(r),
|
|
IsLocalAccountID: h.localAccountIDs,
|
|
TableBucketName: bucketName,
|
|
IdentityActions: getIdentityActions(r),
|
|
DefaultAllow: h.defaultAllowFor(r),
|
|
}) {
|
|
return "", time.Time{}, NewAuthError(operation, principal, "not authorized to "+operation)
|
|
}
|
|
|
|
return bucketARN, bucketMetadata.CreatedAt, nil
|
|
}
|
|
|
|
// authorizeMaintenanceTable checks the caller may perform operation on the
|
|
// table, returning the table ARN and the creation time of the table record the
|
|
// check read.
|
|
func (h *S3TablesHandler) authorizeMaintenanceTable(r *http.Request, filerClient FilerClient, operation, bucketName, namespaceName, tableName string) (string, time.Time, error) {
|
|
tablePath := GetTablePath(bucketName, namespaceName, tableName)
|
|
bucketPath := GetTableBucketPath(bucketName)
|
|
|
|
var metadata tableMetadataInternal
|
|
var bucketPolicy, tablePolicy string
|
|
var bucketTags, tableTags map[string]string
|
|
err := filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
data, err := h.readTableMetadataAttr(r.Context(), client, tablePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := json.Unmarshal(data, &metadata); err != nil {
|
|
return fmt.Errorf("failed to unmarshal table metadata: %w", err)
|
|
}
|
|
if bucketPolicy, err = h.readPolicy(r, client, bucketPath); err != nil {
|
|
return err
|
|
}
|
|
if bucketTags, err = h.readTags(r.Context(), client, bucketPath); err != nil {
|
|
return err
|
|
}
|
|
if tablePolicy, err = h.readPolicy(r, client, tablePath); err != nil {
|
|
return err
|
|
}
|
|
tableTags, err = h.readTags(r.Context(), client, tablePath)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return "", time.Time{}, err
|
|
}
|
|
|
|
tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespaceName+"/"+tableName)
|
|
principal := h.getAccountID(r)
|
|
ctx := &PolicyContext{
|
|
TrustedIssuers: h.trustedIssuers(r),
|
|
IsLocalAccountID: h.localAccountIDs,
|
|
TableBucketName: bucketName,
|
|
Namespace: namespaceName,
|
|
TableName: tableName,
|
|
TableBucketTags: bucketTags,
|
|
ResourceTags: tableTags,
|
|
IdentityActions: getIdentityActions(r),
|
|
DefaultAllow: h.defaultAllowFor(r),
|
|
}
|
|
if deniedByPolicy(operation, principal, tablePolicy, tableARN, ctx) ||
|
|
!CheckPermissionWithContext(operation, principal, metadata.OwnerAccountID, bucketPolicy, tableARN, ctx) {
|
|
return "", time.Time{}, NewAuthError(operation, principal, "not authorized to "+operation)
|
|
}
|
|
|
|
return tableARN, metadata.CreatedAt, nil
|
|
}
|
|
|
|
// readPolicy returns the policy stored on path, or an empty string when the
|
|
// resource has none.
|
|
func (h *S3TablesHandler) readPolicy(r *http.Request, client filer_pb.SeaweedFilerClient, path string) (string, error) {
|
|
data, err := h.getExtendedAttribute(r.Context(), client, path, ExtendedKeyPolicy)
|
|
if err != nil {
|
|
if errors.Is(err, ErrAttributeNotFound) {
|
|
return "", nil
|
|
}
|
|
return "", fmt.Errorf("failed to read policy: %w", err)
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
// writeMaintenanceError maps the shared failure modes of the maintenance
|
|
// handlers onto responses.
|
|
func (h *S3TablesHandler) writeMaintenanceError(w http.ResponseWriter, err error, notFoundCode, notFoundMessage string) {
|
|
switch {
|
|
case errors.Is(err, filer_pb.ErrNotFound):
|
|
h.writeError(w, http.StatusNotFound, notFoundCode, notFoundMessage)
|
|
case isAuthError(err):
|
|
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, err.Error())
|
|
case errors.Is(err, ErrConcurrentUpdate):
|
|
h.writeError(w, http.StatusConflict, ErrCodeConflict, "maintenance configuration changed concurrently, retry the request")
|
|
default:
|
|
h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, err.Error())
|
|
}
|
|
}
|
|
|
|
// validateMaintenanceValue rejects a type that does not belong to the scope and
|
|
// settings that do not name that same type.
|
|
func validateMaintenanceValue(maintenanceType string, allowed map[string]bool, value *MaintenanceConfigurationValue) error {
|
|
if maintenanceType == "" {
|
|
return fmt.Errorf("type is required")
|
|
}
|
|
if !allowed[maintenanceType] {
|
|
return fmt.Errorf("unsupported maintenance type %q", maintenanceType)
|
|
}
|
|
if value == nil {
|
|
return fmt.Errorf("value is required")
|
|
}
|
|
|
|
switch value.Status {
|
|
case MaintenanceStatusEnabled, MaintenanceStatusDisabled:
|
|
case "":
|
|
return fmt.Errorf("value.status is required")
|
|
default:
|
|
return fmt.Errorf("invalid value.status %q, expected %q or %q", value.Status, MaintenanceStatusEnabled, MaintenanceStatusDisabled)
|
|
}
|
|
|
|
if value.Settings != nil && !settingsMatchType(maintenanceType, value.Settings) {
|
|
return fmt.Errorf("value.settings must only contain %s", maintenanceType)
|
|
}
|
|
return validateMaintenanceSettings(value.Settings)
|
|
}
|
|
|
|
// AWS bounds every maintenance setting to a positive 32-bit value. Accepting
|
|
// anything else would store a number the worker then ignores or saturates, so
|
|
// the configuration read back would not be the one that runs.
|
|
const (
|
|
maintenanceSettingMin int64 = 1
|
|
maintenanceSettingMax int64 = 2147483647
|
|
)
|
|
|
|
func validateMaintenanceSettings(settings *MaintenanceSettings) error {
|
|
if settings == nil {
|
|
return nil
|
|
}
|
|
|
|
if c := settings.IcebergCompaction; c != nil {
|
|
if err := validateCompactionStrategy(c.Strategy); err != nil {
|
|
return err
|
|
}
|
|
if err := validateMaintenanceSetting("targetFileSizeMB", c.TargetFileSizeMB); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if m := settings.IcebergSnapshotManagement; m != nil {
|
|
if err := validateMaintenanceSetting("minSnapshotsToKeep", m.MinSnapshotsToKeep); err != nil {
|
|
return err
|
|
}
|
|
if err := validateMaintenanceSetting("maxSnapshotAgeHours", m.MaxSnapshotAgeHours); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if u := settings.IcebergUnreferencedFileRemoval; u != nil {
|
|
if err := validateMaintenanceSetting("unreferencedDays", u.UnreferencedDays); err != nil {
|
|
return err
|
|
}
|
|
if err := validateMaintenanceSetting("nonCurrentDays", u.NonCurrentDays); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateMaintenanceSetting(name string, value *int64) error {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
if *value < maintenanceSettingMin || *value > maintenanceSettingMax {
|
|
return fmt.Errorf("%s must be between %d and %d, got %d", name, maintenanceSettingMin, maintenanceSettingMax, *value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateCompactionStrategy rejects a strategy the maintenance worker cannot
|
|
// carry out, rather than accepting it and quietly compacting some other way.
|
|
func validateCompactionStrategy(strategy string) error {
|
|
switch strategy {
|
|
case "", CompactionStrategyAuto, CompactionStrategyBinpack, CompactionStrategySort:
|
|
return nil
|
|
case CompactionStrategyZOrder:
|
|
return fmt.Errorf("compaction strategy %q is not supported", strategy)
|
|
default:
|
|
return fmt.Errorf("invalid compaction strategy %q, expected one of %q, %q, %q",
|
|
strategy, CompactionStrategyAuto, CompactionStrategyBinpack, CompactionStrategySort)
|
|
}
|
|
}
|
|
|
|
func settingsMatchType(maintenanceType string, settings *MaintenanceSettings) bool {
|
|
switch maintenanceType {
|
|
case MaintenanceTypeIcebergCompaction:
|
|
return settings.IcebergSnapshotManagement == nil && settings.IcebergUnreferencedFileRemoval == nil
|
|
case MaintenanceTypeIcebergSnapshotManagement:
|
|
return settings.IcebergCompaction == nil && settings.IcebergUnreferencedFileRemoval == nil
|
|
case MaintenanceTypeIcebergUnreferencedFileRemoval:
|
|
return settings.IcebergCompaction == nil && settings.IcebergSnapshotManagement == nil
|
|
}
|
|
return false
|
|
}
|