s3tables: support multi-level namespace normalization
This commit is contained in:
@@ -63,6 +63,10 @@ type tableBucketMetadata struct {
|
|||||||
|
|
||||||
const s3TablesAdminListLimit = 1000
|
const s3TablesAdminListLimit = 1000
|
||||||
|
|
||||||
|
func parseNamespaceInput(namespace string) ([]string, error) {
|
||||||
|
return s3tables.ParseNamespace(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
func newS3TablesManager() *s3tables.Manager {
|
func newS3TablesManager() *s3tables.Manager {
|
||||||
manager := s3tables.NewManager()
|
manager := s3tables.NewManager()
|
||||||
manager.SetAccountID(s3_constants.AccountAdminId)
|
manager.SetAccountID(s3_constants.AccountAdminId)
|
||||||
@@ -158,7 +162,11 @@ func (s *AdminServer) GetS3TablesTablesData(ctx context.Context, bucketArn, name
|
|||||||
var resp s3tables.ListTablesResponse
|
var resp s3tables.ListTablesResponse
|
||||||
var ns []string
|
var ns []string
|
||||||
if namespace != "" {
|
if namespace != "" {
|
||||||
ns = []string{namespace}
|
parts, err := parseNamespaceInput(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return S3TablesTablesData{}, err
|
||||||
|
}
|
||||||
|
ns = parts
|
||||||
}
|
}
|
||||||
req := &s3tables.ListTablesRequest{TableBucketARN: bucketArn, Namespace: ns, MaxTables: s3TablesAdminListLimit}
|
req := &s3tables.ListTablesRequest{TableBucketARN: bucketArn, Namespace: ns, MaxTables: s3TablesAdminListLimit}
|
||||||
if err := s.executeS3TablesOperation(ctx, "ListTables", req, &resp); err != nil {
|
if err := s.executeS3TablesOperation(ctx, "ListTables", req, &resp); err != nil {
|
||||||
@@ -257,9 +265,13 @@ func (s *AdminServer) GetIcebergTablesData(ctx context.Context, catalogName, buc
|
|||||||
// GetIcebergTableDetailsData returns Iceberg table metadata and snapshot information.
|
// GetIcebergTableDetailsData returns Iceberg table metadata and snapshot information.
|
||||||
func (s *AdminServer) GetIcebergTableDetailsData(ctx context.Context, catalogName, bucketArn, namespace, tableName string) (IcebergTableDetailsData, error) {
|
func (s *AdminServer) GetIcebergTableDetailsData(ctx context.Context, catalogName, bucketArn, namespace, tableName string) (IcebergTableDetailsData, error) {
|
||||||
var resp s3tables.GetTableResponse
|
var resp s3tables.GetTableResponse
|
||||||
|
namespaceParts, err := parseNamespaceInput(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return IcebergTableDetailsData{}, err
|
||||||
|
}
|
||||||
req := &s3tables.GetTableRequest{
|
req := &s3tables.GetTableRequest{
|
||||||
TableBucketARN: bucketArn,
|
TableBucketARN: bucketArn,
|
||||||
Namespace: []string{namespace},
|
Namespace: namespaceParts,
|
||||||
Name: tableName,
|
Name: tableName,
|
||||||
}
|
}
|
||||||
if err := s.executeS3TablesOperation(ctx, "GetTable", req, &resp); err != nil {
|
if err := s.executeS3TablesOperation(ctx, "GetTable", req, &resp); err != nil {
|
||||||
@@ -686,7 +698,12 @@ func (s *AdminServer) CreateS3TablesNamespace(c *gin.Context) {
|
|||||||
c.JSON(400, gin.H{"error": "bucket_arn and name are required"})
|
c.JSON(400, gin.H{"error": "bucket_arn and name are required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
createReq := &s3tables.CreateNamespaceRequest{TableBucketARN: req.BucketARN, Namespace: []string{req.Name}}
|
namespaceParts, err := parseNamespaceInput(req.Name)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": "Invalid namespace: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
createReq := &s3tables.CreateNamespaceRequest{TableBucketARN: req.BucketARN, Namespace: namespaceParts}
|
||||||
var resp s3tables.CreateNamespaceResponse
|
var resp s3tables.CreateNamespaceResponse
|
||||||
if err := s.executeS3TablesOperation(c.Request.Context(), "CreateNamespace", createReq, &resp); err != nil {
|
if err := s.executeS3TablesOperation(c.Request.Context(), "CreateNamespace", createReq, &resp); err != nil {
|
||||||
writeS3TablesError(c, err)
|
writeS3TablesError(c, err)
|
||||||
@@ -705,7 +722,12 @@ func (s *AdminServer) DeleteS3TablesNamespace(c *gin.Context) {
|
|||||||
c.JSON(400, gin.H{"error": "bucket and name query parameters are required"})
|
c.JSON(400, gin.H{"error": "bucket and name query parameters are required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req := &s3tables.DeleteNamespaceRequest{TableBucketARN: bucketArn, Namespace: []string{namespace}}
|
namespaceParts, err := parseNamespaceInput(namespace)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": "Invalid namespace: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req := &s3tables.DeleteNamespaceRequest{TableBucketARN: bucketArn, Namespace: namespaceParts}
|
||||||
if err := s.executeS3TablesOperation(c.Request.Context(), "DeleteNamespace", req, nil); err != nil {
|
if err := s.executeS3TablesOperation(c.Request.Context(), "DeleteNamespace", req, nil); err != nil {
|
||||||
writeS3TablesError(c, err)
|
writeS3TablesError(c, err)
|
||||||
return
|
return
|
||||||
@@ -745,6 +767,11 @@ func (s *AdminServer) CreateS3TablesTable(c *gin.Context) {
|
|||||||
c.JSON(400, gin.H{"error": "bucket_arn, namespace, and name are required"})
|
c.JSON(400, gin.H{"error": "bucket_arn, namespace, and name are required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
namespaceParts, err := parseNamespaceInput(req.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": "Invalid namespace: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
format := req.Format
|
format := req.Format
|
||||||
if format == "" {
|
if format == "" {
|
||||||
format = "ICEBERG"
|
format = "ICEBERG"
|
||||||
@@ -757,7 +784,7 @@ func (s *AdminServer) CreateS3TablesTable(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
createReq := &s3tables.CreateTableRequest{
|
createReq := &s3tables.CreateTableRequest{
|
||||||
TableBucketARN: req.BucketARN,
|
TableBucketARN: req.BucketARN,
|
||||||
Namespace: []string{req.Namespace},
|
Namespace: namespaceParts,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Format: format,
|
Format: format,
|
||||||
Tags: req.Tags,
|
Tags: req.Tags,
|
||||||
@@ -780,7 +807,12 @@ func (s *AdminServer) DeleteS3TablesTable(c *gin.Context) {
|
|||||||
c.JSON(400, gin.H{"error": "bucket, namespace, and name query parameters are required"})
|
c.JSON(400, gin.H{"error": "bucket, namespace, and name query parameters are required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req := &s3tables.DeleteTableRequest{TableBucketARN: bucketArn, Namespace: []string{namespace}, Name: name, VersionToken: version}
|
namespaceParts, err := parseNamespaceInput(namespace)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": "Invalid namespace: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req := &s3tables.DeleteTableRequest{TableBucketARN: bucketArn, Namespace: namespaceParts, Name: name, VersionToken: version}
|
||||||
if err := s.executeS3TablesOperation(c.Request.Context(), "DeleteTable", req, nil); err != nil {
|
if err := s.executeS3TablesOperation(c.Request.Context(), "DeleteTable", req, nil); err != nil {
|
||||||
writeS3TablesError(c, err)
|
writeS3TablesError(c, err)
|
||||||
return
|
return
|
||||||
@@ -853,7 +885,12 @@ func (s *AdminServer) PutS3TablesTablePolicy(c *gin.Context) {
|
|||||||
c.JSON(400, gin.H{"error": "bucket_arn, namespace, name, and policy are required"})
|
c.JSON(400, gin.H{"error": "bucket_arn, namespace, name, and policy are required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
putReq := &s3tables.PutTablePolicyRequest{TableBucketARN: req.BucketARN, Namespace: []string{req.Namespace}, Name: req.Name, ResourcePolicy: req.Policy}
|
namespaceParts, err := parseNamespaceInput(req.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": "Invalid namespace: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
putReq := &s3tables.PutTablePolicyRequest{TableBucketARN: req.BucketARN, Namespace: namespaceParts, Name: req.Name, ResourcePolicy: req.Policy}
|
||||||
if err := s.executeS3TablesOperation(c.Request.Context(), "PutTablePolicy", putReq, nil); err != nil {
|
if err := s.executeS3TablesOperation(c.Request.Context(), "PutTablePolicy", putReq, nil); err != nil {
|
||||||
writeS3TablesError(c, err)
|
writeS3TablesError(c, err)
|
||||||
return
|
return
|
||||||
@@ -869,7 +906,12 @@ func (s *AdminServer) GetS3TablesTablePolicy(c *gin.Context) {
|
|||||||
c.JSON(400, gin.H{"error": "bucket, namespace, and name query parameters are required"})
|
c.JSON(400, gin.H{"error": "bucket, namespace, and name query parameters are required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
getReq := &s3tables.GetTablePolicyRequest{TableBucketARN: bucketArn, Namespace: []string{namespace}, Name: name}
|
namespaceParts, err := parseNamespaceInput(namespace)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": "Invalid namespace: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getReq := &s3tables.GetTablePolicyRequest{TableBucketARN: bucketArn, Namespace: namespaceParts, Name: name}
|
||||||
var resp s3tables.GetTablePolicyResponse
|
var resp s3tables.GetTablePolicyResponse
|
||||||
if err := s.executeS3TablesOperation(c.Request.Context(), "GetTablePolicy", getReq, &resp); err != nil {
|
if err := s.executeS3TablesOperation(c.Request.Context(), "GetTablePolicy", getReq, &resp); err != nil {
|
||||||
writeS3TablesError(c, err)
|
writeS3TablesError(c, err)
|
||||||
@@ -886,7 +928,12 @@ func (s *AdminServer) DeleteS3TablesTablePolicy(c *gin.Context) {
|
|||||||
c.JSON(400, gin.H{"error": "bucket, namespace, and name query parameters are required"})
|
c.JSON(400, gin.H{"error": "bucket, namespace, and name query parameters are required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
deleteReq := &s3tables.DeleteTablePolicyRequest{TableBucketARN: bucketArn, Namespace: []string{namespace}, Name: name}
|
namespaceParts, err := parseNamespaceInput(namespace)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": "Invalid namespace: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deleteReq := &s3tables.DeleteTablePolicyRequest{TableBucketARN: bucketArn, Namespace: namespaceParts, Name: name}
|
||||||
if err := s.executeS3TablesOperation(c.Request.Context(), "DeleteTablePolicy", deleteReq, nil); err != nil {
|
if err := s.executeS3TablesOperation(c.Request.Context(), "DeleteTablePolicy", deleteReq, nil); err != nil {
|
||||||
writeS3TablesError(c, err)
|
writeS3TablesError(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -725,20 +725,30 @@ function s3TablesNamespaceNameError(name) {
|
|||||||
if (name.includes('/')) {
|
if (name.includes('/')) {
|
||||||
return "namespace name cannot contain '/'";
|
return "namespace name cannot contain '/'";
|
||||||
}
|
}
|
||||||
if (!isLowercaseLetterOrDigit(name[0])) {
|
|
||||||
return 'Namespace name must start with a letter or digit';
|
const parts = name.split('.');
|
||||||
}
|
for (const part of parts) {
|
||||||
if (!isLowercaseLetterOrDigit(name[name.length - 1])) {
|
if (!part) {
|
||||||
return 'Namespace name must end with a letter or digit';
|
return 'namespace levels cannot be empty';
|
||||||
}
|
}
|
||||||
for (const ch of name) {
|
if (part.length < 1 || part.length > 255) {
|
||||||
if (isLowercaseLetterOrDigit(ch) || ch === '_') {
|
return 'Namespace name must be between 1 and 255 characters';
|
||||||
continue;
|
}
|
||||||
|
if (!isLowercaseLetterOrDigit(part[0])) {
|
||||||
|
return 'Namespace name must start with a letter or digit';
|
||||||
|
}
|
||||||
|
if (!isLowercaseLetterOrDigit(part[part.length - 1])) {
|
||||||
|
return 'Namespace name must end with a letter or digit';
|
||||||
|
}
|
||||||
|
for (const ch of part) {
|
||||||
|
if (isLowercaseLetterOrDigit(ch) || ch === '_') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return "invalid namespace name: only 'a-z', '0-9', and '_' are allowed";
|
||||||
|
}
|
||||||
|
if (part.startsWith('aws')) {
|
||||||
|
return "namespace name cannot start with reserved prefix 'aws'";
|
||||||
}
|
}
|
||||||
return "invalid namespace name: only 'a-z', '0-9', and '_' are allowed";
|
|
||||||
}
|
|
||||||
if (name.startsWith('aws')) {
|
|
||||||
return "namespace name cannot start with reserved prefix 'aws'";
|
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ templ IcebergNamespaces(data dash.IcebergNamespacesData) {
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="icebergNamespaceName" class="form-label">Namespace</label>
|
<label for="icebergNamespaceName" class="form-label">Namespace</label>
|
||||||
<input type="text" class="form-control" id="icebergNamespaceName" name="name" placeholder="analytics" required/>
|
<input type="text" class="form-control" id="icebergNamespaceName" name="name" placeholder="analytics" required/>
|
||||||
<div class="form-text">Use lowercase letters, numbers, and underscores. Nested namespaces are not supported.</div>
|
<div class="form-text">Use lowercase letters, numbers, and underscores. Use dots for nested namespaces (for example, analytics.daily).</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ func IcebergNamespaces(data dash.IcebergNamespacesData) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\"> <input type=\"hidden\" id=\"icebergNamespaceCsrfToken\" name=\"csrf_token\"><div class=\"mb-3\"><label for=\"icebergNamespaceName\" class=\"form-label\">Namespace</label> <input type=\"text\" class=\"form-control\" id=\"icebergNamespaceName\" name=\"name\" placeholder=\"analytics\" required><div class=\"form-text\">Use lowercase letters, numbers, and underscores. Nested namespaces are not supported.</div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\"><i class=\"fas fa-plus me-1\"></i>Create</button></div></form></div></div></div><script>\n\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\tinitIcebergNamespaces();\n\t\t});\n\t</script>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\"> <input type=\"hidden\" id=\"icebergNamespaceCsrfToken\" name=\"csrf_token\"><div class=\"mb-3\"><label for=\"icebergNamespaceName\" class=\"form-label\">Namespace</label> <input type=\"text\" class=\"form-control\" id=\"icebergNamespaceName\" name=\"name\" placeholder=\"analytics\" required><div class=\"form-text\">Use lowercase letters, numbers, and underscores. Use dots for nested namespaces (for example, analytics.daily).</div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\"><i class=\"fas fa-plus me-1\"></i>Create</button></div></form></div></div></div><script>\n\t\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\t\tinitIcebergNamespaces();\n\t\t});\n\t</script>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,14 +154,14 @@ templ S3TablesNamespaces(data dash.S3TablesNamespacesData) {
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<form id="createS3TablesNamespaceForm">
|
<form id="createS3TablesNamespaceForm">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<input type="hidden" name="bucket_arn" value={ data.BucketARN }/>
|
<input type="hidden" name="bucket_arn" value={ data.BucketARN }/>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="s3tablesNamespaceName" class="form-label">Namespace</label>
|
<label for="s3tablesNamespaceName" class="form-label">Namespace</label>
|
||||||
<input type="text" class="form-control" id="s3tablesNamespaceName" name="name" placeholder="analytics" required/>
|
<input type="text" class="form-control" id="s3tablesNamespaceName" name="name" placeholder="analytics" required/>
|
||||||
<div class="form-text">Use lowercase letters, numbers, and underscores. Nested namespaces are not supported.</div>
|
<div class="form-text">Use lowercase letters, numbers, and underscores. Use dots for nested namespaces (for example, analytics.daily).</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
@@ -235,41 +235,53 @@ templ S3TablesNamespaces(data dash.S3TablesNamespacesData) {
|
|||||||
input.setCustomValidity('');
|
input.setCustomValidity('');
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
let message = '';
|
let message = '';
|
||||||
if (!name) {
|
if (!name) {
|
||||||
message = 'Namespace name is required';
|
message = 'Namespace name is required';
|
||||||
} else if (name.length < 1 || name.length > 255) {
|
} else if (name.length < 1 || name.length > 255) {
|
||||||
message = 'Namespace name must be between 1 and 255 characters';
|
message = 'Namespace name must be between 1 and 255 characters';
|
||||||
} else if (name === '.' || name === '..') {
|
} else if (name === '.' || name === '..') {
|
||||||
message = "namespace name cannot be '.' or '..'";
|
message = "namespace name cannot be '.' or '..'";
|
||||||
} else if (name.includes('/')) {
|
} else if (name.includes('/')) {
|
||||||
message = "namespace name cannot contain '/'";
|
message = "namespace name cannot contain '/'";
|
||||||
} else {
|
|
||||||
const start = name[0];
|
|
||||||
const end = name[name.length - 1];
|
|
||||||
const isStartValid = (start >= 'a' && start <= 'z') || (start >= '0' && start <= '9');
|
|
||||||
const isEndValid = (end >= 'a' && end <= 'z') || (end >= '0' && end <= '9');
|
|
||||||
if (!isStartValid) {
|
|
||||||
message = 'Namespace name must start with a letter or digit';
|
|
||||||
} else if (!isEndValid) {
|
|
||||||
message = 'Namespace name must end with a letter or digit';
|
|
||||||
} else {
|
} else {
|
||||||
for (const ch of name) {
|
for (const part of name.split('.')) {
|
||||||
const isLower = ch >= 'a' && ch <= 'z';
|
if (!part) {
|
||||||
const isDigit = ch >= '0' && ch <= '9';
|
message = 'namespace levels cannot be empty';
|
||||||
if (!(isLower || isDigit || ch === '_')) {
|
break;
|
||||||
message = "invalid namespace name: only 'a-z', '0-9', and '_' are allowed";
|
}
|
||||||
|
const start = part[0];
|
||||||
|
const end = part[part.length - 1];
|
||||||
|
const isStartValid = (start >= 'a' && start <= 'z') || (start >= '0' && start <= '9');
|
||||||
|
const isEndValid = (end >= 'a' && end <= 'z') || (end >= '0' && end <= '9');
|
||||||
|
if (!isStartValid) {
|
||||||
|
message = 'Namespace name must start with a letter or digit';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!isEndValid) {
|
||||||
|
message = 'Namespace name must end with a letter or digit';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (const ch of part) {
|
||||||
|
const isLower = ch >= 'a' && ch <= 'z';
|
||||||
|
const isDigit = ch >= '0' && ch <= '9';
|
||||||
|
if (!(isLower || isDigit || ch === '_')) {
|
||||||
|
message = "invalid namespace name: only 'a-z', '0-9', and '_' are allowed";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (message) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (part.startsWith('aws')) {
|
||||||
|
message = "namespace name cannot start with reserved prefix 'aws'";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!message && name.startsWith('aws')) {
|
|
||||||
message = "namespace name cannot start with reserved prefix 'aws'";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
input.setCustomValidity(message);
|
||||||
|
return message;
|
||||||
}
|
}
|
||||||
input.setCustomValidity(message);
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
s3tablesNamespaceDeleteModalInstance = new bootstrap.Modal(document.getElementById('deleteS3TablesNamespaceModal'));
|
s3tablesNamespaceDeleteModalInstance = new bootstrap.Modal(document.getElementById('deleteS3TablesNamespaceModal'));
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -231,11 +231,23 @@ func parseOptionalNamespace(r *http.Request, name string) []string {
|
|||||||
if value == "" {
|
if value == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if _, err := s3tables.ValidateNamespace([]string{value}); err != nil {
|
parts, err := s3tables.ParseNamespace(value)
|
||||||
|
if err != nil {
|
||||||
glog.V(1).Infof("invalid namespace value for %s: %q: %v", name, value, err)
|
glog.V(1).Infof("invalid namespace value for %s: %q: %v", name, value, err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return []string{value}
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRequiredNamespacePathParam(r *http.Request, name string) ([]string, error) {
|
||||||
|
value, err := getDecodedPathParam(r, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if value == "" {
|
||||||
|
return nil, fmt.Errorf("%s is required", name)
|
||||||
|
}
|
||||||
|
return s3tables.ParseNamespace(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseTagKeys handles tag key parsing from query parameters.
|
// parseTagKeys handles tag key parsing from query parameters.
|
||||||
@@ -352,19 +364,13 @@ func buildGetNamespaceRequest(r *http.Request) (interface{}, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
namespace, err := getDecodedPathParam(r, "namespace")
|
namespace, err := parseRequiredNamespacePathParam(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if namespace == "" {
|
|
||||||
return nil, fmt.Errorf("namespace is required")
|
|
||||||
}
|
|
||||||
if _, err := s3tables.ValidateNamespace([]string{namespace}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &s3tables.GetNamespaceRequest{
|
return &s3tables.GetNamespaceRequest{
|
||||||
TableBucketARN: tableBucketARN,
|
TableBucketARN: tableBucketARN,
|
||||||
Namespace: []string{namespace},
|
Namespace: namespace,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,19 +379,13 @@ func buildDeleteNamespaceRequest(r *http.Request) (interface{}, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
namespace, err := getDecodedPathParam(r, "namespace")
|
namespace, err := parseRequiredNamespacePathParam(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if namespace == "" {
|
|
||||||
return nil, fmt.Errorf("namespace is required")
|
|
||||||
}
|
|
||||||
if _, err := s3tables.ValidateNamespace([]string{namespace}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &s3tables.DeleteNamespaceRequest{
|
return &s3tables.DeleteNamespaceRequest{
|
||||||
TableBucketARN: tableBucketARN,
|
TableBucketARN: tableBucketARN,
|
||||||
Namespace: []string{namespace},
|
Namespace: namespace,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,18 +398,12 @@ func buildCreateTableRequest(r *http.Request) (interface{}, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
namespace, err := getDecodedPathParam(r, "namespace")
|
namespace, err := parseRequiredNamespacePathParam(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if namespace == "" {
|
|
||||||
return nil, fmt.Errorf("namespace is required")
|
|
||||||
}
|
|
||||||
if _, err := s3tables.ValidateNamespace([]string{namespace}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.TableBucketARN = tableBucketARN
|
req.TableBucketARN = tableBucketARN
|
||||||
req.Namespace = []string{namespace}
|
req.Namespace = namespace
|
||||||
return &req, nil
|
return &req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,16 +447,10 @@ func buildDeleteTableRequest(r *http.Request) (interface{}, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
namespace, err := getDecodedPathParam(r, "namespace")
|
namespace, err := parseRequiredNamespacePathParam(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if namespace == "" {
|
|
||||||
return nil, fmt.Errorf("namespace is required")
|
|
||||||
}
|
|
||||||
if _, err := s3tables.ValidateNamespace([]string{namespace}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
name, err := getDecodedPathParam(r, "name")
|
name, err := getDecodedPathParam(r, "name")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -475,7 +463,7 @@ func buildDeleteTableRequest(r *http.Request) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
return &s3tables.DeleteTableRequest{
|
return &s3tables.DeleteTableRequest{
|
||||||
TableBucketARN: tableBucketARN,
|
TableBucketARN: tableBucketARN,
|
||||||
Namespace: []string{namespace},
|
Namespace: namespace,
|
||||||
Name: name,
|
Name: name,
|
||||||
VersionToken: r.URL.Query().Get("versionToken"),
|
VersionToken: r.URL.Query().Get("versionToken"),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -490,16 +478,10 @@ func buildPutTablePolicyRequest(r *http.Request) (interface{}, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
namespace, err := getDecodedPathParam(r, "namespace")
|
namespace, err := parseRequiredNamespacePathParam(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if namespace == "" {
|
|
||||||
return nil, fmt.Errorf("namespace is required")
|
|
||||||
}
|
|
||||||
if _, err := s3tables.ValidateNamespace([]string{namespace}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
name, err := getDecodedPathParam(r, "name")
|
name, err := getDecodedPathParam(r, "name")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -511,7 +493,7 @@ func buildPutTablePolicyRequest(r *http.Request) (interface{}, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.TableBucketARN = tableBucketARN
|
req.TableBucketARN = tableBucketARN
|
||||||
req.Namespace = []string{namespace}
|
req.Namespace = namespace
|
||||||
req.Name = name
|
req.Name = name
|
||||||
return &req, nil
|
return &req, nil
|
||||||
}
|
}
|
||||||
@@ -521,16 +503,10 @@ func buildGetTablePolicyRequest(r *http.Request) (interface{}, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
namespace, err := getDecodedPathParam(r, "namespace")
|
namespace, err := parseRequiredNamespacePathParam(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if namespace == "" {
|
|
||||||
return nil, fmt.Errorf("namespace is required")
|
|
||||||
}
|
|
||||||
if _, err := s3tables.ValidateNamespace([]string{namespace}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
name, err := getDecodedPathParam(r, "name")
|
name, err := getDecodedPathParam(r, "name")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -543,7 +519,7 @@ func buildGetTablePolicyRequest(r *http.Request) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
return &s3tables.GetTablePolicyRequest{
|
return &s3tables.GetTablePolicyRequest{
|
||||||
TableBucketARN: tableBucketARN,
|
TableBucketARN: tableBucketARN,
|
||||||
Namespace: []string{namespace},
|
Namespace: namespace,
|
||||||
Name: name,
|
Name: name,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -553,16 +529,10 @@ func buildDeleteTablePolicyRequest(r *http.Request) (interface{}, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
namespace, err := getDecodedPathParam(r, "namespace")
|
namespace, err := parseRequiredNamespacePathParam(r, "namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if namespace == "" {
|
|
||||||
return nil, fmt.Errorf("namespace is required")
|
|
||||||
}
|
|
||||||
if _, err := s3tables.ValidateNamespace([]string{namespace}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
name, err := getDecodedPathParam(r, "name")
|
name, err := getDecodedPathParam(r, "name")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -575,7 +545,7 @@ func buildDeleteTablePolicyRequest(r *http.Request) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
return &s3tables.DeleteTablePolicyRequest{
|
return &s3tables.DeleteTablePolicyRequest{
|
||||||
TableBucketARN: tableBucketARN,
|
TableBucketARN: tableBucketARN,
|
||||||
Namespace: []string{namespace},
|
Namespace: namespace,
|
||||||
Name: name,
|
Name: name,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ func (h *S3TablesHandler) handleGetTable(w http.ResponseWriter, r *http.Request,
|
|||||||
resp := &GetTableResponse{
|
resp := &GetTableResponse{
|
||||||
Name: metadata.Name,
|
Name: metadata.Name,
|
||||||
TableARN: tableARN,
|
TableARN: tableARN,
|
||||||
Namespace: []string{metadata.Namespace},
|
Namespace: expandNamespace(metadata.Namespace),
|
||||||
Format: metadata.Format,
|
Format: metadata.Format,
|
||||||
CreatedAt: metadata.CreatedAt,
|
CreatedAt: metadata.CreatedAt,
|
||||||
ModifiedAt: metadata.ModifiedAt,
|
ModifiedAt: metadata.ModifiedAt,
|
||||||
@@ -683,7 +683,7 @@ func (h *S3TablesHandler) listTablesWithClient(r *http.Request, client filer_pb.
|
|||||||
tables = append(tables, TableSummary{
|
tables = append(tables, TableSummary{
|
||||||
Name: entry.Entry.Name,
|
Name: entry.Entry.Name,
|
||||||
TableARN: tableARN,
|
TableARN: tableARN,
|
||||||
Namespace: []string{namespaceName},
|
Namespace: expandNamespace(namespaceName),
|
||||||
CreatedAt: metadata.CreatedAt,
|
CreatedAt: metadata.CreatedAt,
|
||||||
ModifiedAt: metadata.ModifiedAt,
|
ModifiedAt: metadata.ModifiedAt,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
bucketNamePatternStr = `[a-z0-9-]+`
|
bucketNamePatternStr = `[a-z0-9-]+`
|
||||||
tableNamespacePatternStr = `[a-z0-9_]+`
|
tableNamespacePatternStr = `[a-z0-9_.]+`
|
||||||
tableNamePatternStr = `[a-z0-9_]+`
|
tableNamePatternStr = `[a-z0-9_]+`
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +54,6 @@ func ParseBucketNameFromARN(arn string) (string, error) {
|
|||||||
// parseTableFromARN extracts bucket name, namespace, and table name from ARN
|
// parseTableFromARN extracts bucket name, namespace, and table name from ARN
|
||||||
// ARN format: arn:aws:s3tables:{region}:{account}:bucket/{bucket-name}/table/{namespace}/{table-name}
|
// ARN format: arn:aws:s3tables:{region}:{account}:bucket/{bucket-name}/table/{namespace}/{table-name}
|
||||||
func parseTableFromARN(arn string) (bucketName, namespace, tableName string, err error) {
|
func parseTableFromARN(arn string) (bucketName, namespace, tableName string, err error) {
|
||||||
// Updated regex to align with namespace validation (single-segment)
|
|
||||||
matches := tableARNPattern.FindStringSubmatch(arn)
|
matches := tableARNPattern.FindStringSubmatch(arn)
|
||||||
if len(matches) != 4 {
|
if len(matches) != 4 {
|
||||||
return "", "", "", fmt.Errorf("invalid table ARN: %s", arn)
|
return "", "", "", fmt.Errorf("invalid table ARN: %s", arn)
|
||||||
@@ -66,9 +65,7 @@ func parseTableFromARN(arn string) (bucketName, namespace, tableName string, err
|
|||||||
return "", "", "", fmt.Errorf("invalid bucket name in ARN: %v", err)
|
return "", "", "", fmt.Errorf("invalid bucket name in ARN: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Namespace is already constrained by the regex; validate it directly.
|
namespace, err = validateNamespace([]string{matches[2]})
|
||||||
namespace = matches[2]
|
|
||||||
_, err = validateNamespace([]string{namespace})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", fmt.Errorf("invalid namespace in ARN: %v", err)
|
return "", "", "", fmt.Errorf("invalid namespace in ARN: %v", err)
|
||||||
}
|
}
|
||||||
@@ -326,35 +323,27 @@ func splitPath(p string) (dir, name string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateNamespace validates that the namespace provided is supported (single-level)
|
func validateNamespacePart(name string) error {
|
||||||
func validateNamespace(namespace []string) (string, error) {
|
|
||||||
if len(namespace) == 0 {
|
|
||||||
return "", fmt.Errorf("namespace is required")
|
|
||||||
}
|
|
||||||
if len(namespace) > 1 {
|
|
||||||
return "", fmt.Errorf("multi-level namespaces are not supported")
|
|
||||||
}
|
|
||||||
name := namespace[0]
|
|
||||||
if len(name) < 1 || len(name) > 255 {
|
if len(name) < 1 || len(name) > 255 {
|
||||||
return "", fmt.Errorf("namespace name must be between 1 and 255 characters")
|
return fmt.Errorf("namespace name must be between 1 and 255 characters")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent path traversal and multi-segment paths
|
// Prevent path traversal and multi-segment paths
|
||||||
if name == "." || name == ".." {
|
if name == "." || name == ".." {
|
||||||
return "", fmt.Errorf("namespace name cannot be '.' or '..'")
|
return fmt.Errorf("namespace name cannot be '.' or '..'")
|
||||||
}
|
}
|
||||||
if strings.Contains(name, "/") {
|
if strings.Contains(name, "/") {
|
||||||
return "", fmt.Errorf("namespace name cannot contain '/'")
|
return fmt.Errorf("namespace name cannot contain '/'")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must start and end with a letter or digit
|
// Must start and end with a letter or digit
|
||||||
start := name[0]
|
start := name[0]
|
||||||
end := name[len(name)-1]
|
end := name[len(name)-1]
|
||||||
if !((start >= 'a' && start <= 'z') || (start >= '0' && start <= '9')) {
|
if !((start >= 'a' && start <= 'z') || (start >= '0' && start <= '9')) {
|
||||||
return "", fmt.Errorf("namespace name must start with a letter or digit")
|
return fmt.Errorf("namespace name must start with a letter or digit")
|
||||||
}
|
}
|
||||||
if !((end >= 'a' && end <= 'z') || (end >= '0' && end <= '9')) {
|
if !((end >= 'a' && end <= 'z') || (end >= '0' && end <= '9')) {
|
||||||
return "", fmt.Errorf("namespace name must end with a letter or digit")
|
return fmt.Errorf("namespace name must end with a letter or digit")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allowed characters: a-z, 0-9, _
|
// Allowed characters: a-z, 0-9, _
|
||||||
@@ -362,15 +351,46 @@ func validateNamespace(namespace []string) (string, error) {
|
|||||||
if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' {
|
if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("invalid namespace name: only 'a-z', '0-9', and '_' are allowed")
|
return fmt.Errorf("invalid namespace name: only 'a-z', '0-9', and '_' are allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reserved prefix
|
// Reserved prefix
|
||||||
if strings.HasPrefix(name, "aws") {
|
if strings.HasPrefix(name, "aws") {
|
||||||
return "", fmt.Errorf("namespace name cannot start with reserved prefix 'aws'")
|
return fmt.Errorf("namespace name cannot start with reserved prefix 'aws'")
|
||||||
}
|
}
|
||||||
|
|
||||||
return name, nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeNamespace(namespace []string) ([]string, error) {
|
||||||
|
if len(namespace) == 0 {
|
||||||
|
return nil, fmt.Errorf("namespace is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := namespace
|
||||||
|
if len(namespace) == 1 {
|
||||||
|
parts = strings.Split(namespace[0], ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized := make([]string, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
if err := validateNamespacePart(part); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
normalized = append(normalized, part)
|
||||||
|
}
|
||||||
|
return normalized, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateNamespace validates namespace identifiers and returns an internal namespace key.
|
||||||
|
// A single dotted namespace value is interpreted as multi-level namespace for compatibility
|
||||||
|
// with path-style APIs, for example "analytics.daily" => ["analytics", "daily"].
|
||||||
|
func validateNamespace(namespace []string) (string, error) {
|
||||||
|
parts, err := normalizeNamespace(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return flattenNamespace(parts), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateNamespace is a wrapper to validate namespace for other packages.
|
// ValidateNamespace is a wrapper to validate namespace for other packages.
|
||||||
@@ -378,6 +398,11 @@ func ValidateNamespace(namespace []string) (string, error) {
|
|||||||
return validateNamespace(namespace)
|
return validateNamespace(namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseNamespace parses a namespace string into namespace parts.
|
||||||
|
func ParseNamespace(namespace string) ([]string, error) {
|
||||||
|
return normalizeNamespace([]string{namespace})
|
||||||
|
}
|
||||||
|
|
||||||
// validateTableName validates a table name
|
// validateTableName validates a table name
|
||||||
func validateTableName(name string) (string, error) {
|
func validateTableName(name string) (string, error) {
|
||||||
if len(name) < 1 || len(name) > 255 {
|
if len(name) < 1 || len(name) > 255 {
|
||||||
@@ -415,3 +440,14 @@ func flattenNamespace(namespace []string) string {
|
|||||||
}
|
}
|
||||||
return strings.Join(namespace, ".")
|
return strings.Join(namespace, ".")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func expandNamespace(namespace string) []string {
|
||||||
|
if namespace == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parts, err := ParseNamespace(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return []string{namespace}
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|||||||
126
weed/s3api/s3tables/utils_namespace_test.go
Normal file
126
weed/s3api/s3tables/utils_namespace_test.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package s3tables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateNamespaceSupportsMultiLevel(t *testing.T) {
|
||||||
|
got, err := validateNamespace([]string{"analytics", "daily"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("validateNamespace returned error: %v", err)
|
||||||
|
}
|
||||||
|
if got != "analytics.daily" {
|
||||||
|
t.Fatalf("validateNamespace = %q, want %q", got, "analytics.daily")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateNamespaceSupportsDottedInput(t *testing.T) {
|
||||||
|
got, err := validateNamespace([]string{"analytics.daily"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("validateNamespace returned error: %v", err)
|
||||||
|
}
|
||||||
|
if got != "analytics.daily" {
|
||||||
|
t.Fatalf("validateNamespace = %q, want %q", got, "analytics.daily")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateNamespaceRejectsEmptyDottedSegment(t *testing.T) {
|
||||||
|
_, err := validateNamespace([]string{"analytics..daily"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected validateNamespace to fail for empty dotted segment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseNamespace(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
namespace string
|
||||||
|
want []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single level",
|
||||||
|
namespace: "analytics",
|
||||||
|
want: []string{"analytics"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi level dotted",
|
||||||
|
namespace: "analytics.daily",
|
||||||
|
want: []string{"analytics", "daily"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid reserved prefix",
|
||||||
|
namespace: "analytics.awsprod",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid empty segment",
|
||||||
|
namespace: "analytics..daily",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := ParseNamespace(tt.namespace)
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("ParseNamespace(%q) expected error", tt.namespace)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNamespace(%q) unexpected error: %v", tt.namespace, err)
|
||||||
|
}
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Fatalf("ParseNamespace(%q) = %v, want %v", tt.namespace, got, tt.want)
|
||||||
|
}
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != tt.want[i] {
|
||||||
|
t.Fatalf("ParseNamespace(%q) = %v, want %v", tt.namespace, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTableFromARNWithMultiLevelNamespace(t *testing.T) {
|
||||||
|
arn := "arn:aws:s3tables:us-east-1:123456789012:bucket/testbucket/table/analytics.daily/events"
|
||||||
|
bucket, namespace, table, err := parseTableFromARN(arn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseTableFromARN returned error: %v", err)
|
||||||
|
}
|
||||||
|
if bucket != "testbucket" {
|
||||||
|
t.Fatalf("bucket = %q, want %q", bucket, "testbucket")
|
||||||
|
}
|
||||||
|
if namespace != "analytics.daily" {
|
||||||
|
t.Fatalf("namespace = %q, want %q", namespace, "analytics.daily")
|
||||||
|
}
|
||||||
|
if table != "events" {
|
||||||
|
t.Fatalf("table = %q, want %q", table, "events")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildTableARNWithDottedNamespace(t *testing.T) {
|
||||||
|
arn, err := BuildTableARN("us-east-1", "123456789012", "testbucket", "analytics.daily", "events")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildTableARN returned error: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(arn, "/table/analytics.daily/events") {
|
||||||
|
t.Fatalf("BuildTableARN returned %q, missing normalized namespace/table path", arn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandNamespace(t *testing.T) {
|
||||||
|
got := expandNamespace("analytics.daily")
|
||||||
|
want := []string{"analytics", "daily"}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("expandNamespace = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Fatalf("expandNamespace = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user