Add s3tables shell and admin UI (#8172)
* Add shared s3tables manager * Add s3tables shell commands * Add s3tables admin API * Add s3tables admin UI * Fix admin s3tables namespace create * Rename table buckets menu * Centralize s3tables tag validation * Reuse s3tables manager in admin * Extract s3tables list limit * Add s3tables bucket ARN helper * Remove write middleware from s3tables APIs * Fix bucket link and policy hint * Fix table tag parsing and nav link * Disable namespace table link on invalid ARN * Improve s3tables error decode * Return flag parse errors for s3tables tag * Accept query params for namespace create * Bind namespace create form data * Read s3tables JS data from DOM * s3tables: allow empty region ARN * shell: pass s3tables account id * shell: require account for table buckets * shell: use bucket name for namespaces * shell: use bucket name for tables * shell: use bucket name for tags * admin: add table buckets links in file browser * s3api: reuse s3tables tag validation * admin: harden s3tables UI handlers * fix admin list table buckets * allow admin s3tables access * validate s3tables bucket tags * log s3tables bucket metadata errors * rollback table bucket on owner failure * show s3tables bucket owner * add s3tables iam conditions * Add s3tables user permissions UI * Authorize s3tables using identity actions * Add s3tables permissions to user modal * Disambiguate bucket scope in user permissions * Block table bucket names that match S3 buckets * Pretty-print IAM identity JSON * Include tags in s3tables permission context * admin: refactor S3 Tables inline JavaScript into a separate file * s3tables: extend IAM policy condition operators support * shell: use LookupEntry wrapper for s3tables bucket conflict check * admin: handle buildBucketPermissions validation in create/update flows
This commit is contained in:
254
weed/shell/command_s3tables_bucket.go
Normal file
254
weed/shell/command_s3tables_bucket.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Commands = append(Commands, &commandS3TablesBucket{})
|
||||
}
|
||||
|
||||
type commandS3TablesBucket struct{}
|
||||
|
||||
func (c *commandS3TablesBucket) Name() string {
|
||||
return "s3tables.bucket"
|
||||
}
|
||||
|
||||
func (c *commandS3TablesBucket) Help() string {
|
||||
return `manage s3tables table buckets
|
||||
|
||||
# create a table bucket
|
||||
s3tables.bucket -create -name <bucket> -account <account_id> [-tags key1=val1,key2=val2]
|
||||
|
||||
# list table buckets
|
||||
s3tables.bucket -list -account <account_id> [-prefix <prefix>] [-limit <n>] [-continuation <token>]
|
||||
|
||||
# get a table bucket
|
||||
s3tables.bucket -get -name <bucket> -account <account_id>
|
||||
|
||||
# delete a table bucket
|
||||
s3tables.bucket -delete -name <bucket> -account <account_id>
|
||||
|
||||
# manage bucket policy
|
||||
s3tables.bucket -put-policy -name <bucket> -account <account_id> -file policy.json
|
||||
s3tables.bucket -get-policy -name <bucket> -account <account_id>
|
||||
s3tables.bucket -delete-policy -name <bucket> -account <account_id>
|
||||
`
|
||||
}
|
||||
|
||||
func (c *commandS3TablesBucket) HasTag(CommandTag) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *commandS3TablesBucket) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
cmd := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
|
||||
create := cmd.Bool("create", false, "create table bucket")
|
||||
list := cmd.Bool("list", false, "list table buckets")
|
||||
get := cmd.Bool("get", false, "get table bucket")
|
||||
deleteBucket := cmd.Bool("delete", false, "delete table bucket")
|
||||
putPolicy := cmd.Bool("put-policy", false, "put table bucket policy")
|
||||
getPolicy := cmd.Bool("get-policy", false, "get table bucket policy")
|
||||
deletePolicy := cmd.Bool("delete-policy", false, "delete table bucket policy")
|
||||
|
||||
name := cmd.String("name", "", "table bucket name")
|
||||
prefix := cmd.String("prefix", "", "bucket prefix")
|
||||
limit := cmd.Int("limit", 100, "max buckets to return")
|
||||
continuation := cmd.String("continuation", "", "continuation token")
|
||||
tags := cmd.String("tags", "", "comma separated tags key=value")
|
||||
policyFile := cmd.String("file", "", "policy file (json)")
|
||||
account := cmd.String("account", "", "owner account id")
|
||||
|
||||
if err := cmd.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actions := []*bool{create, list, get, deleteBucket, putPolicy, getPolicy, deletePolicy}
|
||||
count := 0
|
||||
for _, action := range actions {
|
||||
if *action {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
return fmt.Errorf("exactly one action must be specified")
|
||||
}
|
||||
|
||||
switch {
|
||||
case *create:
|
||||
if *name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
if *account == "" {
|
||||
return fmt.Errorf("-account is required")
|
||||
}
|
||||
if err := ensureNoS3BucketNameConflict(commandEnv, *name); err != nil {
|
||||
return err
|
||||
}
|
||||
req := &s3tables.CreateTableBucketRequest{Name: *name}
|
||||
if *tags != "" {
|
||||
parsed, err := parseS3TablesTags(*tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Tags = parsed
|
||||
}
|
||||
var resp s3tables.CreateTableBucketResponse
|
||||
if err := executeS3Tables(commandEnv, "CreateTableBucket", req, &resp, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintf(writer, "ARN: %s\n", resp.ARN)
|
||||
case *list:
|
||||
if *account == "" {
|
||||
return fmt.Errorf("-account is required")
|
||||
}
|
||||
req := &s3tables.ListTableBucketsRequest{Prefix: *prefix, ContinuationToken: *continuation, MaxBuckets: *limit}
|
||||
var resp s3tables.ListTableBucketsResponse
|
||||
if err := executeS3Tables(commandEnv, "ListTableBuckets", req, &resp, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
if len(resp.TableBuckets) == 0 {
|
||||
fmt.Fprintln(writer, "No table buckets found")
|
||||
return nil
|
||||
}
|
||||
for _, bucket := range resp.TableBuckets {
|
||||
fmt.Fprintf(writer, "Name: %s\n", bucket.Name)
|
||||
fmt.Fprintf(writer, "ARN: %s\n", bucket.ARN)
|
||||
fmt.Fprintf(writer, "CreatedAt: %s\n", bucket.CreatedAt.Format(timeFormat))
|
||||
fmt.Fprintln(writer, "---")
|
||||
}
|
||||
if resp.ContinuationToken != "" {
|
||||
fmt.Fprintf(writer, "ContinuationToken: %s\n", resp.ContinuationToken)
|
||||
}
|
||||
case *get:
|
||||
if *name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
if *account == "" {
|
||||
return fmt.Errorf("-account is required")
|
||||
}
|
||||
accountID := *account
|
||||
arn, err := buildS3TablesBucketARN(*name, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &s3tables.GetTableBucketRequest{TableBucketARN: arn}
|
||||
var resp s3tables.GetTableBucketResponse
|
||||
if err := executeS3Tables(commandEnv, "GetTableBucket", req, &resp, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintf(writer, "Name: %s\n", resp.Name)
|
||||
fmt.Fprintf(writer, "ARN: %s\n", resp.ARN)
|
||||
fmt.Fprintf(writer, "OwnerAccountID: %s\n", resp.OwnerAccountID)
|
||||
fmt.Fprintf(writer, "CreatedAt: %s\n", resp.CreatedAt.Format(timeFormat))
|
||||
case *deleteBucket:
|
||||
if *name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
if *account == "" {
|
||||
return fmt.Errorf("-account is required")
|
||||
}
|
||||
accountID := *account
|
||||
arn, err := buildS3TablesBucketARN(*name, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &s3tables.DeleteTableBucketRequest{TableBucketARN: arn}
|
||||
if err := executeS3Tables(commandEnv, "DeleteTableBucket", req, nil, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintln(writer, "Deleted table bucket")
|
||||
case *putPolicy:
|
||||
if *name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
if *account == "" {
|
||||
return fmt.Errorf("-account is required")
|
||||
}
|
||||
if *policyFile == "" {
|
||||
return fmt.Errorf("-file is required")
|
||||
}
|
||||
content, err := os.ReadFile(*policyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accountID := *account
|
||||
arn, err := buildS3TablesBucketARN(*name, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &s3tables.PutTableBucketPolicyRequest{TableBucketARN: arn, ResourcePolicy: string(content)}
|
||||
if err := executeS3Tables(commandEnv, "PutTableBucketPolicy", req, nil, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintln(writer, "Bucket policy updated")
|
||||
case *getPolicy:
|
||||
if *name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
if *account == "" {
|
||||
return fmt.Errorf("-account is required")
|
||||
}
|
||||
accountID := *account
|
||||
arn, err := buildS3TablesBucketARN(*name, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &s3tables.GetTableBucketPolicyRequest{TableBucketARN: arn}
|
||||
var resp s3tables.GetTableBucketPolicyResponse
|
||||
if err := executeS3Tables(commandEnv, "GetTableBucketPolicy", req, &resp, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintln(writer, resp.ResourcePolicy)
|
||||
case *deletePolicy:
|
||||
if *name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
if *account == "" {
|
||||
return fmt.Errorf("-account is required")
|
||||
}
|
||||
accountID := *account
|
||||
arn, err := buildS3TablesBucketARN(*name, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &s3tables.DeleteTableBucketPolicyRequest{TableBucketARN: arn}
|
||||
if err := executeS3Tables(commandEnv, "DeleteTableBucketPolicy", req, nil, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintln(writer, "Bucket policy deleted")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureNoS3BucketNameConflict(commandEnv *CommandEnv, bucketName string) error {
|
||||
return commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
resp, err := client.GetFilerConfiguration(context.Background(), &filer_pb.GetFilerConfigurationRequest{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get filer configuration: %w", err)
|
||||
}
|
||||
filerBucketsPath := resp.DirBuckets
|
||||
if filerBucketsPath == "" {
|
||||
filerBucketsPath = s3_constants.DefaultBucketsPath
|
||||
}
|
||||
_, err = filer_pb.LookupEntry(context.Background(), client, &filer_pb.LookupDirectoryEntryRequest{
|
||||
Directory: filerBucketsPath,
|
||||
Name: bucketName,
|
||||
})
|
||||
if err == nil {
|
||||
return fmt.Errorf("bucket name %s is already used by an object store bucket", bucketName)
|
||||
}
|
||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
131
weed/shell/command_s3tables_namespace.go
Normal file
131
weed/shell/command_s3tables_namespace.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Commands = append(Commands, &commandS3TablesNamespace{})
|
||||
}
|
||||
|
||||
type commandS3TablesNamespace struct{}
|
||||
|
||||
func (c *commandS3TablesNamespace) Name() string {
|
||||
return "s3tables.namespace"
|
||||
}
|
||||
|
||||
func (c *commandS3TablesNamespace) Help() string {
|
||||
return `manage s3tables namespaces
|
||||
|
||||
# create a namespace
|
||||
s3tables.namespace -create -bucket <bucket> -account <account_id> -name <namespace>
|
||||
|
||||
# list namespaces
|
||||
s3tables.namespace -list -bucket <bucket> -account <account_id> [-prefix <prefix>] [-limit <n>] [-continuation <token>]
|
||||
|
||||
# get namespace details
|
||||
s3tables.namespace -get -bucket <bucket> -account <account_id> -name <namespace>
|
||||
|
||||
# delete namespace
|
||||
s3tables.namespace -delete -bucket <bucket> -account <account_id> -name <namespace>
|
||||
`
|
||||
}
|
||||
|
||||
func (c *commandS3TablesNamespace) HasTag(CommandTag) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *commandS3TablesNamespace) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
cmd := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
|
||||
create := cmd.Bool("create", false, "create namespace")
|
||||
list := cmd.Bool("list", false, "list namespaces")
|
||||
get := cmd.Bool("get", false, "get namespace")
|
||||
deleteNamespace := cmd.Bool("delete", false, "delete namespace")
|
||||
|
||||
bucketName := cmd.String("bucket", "", "table bucket name")
|
||||
account := cmd.String("account", "", "owner account id")
|
||||
name := cmd.String("name", "", "namespace name")
|
||||
prefix := cmd.String("prefix", "", "namespace prefix")
|
||||
limit := cmd.Int("limit", 100, "max namespaces to return")
|
||||
continuation := cmd.String("continuation", "", "continuation token")
|
||||
|
||||
if err := cmd.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actions := []*bool{create, list, get, deleteNamespace}
|
||||
count := 0
|
||||
for _, action := range actions {
|
||||
if *action {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
return fmt.Errorf("exactly one action must be specified")
|
||||
}
|
||||
if *bucketName == "" {
|
||||
return fmt.Errorf("-bucket is required")
|
||||
}
|
||||
if *account == "" {
|
||||
return fmt.Errorf("-account is required")
|
||||
}
|
||||
|
||||
bucketArn, err := buildS3TablesBucketARN(*bucketName, *account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
namespace := strings.TrimSpace(*name)
|
||||
if (namespace == "" || namespace == "-") && (*create || *get || *deleteNamespace) {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
|
||||
switch {
|
||||
case *create:
|
||||
req := &s3tables.CreateNamespaceRequest{TableBucketARN: bucketArn, Namespace: []string{namespace}}
|
||||
var resp s3tables.CreateNamespaceResponse
|
||||
if err := executeS3Tables(commandEnv, "CreateNamespace", req, &resp, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintf(writer, "Namespace: %s\n", strings.Join(resp.Namespace, "/"))
|
||||
case *list:
|
||||
req := &s3tables.ListNamespacesRequest{TableBucketARN: bucketArn, Prefix: *prefix, ContinuationToken: *continuation, MaxNamespaces: *limit}
|
||||
var resp s3tables.ListNamespacesResponse
|
||||
if err := executeS3Tables(commandEnv, "ListNamespaces", req, &resp, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
if len(resp.Namespaces) == 0 {
|
||||
fmt.Fprintln(writer, "No namespaces found")
|
||||
return nil
|
||||
}
|
||||
for _, ns := range resp.Namespaces {
|
||||
fmt.Fprintf(writer, "Namespace: %s\n", strings.Join(ns.Namespace, "/"))
|
||||
fmt.Fprintf(writer, "CreatedAt: %s\n", ns.CreatedAt.Format(timeFormat))
|
||||
fmt.Fprintln(writer, "---")
|
||||
}
|
||||
if resp.ContinuationToken != "" {
|
||||
fmt.Fprintf(writer, "ContinuationToken: %s\n", resp.ContinuationToken)
|
||||
}
|
||||
case *get:
|
||||
req := &s3tables.GetNamespaceRequest{TableBucketARN: bucketArn, Namespace: []string{namespace}}
|
||||
var resp s3tables.GetNamespaceResponse
|
||||
if err := executeS3Tables(commandEnv, "GetNamespace", req, &resp, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintf(writer, "Namespace: %s\n", strings.Join(resp.Namespace, "/"))
|
||||
fmt.Fprintf(writer, "OwnerAccountID: %s\n", resp.OwnerAccountID)
|
||||
fmt.Fprintf(writer, "CreatedAt: %s\n", resp.CreatedAt.Format(timeFormat))
|
||||
case *deleteNamespace:
|
||||
req := &s3tables.DeleteNamespaceRequest{TableBucketARN: bucketArn, Namespace: []string{namespace}}
|
||||
if err := executeS3Tables(commandEnv, "DeleteNamespace", req, nil, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintln(writer, "Namespace deleted")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
205
weed/shell/command_s3tables_table.go
Normal file
205
weed/shell/command_s3tables_table.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Commands = append(Commands, &commandS3TablesTable{})
|
||||
}
|
||||
|
||||
type commandS3TablesTable struct{}
|
||||
|
||||
func (c *commandS3TablesTable) Name() string {
|
||||
return "s3tables.table"
|
||||
}
|
||||
|
||||
func (c *commandS3TablesTable) Help() string {
|
||||
return `manage s3tables tables
|
||||
|
||||
# create a table
|
||||
s3tables.table -create -bucket <bucket> -account <account_id> -namespace <namespace> -name <table> -format ICEBERG [-metadata metadata.json] [-tags key=value]
|
||||
|
||||
# list tables
|
||||
s3tables.table -list -bucket <bucket> -account <account_id> [-namespace <namespace>] [-prefix <prefix>] [-limit <n>] [-continuation <token>]
|
||||
|
||||
# get table details
|
||||
s3tables.table -get -bucket <bucket> -account <account_id> -namespace <namespace> -name <table>
|
||||
|
||||
# delete table
|
||||
s3tables.table -delete -bucket <bucket> -account <account_id> -namespace <namespace> -name <table> [-version <token>]
|
||||
|
||||
# manage table policy
|
||||
s3tables.table -put-policy -bucket <bucket> -account <account_id> -namespace <namespace> -name <table> -file policy.json
|
||||
s3tables.table -get-policy -bucket <bucket> -account <account_id> -namespace <namespace> -name <table>
|
||||
s3tables.table -delete-policy -bucket <bucket> -account <account_id> -namespace <namespace> -name <table>
|
||||
`
|
||||
}
|
||||
|
||||
func (c *commandS3TablesTable) HasTag(CommandTag) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *commandS3TablesTable) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
cmd := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
|
||||
create := cmd.Bool("create", false, "create table")
|
||||
list := cmd.Bool("list", false, "list tables")
|
||||
get := cmd.Bool("get", false, "get table")
|
||||
deleteTable := cmd.Bool("delete", false, "delete table")
|
||||
putPolicy := cmd.Bool("put-policy", false, "put table policy")
|
||||
getPolicy := cmd.Bool("get-policy", false, "get table policy")
|
||||
deletePolicy := cmd.Bool("delete-policy", false, "delete table policy")
|
||||
|
||||
bucketName := cmd.String("bucket", "", "table bucket name")
|
||||
account := cmd.String("account", "", "owner account id")
|
||||
namespace := cmd.String("namespace", "", "namespace")
|
||||
name := cmd.String("name", "", "table name")
|
||||
format := cmd.String("format", "ICEBERG", "table format")
|
||||
metadataFile := cmd.String("metadata", "", "table metadata json file")
|
||||
tags := cmd.String("tags", "", "comma separated tags key=value")
|
||||
prefix := cmd.String("prefix", "", "table name prefix")
|
||||
limit := cmd.Int("limit", 100, "max tables to return")
|
||||
continuation := cmd.String("continuation", "", "continuation token")
|
||||
version := cmd.String("version", "", "version token")
|
||||
policyFile := cmd.String("file", "", "policy file (json)")
|
||||
|
||||
if err := cmd.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actions := []*bool{create, list, get, deleteTable, putPolicy, getPolicy, deletePolicy}
|
||||
count := 0
|
||||
for _, action := range actions {
|
||||
if *action {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
return fmt.Errorf("exactly one action must be specified")
|
||||
}
|
||||
if *bucketName == "" {
|
||||
return fmt.Errorf("-bucket is required")
|
||||
}
|
||||
if *account == "" {
|
||||
return fmt.Errorf("-account is required")
|
||||
}
|
||||
|
||||
bucketArn, err := buildS3TablesBucketARN(*bucketName, *account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns := strings.TrimSpace(*namespace)
|
||||
if (*create || *get || *deleteTable || *putPolicy || *getPolicy || *deletePolicy) && ns == "" {
|
||||
return fmt.Errorf("-namespace is required")
|
||||
}
|
||||
if (*create || *get || *deleteTable || *putPolicy || *getPolicy || *deletePolicy) && *name == "" {
|
||||
return fmt.Errorf("-name is required")
|
||||
}
|
||||
|
||||
switch {
|
||||
case *create:
|
||||
var metadata *s3tables.TableMetadata
|
||||
if *metadataFile != "" {
|
||||
content, err := os.ReadFile(*metadataFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(content, &metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
req := &s3tables.CreateTableRequest{TableBucketARN: bucketArn, Namespace: []string{ns}, Name: *name, Format: *format, Metadata: metadata}
|
||||
if *tags != "" {
|
||||
parsed, err := parseS3TablesTags(*tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Tags = parsed
|
||||
}
|
||||
var resp s3tables.CreateTableResponse
|
||||
if err := executeS3Tables(commandEnv, "CreateTable", req, &resp, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintf(writer, "TableARN: %s\n", resp.TableARN)
|
||||
fmt.Fprintf(writer, "VersionToken: %s\n", resp.VersionToken)
|
||||
case *list:
|
||||
var nsList []string
|
||||
if ns != "" {
|
||||
nsList = []string{ns}
|
||||
}
|
||||
req := &s3tables.ListTablesRequest{TableBucketARN: bucketArn, Namespace: nsList, Prefix: *prefix, ContinuationToken: *continuation, MaxTables: *limit}
|
||||
var resp s3tables.ListTablesResponse
|
||||
if err := executeS3Tables(commandEnv, "ListTables", req, &resp, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
if len(resp.Tables) == 0 {
|
||||
fmt.Fprintln(writer, "No tables found")
|
||||
return nil
|
||||
}
|
||||
for _, table := range resp.Tables {
|
||||
fmt.Fprintf(writer, "Name: %s\n", table.Name)
|
||||
fmt.Fprintf(writer, "TableARN: %s\n", table.TableARN)
|
||||
fmt.Fprintf(writer, "Namespace: %s\n", strings.Join(table.Namespace, "/"))
|
||||
fmt.Fprintf(writer, "CreatedAt: %s\n", table.CreatedAt.Format(timeFormat))
|
||||
fmt.Fprintf(writer, "ModifiedAt: %s\n", table.ModifiedAt.Format(timeFormat))
|
||||
fmt.Fprintln(writer, "---")
|
||||
}
|
||||
if resp.ContinuationToken != "" {
|
||||
fmt.Fprintf(writer, "ContinuationToken: %s\n", resp.ContinuationToken)
|
||||
}
|
||||
case *get:
|
||||
req := &s3tables.GetTableRequest{TableBucketARN: bucketArn, Namespace: []string{ns}, Name: *name}
|
||||
var resp s3tables.GetTableResponse
|
||||
if err := executeS3Tables(commandEnv, "GetTable", req, &resp, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintf(writer, "Name: %s\n", resp.Name)
|
||||
fmt.Fprintf(writer, "TableARN: %s\n", resp.TableARN)
|
||||
fmt.Fprintf(writer, "Namespace: %s\n", strings.Join(resp.Namespace, "/"))
|
||||
fmt.Fprintf(writer, "OwnerAccountID: %s\n", resp.OwnerAccountID)
|
||||
fmt.Fprintf(writer, "CreatedAt: %s\n", resp.CreatedAt.Format(timeFormat))
|
||||
fmt.Fprintf(writer, "ModifiedAt: %s\n", resp.ModifiedAt.Format(timeFormat))
|
||||
fmt.Fprintf(writer, "VersionToken: %s\n", resp.VersionToken)
|
||||
case *deleteTable:
|
||||
req := &s3tables.DeleteTableRequest{TableBucketARN: bucketArn, Namespace: []string{ns}, Name: *name, VersionToken: *version}
|
||||
if err := executeS3Tables(commandEnv, "DeleteTable", req, nil, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintln(writer, "Table deleted")
|
||||
case *putPolicy:
|
||||
if *policyFile == "" {
|
||||
return fmt.Errorf("-file is required")
|
||||
}
|
||||
content, err := os.ReadFile(*policyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &s3tables.PutTablePolicyRequest{TableBucketARN: bucketArn, Namespace: []string{ns}, Name: *name, ResourcePolicy: string(content)}
|
||||
if err := executeS3Tables(commandEnv, "PutTablePolicy", req, nil, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintln(writer, "Table policy updated")
|
||||
case *getPolicy:
|
||||
req := &s3tables.GetTablePolicyRequest{TableBucketARN: bucketArn, Namespace: []string{ns}, Name: *name}
|
||||
var resp s3tables.GetTablePolicyResponse
|
||||
if err := executeS3Tables(commandEnv, "GetTablePolicy", req, &resp, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintln(writer, resp.ResourcePolicy)
|
||||
case *deletePolicy:
|
||||
req := &s3tables.DeleteTablePolicyRequest{TableBucketARN: bucketArn, Namespace: []string{ns}, Name: *name}
|
||||
if err := executeS3Tables(commandEnv, "DeleteTablePolicy", req, nil, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintln(writer, "Table policy deleted")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
131
weed/shell/command_s3tables_tag.go
Normal file
131
weed/shell/command_s3tables_tag.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Commands = append(Commands, &commandS3TablesTag{})
|
||||
}
|
||||
|
||||
type commandS3TablesTag struct{}
|
||||
|
||||
func (c *commandS3TablesTag) Name() string {
|
||||
return "s3tables.tag"
|
||||
}
|
||||
|
||||
func (c *commandS3TablesTag) Help() string {
|
||||
return `manage s3tables tags
|
||||
|
||||
# tag a table bucket
|
||||
s3tables.tag -put -bucket <bucket> -account <account_id> -tags key1=val1,key2=val2
|
||||
|
||||
# tag a table
|
||||
s3tables.tag -put -bucket <bucket> -account <account_id> -namespace <namespace> -name <table> -tags key1=val1,key2=val2
|
||||
|
||||
# list tags for a resource
|
||||
s3tables.tag -list -bucket <bucket> -account <account_id> [-namespace <namespace> -name <table>]
|
||||
|
||||
# remove tags
|
||||
s3tables.tag -delete -bucket <bucket> -account <account_id> [-namespace <namespace> -name <table>] -keys key1,key2
|
||||
`
|
||||
}
|
||||
|
||||
func (c *commandS3TablesTag) HasTag(CommandTag) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *commandS3TablesTag) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error {
|
||||
cmd := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
|
||||
put := cmd.Bool("put", false, "tag resource")
|
||||
list := cmd.Bool("list", false, "list tags")
|
||||
del := cmd.Bool("delete", false, "delete tags")
|
||||
|
||||
bucket := cmd.String("bucket", "", "table bucket name")
|
||||
account := cmd.String("account", "", "owner account id")
|
||||
namespace := cmd.String("namespace", "", "namespace")
|
||||
name := cmd.String("name", "", "table name")
|
||||
tags := cmd.String("tags", "", "comma separated tags key=value")
|
||||
keys := cmd.String("keys", "", "comma separated tag keys")
|
||||
|
||||
if err := cmd.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actions := []*bool{put, list, del}
|
||||
count := 0
|
||||
for _, action := range actions {
|
||||
if *action {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
return fmt.Errorf("exactly one action must be specified")
|
||||
}
|
||||
if *bucket == "" {
|
||||
return fmt.Errorf("-bucket is required")
|
||||
}
|
||||
if *account == "" {
|
||||
return fmt.Errorf("-account is required")
|
||||
}
|
||||
resourceArn, err := buildS3TablesBucketARN(*bucket, *account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *namespace != "" || *name != "" {
|
||||
if *namespace == "" || *name == "" {
|
||||
return fmt.Errorf("-namespace and -name are required for table tags")
|
||||
}
|
||||
resourceArn, err = buildS3TablesTableARN(*bucket, *namespace, *name, *account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case *put:
|
||||
if *tags == "" {
|
||||
return fmt.Errorf("-tags is required")
|
||||
}
|
||||
parsed, err := parseS3TablesTags(*tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &s3tables.TagResourceRequest{ResourceARN: resourceArn, Tags: parsed}
|
||||
if err := executeS3Tables(commandEnv, "TagResource", req, nil, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintln(writer, "Tags updated")
|
||||
case *list:
|
||||
req := &s3tables.ListTagsForResourceRequest{ResourceARN: resourceArn}
|
||||
var resp s3tables.ListTagsForResourceResponse
|
||||
if err := executeS3Tables(commandEnv, "ListTagsForResource", req, &resp, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
if len(resp.Tags) == 0 {
|
||||
fmt.Fprintln(writer, "No tags found")
|
||||
return nil
|
||||
}
|
||||
for k, v := range resp.Tags {
|
||||
fmt.Fprintf(writer, "%s=%s\n", k, v)
|
||||
}
|
||||
case *del:
|
||||
if *keys == "" {
|
||||
return fmt.Errorf("-keys is required")
|
||||
}
|
||||
parsed, err := parseS3TablesTagKeys(*keys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &s3tables.UntagResourceRequest{ResourceARN: resourceArn, TagKeys: parsed}
|
||||
if err := executeS3Tables(commandEnv, "UntagResource", req, nil, *account); err != nil {
|
||||
return parseS3TablesError(err)
|
||||
}
|
||||
fmt.Fprintln(writer, "Tags removed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
89
weed/shell/s3tables_helpers.go
Normal file
89
weed/shell/s3tables_helpers.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const s3TablesDefaultRegion = ""
|
||||
const timeFormat = "2006-01-02T15:04:05Z07:00"
|
||||
|
||||
func withFilerClient(commandEnv *CommandEnv, fn func(client filer_pb.SeaweedFilerClient) error) error {
|
||||
return pb.WithGrpcClient(false, 0, func(conn *grpc.ClientConn) error {
|
||||
client := filer_pb.NewSeaweedFilerClient(conn)
|
||||
return fn(client)
|
||||
}, commandEnv.option.FilerAddress.ToGrpcAddress(), false, commandEnv.option.GrpcDialOption)
|
||||
}
|
||||
|
||||
func executeS3Tables(commandEnv *CommandEnv, operation string, req interface{}, resp interface{}, accountID string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
return withFilerClient(commandEnv, func(client filer_pb.SeaweedFilerClient) error {
|
||||
manager := s3tables.NewManager()
|
||||
mgrClient := s3tables.NewManagerClient(client)
|
||||
return manager.Execute(ctx, mgrClient, operation, req, resp, accountID)
|
||||
})
|
||||
}
|
||||
|
||||
func parseS3TablesError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var s3Err *s3tables.S3TablesError
|
||||
if errors.As(err, &s3Err) {
|
||||
if s3Err.Message != "" {
|
||||
return fmt.Errorf("%s: %s", s3Err.Type, s3Err.Message)
|
||||
}
|
||||
return fmt.Errorf("%s", s3Err.Type)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func parseS3TablesTags(value string) (map[string]string, error) {
|
||||
parsed := make(map[string]string)
|
||||
for _, kv := range strings.Split(value, ",") {
|
||||
if kv == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(kv, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid tag: %s", kv)
|
||||
}
|
||||
parsed[parts[0]] = parts[1]
|
||||
}
|
||||
if err := s3tables.ValidateTags(parsed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func parseS3TablesTagKeys(value string) ([]string, error) {
|
||||
var keys []string
|
||||
for _, key := range strings.Split(value, ",") {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, fmt.Errorf("tagKeys are required")
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func buildS3TablesBucketARN(bucketName, accountID string) (string, error) {
|
||||
return s3tables.BuildBucketARN(s3TablesDefaultRegion, accountID, bucketName)
|
||||
}
|
||||
|
||||
func buildS3TablesTableARN(bucketName, namespace, tableName, accountID string) (string, error) {
|
||||
return s3tables.BuildTableARN(s3TablesDefaultRegion, accountID, bucketName, namespace, tableName)
|
||||
}
|
||||
Reference in New Issue
Block a user