mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-18 20:18:27 +02:00
* s3tables: scope OIDC-derived account IDs to the token issuer Table ownership keyed off the bare sub claim, so two identity providers issuing the same sub collided on one account - a caller could act on tables owned by a different provider's user. OIDC bearer tokens now carry the same issuer-namespaced (sub, iss) hash STS sessions already expose as aws:userid, and getAccountID prefers it over the raw claims. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3tables: keep legacy owners working and scrub client-subject claims - OIDC bearer tokens without a subject are rejected, and a token-supplied aws:userid claim is always overwritten with the verified (sub, iss) hash, so a forged claim can no longer stand in for the principal. - Owners written before issuer scoping hold a bare subject; permission checks now re-anchor that subject under the caller's issuer, so the same (sub, iss) pair keeps its ownership while scoped owners remain issuer-isolated. The token issuer is asserted into claims alongside sub and carried through PolicyContext. * s3tables: anchor legacy subjects to the configured issuer set * s3: enumerate trusted issuers from registered providers too * s3: fail closed when the OIDC provider store cannot be listed trustedOIDCIssuers kept the registered issuers when ListOIDCProviders failed, presenting a partial set as complete. A deployment trusting one registered issuer A plus a stored issuer B would, on a listing error, report only A — the single-issuer shape under which a legacy bare subject re-anchors — letting a caller from A claim a stored subject that belongs to B and gain owner access. With no provider store the registered providers are provably complete, so keep returning them; with a store, a failed listing means the real set is unknown, so return none. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
532 lines
20 KiB
Go
532 lines
20 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"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
|
|
}
|
|
|
|
if _, err := h.authorizeMaintenanceBucket(r, filerClient, "PutTableBucketMaintenanceConfiguration", bucketName); 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); 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
|
|
}
|
|
|
|
if _, err := h.authorizeMaintenanceTable(r, filerClient, "PutTableMaintenanceConfiguration", bucketName, namespaceName, tableName); 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); 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.
|
|
func (h *S3TablesHandler) putMaintenanceConfiguration(
|
|
r *http.Request,
|
|
filerClient FilerClient,
|
|
resourcePath, maintenanceType string,
|
|
value *MaintenanceConfigurationValue,
|
|
) error {
|
|
return filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
return h.mutateEntryExtended(r.Context(), client, resourcePath, func(extended map[string][]byte) error {
|
|
if len(extended[ExtendedKeyMetadata]) == 0 {
|
|
return fmt.Errorf("%w: %s", filer_pb.ErrNotFound, resourcePath)
|
|
}
|
|
|
|
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.
|
|
func (h *S3TablesHandler) authorizeMaintenanceBucket(r *http.Request, filerClient FilerClient, operation, bucketName string) (string, 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.readBucketPolicy(r, client, bucketPath)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
|
|
principal := h.getAccountID(r)
|
|
if !CheckPermissionWithContext(operation, principal, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
|
TrustedIssuers: h.trustedIssuers(r),
|
|
TableBucketName: bucketName,
|
|
IdentityActions: getIdentityActions(r),
|
|
DefaultAllow: h.defaultAllowFor(r),
|
|
}) {
|
|
return "", NewAuthError(operation, principal, "not authorized to "+operation)
|
|
}
|
|
|
|
return bucketARN, nil
|
|
}
|
|
|
|
// authorizeMaintenanceTable checks the caller may perform operation on the
|
|
// table, returning the table ARN.
|
|
func (h *S3TablesHandler) authorizeMaintenanceTable(r *http.Request, filerClient FilerClient, operation, bucketName, namespaceName, tableName string) (string, error) {
|
|
tablePath := GetTablePath(bucketName, namespaceName, tableName)
|
|
bucketPath := GetTableBucketPath(bucketName)
|
|
|
|
var metadata tableMetadataInternal
|
|
var bucketPolicy string
|
|
err := filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
data, err := h.getExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyMetadata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := json.Unmarshal(data, &metadata); err != nil {
|
|
return fmt.Errorf("failed to unmarshal table metadata: %w", err)
|
|
}
|
|
bucketPolicy, err = h.readBucketPolicy(r, client, bucketPath)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespaceName+"/"+tableName)
|
|
principal := h.getAccountID(r)
|
|
if !CheckPermissionWithContext(operation, principal, metadata.OwnerAccountID, bucketPolicy, tableARN, &PolicyContext{
|
|
TrustedIssuers: h.trustedIssuers(r),
|
|
TableBucketName: bucketName,
|
|
Namespace: namespaceName,
|
|
TableName: tableName,
|
|
IdentityActions: getIdentityActions(r),
|
|
DefaultAllow: h.defaultAllowFor(r),
|
|
}) {
|
|
return "", NewAuthError(operation, principal, "not authorized to "+operation)
|
|
}
|
|
|
|
return tableARN, nil
|
|
}
|
|
|
|
// readBucketPolicy returns the bucket policy, or an empty string when the
|
|
// bucket has none.
|
|
func (h *S3TablesHandler) readBucketPolicy(r *http.Request, client filer_pb.SeaweedFilerClient, bucketPath string) (string, error) {
|
|
data, err := h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyPolicy)
|
|
if err != nil {
|
|
if errors.Is(err, ErrAttributeNotFound) {
|
|
return "", nil
|
|
}
|
|
return "", fmt.Errorf("failed to read bucket 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
|
|
}
|