mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-15 08:18:42 +01:00
* s3tables: re-anchor legacy owners for federated STS sessions A web-identity role session was keyed on its bare OIDC subject until owners became issuer-scoped. Its sessions now carry the scoped aws:userid but no asserted issuers, so the legacy subject check never applied and the session lost the table buckets and namespaces it had created. Read the configured issuer set for such sessions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3api: keep token claims from asserting trusted issuers on STS sessions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * iceberg: give the catalog's table handler the local accounts and issuer set Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3tables: test a federated session on a bucket its legacy subject owns Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * sts: drop a token-supplied aws:userid when a session has no parent user Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3api: take a session's aws:userid from its signed parent user Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3api: test that session claims keep only the server-asserted aws:userid Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3tables: match legacy subject patterns in table bucket policy principals A policy written before issuer scoping can name role sessions by a pattern over their bare OIDC subjects. Match such a pattern against the caller's subject claim once that subject resolves to the session's scoped principal under the single configured issuer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3tables: test a federated session against a legacy subject pattern principal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
562 lines
21 KiB
Go
562 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),
|
|
Subject: callerSubject(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),
|
|
Subject: callerSubject(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
|
|
}
|