Browse Source
Admin UI: Add policies (#6968)
Admin UI: Add policies (#6968)
* add policies to UI, accessing filer directly * view, edit policies * add back buttons for "users" page * remove unused * fix ui dark mode when modal is closed * bucket view details button * fix browser buttons * filer action button works * clean up masters page * fix volume servers action buttons * fix collections page action button * fix properties page * more obvious * fix directory creation file mode * Update file_browser_handlers.go * directory permissionpull/6973/head
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 4921 additions and 2363 deletions
-
1weed/admin/dash/admin_server.go
-
80weed/admin/dash/file_browser_data.go
-
85weed/admin/dash/file_mode_utils.go
-
225weed/admin/dash/policies_management.go
-
27weed/admin/handlers/admin_handlers.go
-
15weed/admin/handlers/file_browser_handlers.go
-
121weed/admin/handlers/maintenance_handlers.go
-
273weed/admin/handlers/policy_handlers.go
-
211weed/admin/view/app/cluster_collections.templ
-
87weed/admin/view/app/cluster_collections_templ.go
-
56weed/admin/view/app/cluster_filers.templ
-
2weed/admin/view/app/cluster_filers_templ.go
-
119weed/admin/view/app/cluster_masters.templ
-
57weed/admin/view/app/cluster_masters_templ.go
-
177weed/admin/view/app/cluster_volume_servers.templ
-
148weed/admin/view/app/cluster_volume_servers_templ.go
-
376weed/admin/view/app/file_browser.templ
-
96weed/admin/view/app/file_browser_templ.go
-
350weed/admin/view/app/object_store_users.templ
-
2weed/admin/view/app/object_store_users_templ.go
-
658weed/admin/view/app/policies.templ
-
204weed/admin/view/app/policies_templ.go
-
297weed/admin/view/app/s3_buckets.templ
-
22weed/admin/view/app/s3_buckets_templ.go
-
5weed/admin/view/layout/layout.templ
-
24weed/admin/view/layout/layout_templ.go
-
22weed/credential/credential_store.go
-
188weed/credential/filer_etc/filer_etc_identity.go
-
114weed/credential/filer_etc/filer_etc_policy.go
-
180weed/credential/filer_etc/filer_etc_store.go
-
302weed/credential/memory/memory_identity.go
-
77weed/credential/memory/memory_policy.go
-
299weed/credential/memory/memory_store.go
-
446weed/credential/postgres/postgres_identity.go
-
130weed/credential/postgres/postgres_policy.go
-
449weed/credential/postgres/postgres_store.go
-
146weed/credential/test/policy_test.go
-
369weed/worker/tasks/balance/ui_templ.go
-
319weed/worker/tasks/erasure_coding/ui_templ.go
-
330weed/worker/tasks/vacuum/ui_templ.go
-
63weed/worker/types/task_ui_templ.go
@ -0,0 +1,85 @@ |
|||
package dash |
|||
|
|||
// FormatFileMode converts file mode to Unix-style string representation (e.g., "drwxr-xr-x")
|
|||
// Handles both Go's os.ModeDir format and standard Unix file type bits
|
|||
func FormatFileMode(mode uint32) string { |
|||
var result []byte = make([]byte, 10) |
|||
|
|||
// File type - handle Go's os.ModeDir first, then standard Unix file type bits
|
|||
if mode&0x80000000 != 0 { // Go's os.ModeDir (0x80000000 = 2147483648)
|
|||
result[0] = 'd' |
|||
} else { |
|||
switch mode & 0170000 { // S_IFMT mask
|
|||
case 0040000: // S_IFDIR
|
|||
result[0] = 'd' |
|||
case 0100000: // S_IFREG
|
|||
result[0] = '-' |
|||
case 0120000: // S_IFLNK
|
|||
result[0] = 'l' |
|||
case 0020000: // S_IFCHR
|
|||
result[0] = 'c' |
|||
case 0060000: // S_IFBLK
|
|||
result[0] = 'b' |
|||
case 0010000: // S_IFIFO
|
|||
result[0] = 'p' |
|||
case 0140000: // S_IFSOCK
|
|||
result[0] = 's' |
|||
default: |
|||
result[0] = '-' // S_IFREG is default
|
|||
} |
|||
} |
|||
|
|||
// Permission bits (always use the lower 12 bits regardless of file type format)
|
|||
// Owner permissions
|
|||
if mode&0400 != 0 { // S_IRUSR
|
|||
result[1] = 'r' |
|||
} else { |
|||
result[1] = '-' |
|||
} |
|||
if mode&0200 != 0 { // S_IWUSR
|
|||
result[2] = 'w' |
|||
} else { |
|||
result[2] = '-' |
|||
} |
|||
if mode&0100 != 0 { // S_IXUSR
|
|||
result[3] = 'x' |
|||
} else { |
|||
result[3] = '-' |
|||
} |
|||
|
|||
// Group permissions
|
|||
if mode&0040 != 0 { // S_IRGRP
|
|||
result[4] = 'r' |
|||
} else { |
|||
result[4] = '-' |
|||
} |
|||
if mode&0020 != 0 { // S_IWGRP
|
|||
result[5] = 'w' |
|||
} else { |
|||
result[5] = '-' |
|||
} |
|||
if mode&0010 != 0 { // S_IXGRP
|
|||
result[6] = 'x' |
|||
} else { |
|||
result[6] = '-' |
|||
} |
|||
|
|||
// Other permissions
|
|||
if mode&0004 != 0 { // S_IROTH
|
|||
result[7] = 'r' |
|||
} else { |
|||
result[7] = '-' |
|||
} |
|||
if mode&0002 != 0 { // S_IWOTH
|
|||
result[8] = 'w' |
|||
} else { |
|||
result[8] = '-' |
|||
} |
|||
if mode&0001 != 0 { // S_IXOTH
|
|||
result[9] = 'x' |
|||
} else { |
|||
result[9] = '-' |
|||
} |
|||
|
|||
return string(result) |
|||
} |
@ -0,0 +1,225 @@ |
|||
package dash |
|||
|
|||
import ( |
|||
"context" |
|||
"fmt" |
|||
"time" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/credential" |
|||
"github.com/seaweedfs/seaweedfs/weed/glog" |
|||
) |
|||
|
|||
type IAMPolicy struct { |
|||
Name string `json:"name"` |
|||
Document credential.PolicyDocument `json:"document"` |
|||
DocumentJSON string `json:"document_json"` |
|||
CreatedAt time.Time `json:"created_at"` |
|||
UpdatedAt time.Time `json:"updated_at"` |
|||
} |
|||
|
|||
type PoliciesCollection struct { |
|||
Policies map[string]credential.PolicyDocument `json:"policies"` |
|||
} |
|||
|
|||
type PoliciesData struct { |
|||
Username string `json:"username"` |
|||
Policies []IAMPolicy `json:"policies"` |
|||
TotalPolicies int `json:"total_policies"` |
|||
LastUpdated time.Time `json:"last_updated"` |
|||
} |
|||
|
|||
// Policy management request structures
|
|||
type CreatePolicyRequest struct { |
|||
Name string `json:"name" binding:"required"` |
|||
Document credential.PolicyDocument `json:"document" binding:"required"` |
|||
DocumentJSON string `json:"document_json"` |
|||
} |
|||
|
|||
type UpdatePolicyRequest struct { |
|||
Document credential.PolicyDocument `json:"document" binding:"required"` |
|||
DocumentJSON string `json:"document_json"` |
|||
} |
|||
|
|||
// PolicyManager interface is now in the credential package
|
|||
|
|||
// CredentialStorePolicyManager implements credential.PolicyManager by delegating to the credential store
|
|||
type CredentialStorePolicyManager struct { |
|||
credentialManager *credential.CredentialManager |
|||
} |
|||
|
|||
// NewCredentialStorePolicyManager creates a new CredentialStorePolicyManager
|
|||
func NewCredentialStorePolicyManager(credentialManager *credential.CredentialManager) *CredentialStorePolicyManager { |
|||
return &CredentialStorePolicyManager{ |
|||
credentialManager: credentialManager, |
|||
} |
|||
} |
|||
|
|||
// GetPolicies retrieves all IAM policies via credential store
|
|||
func (cspm *CredentialStorePolicyManager) GetPolicies(ctx context.Context) (map[string]credential.PolicyDocument, error) { |
|||
// Get policies from credential store
|
|||
// We'll use the credential store to access the filer indirectly
|
|||
// Since policies are stored separately, we need to access the underlying store
|
|||
store := cspm.credentialManager.GetStore() |
|||
glog.V(1).Infof("Getting policies from credential store: %T", store) |
|||
|
|||
// Check if the store supports policy management
|
|||
if policyStore, ok := store.(credential.PolicyManager); ok { |
|||
glog.V(1).Infof("Store supports policy management, calling GetPolicies") |
|||
policies, err := policyStore.GetPolicies(ctx) |
|||
if err != nil { |
|||
glog.Errorf("Error getting policies from store: %v", err) |
|||
return nil, err |
|||
} |
|||
glog.V(1).Infof("Got %d policies from store", len(policies)) |
|||
return policies, nil |
|||
} else { |
|||
// Fallback: use empty policies for stores that don't support policies
|
|||
glog.V(1).Infof("Credential store doesn't support policy management, returning empty policies") |
|||
return make(map[string]credential.PolicyDocument), nil |
|||
} |
|||
} |
|||
|
|||
// CreatePolicy creates a new IAM policy via credential store
|
|||
func (cspm *CredentialStorePolicyManager) CreatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { |
|||
store := cspm.credentialManager.GetStore() |
|||
|
|||
if policyStore, ok := store.(credential.PolicyManager); ok { |
|||
return policyStore.CreatePolicy(ctx, name, document) |
|||
} |
|||
|
|||
return fmt.Errorf("credential store doesn't support policy creation") |
|||
} |
|||
|
|||
// UpdatePolicy updates an existing IAM policy via credential store
|
|||
func (cspm *CredentialStorePolicyManager) UpdatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { |
|||
store := cspm.credentialManager.GetStore() |
|||
|
|||
if policyStore, ok := store.(credential.PolicyManager); ok { |
|||
return policyStore.UpdatePolicy(ctx, name, document) |
|||
} |
|||
|
|||
return fmt.Errorf("credential store doesn't support policy updates") |
|||
} |
|||
|
|||
// DeletePolicy deletes an IAM policy via credential store
|
|||
func (cspm *CredentialStorePolicyManager) DeletePolicy(ctx context.Context, name string) error { |
|||
store := cspm.credentialManager.GetStore() |
|||
|
|||
if policyStore, ok := store.(credential.PolicyManager); ok { |
|||
return policyStore.DeletePolicy(ctx, name) |
|||
} |
|||
|
|||
return fmt.Errorf("credential store doesn't support policy deletion") |
|||
} |
|||
|
|||
// GetPolicy retrieves a specific IAM policy via credential store
|
|||
func (cspm *CredentialStorePolicyManager) GetPolicy(ctx context.Context, name string) (*credential.PolicyDocument, error) { |
|||
store := cspm.credentialManager.GetStore() |
|||
|
|||
if policyStore, ok := store.(credential.PolicyManager); ok { |
|||
return policyStore.GetPolicy(ctx, name) |
|||
} |
|||
|
|||
return nil, fmt.Errorf("credential store doesn't support policy retrieval") |
|||
} |
|||
|
|||
// AdminServer policy management methods using credential.PolicyManager
|
|||
func (s *AdminServer) GetPolicyManager() credential.PolicyManager { |
|||
if s.credentialManager == nil { |
|||
glog.V(1).Infof("Credential manager is nil, policy management not available") |
|||
return nil |
|||
} |
|||
glog.V(1).Infof("Credential manager available, creating CredentialStorePolicyManager") |
|||
return NewCredentialStorePolicyManager(s.credentialManager) |
|||
} |
|||
|
|||
// GetPolicies retrieves all IAM policies
|
|||
func (s *AdminServer) GetPolicies() ([]IAMPolicy, error) { |
|||
policyManager := s.GetPolicyManager() |
|||
if policyManager == nil { |
|||
return nil, fmt.Errorf("policy manager not available") |
|||
} |
|||
|
|||
ctx := context.Background() |
|||
policyMap, err := policyManager.GetPolicies(ctx) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
// Convert map[string]PolicyDocument to []IAMPolicy
|
|||
var policies []IAMPolicy |
|||
for name, doc := range policyMap { |
|||
policy := IAMPolicy{ |
|||
Name: name, |
|||
Document: doc, |
|||
DocumentJSON: "", // Will be populated if needed
|
|||
CreatedAt: time.Now(), |
|||
UpdatedAt: time.Now(), |
|||
} |
|||
policies = append(policies, policy) |
|||
} |
|||
|
|||
return policies, nil |
|||
} |
|||
|
|||
// CreatePolicy creates a new IAM policy
|
|||
func (s *AdminServer) CreatePolicy(name string, document credential.PolicyDocument) error { |
|||
policyManager := s.GetPolicyManager() |
|||
if policyManager == nil { |
|||
return fmt.Errorf("policy manager not available") |
|||
} |
|||
|
|||
ctx := context.Background() |
|||
return policyManager.CreatePolicy(ctx, name, document) |
|||
} |
|||
|
|||
// UpdatePolicy updates an existing IAM policy
|
|||
func (s *AdminServer) UpdatePolicy(name string, document credential.PolicyDocument) error { |
|||
policyManager := s.GetPolicyManager() |
|||
if policyManager == nil { |
|||
return fmt.Errorf("policy manager not available") |
|||
} |
|||
|
|||
ctx := context.Background() |
|||
return policyManager.UpdatePolicy(ctx, name, document) |
|||
} |
|||
|
|||
// DeletePolicy deletes an IAM policy
|
|||
func (s *AdminServer) DeletePolicy(name string) error { |
|||
policyManager := s.GetPolicyManager() |
|||
if policyManager == nil { |
|||
return fmt.Errorf("policy manager not available") |
|||
} |
|||
|
|||
ctx := context.Background() |
|||
return policyManager.DeletePolicy(ctx, name) |
|||
} |
|||
|
|||
// GetPolicy retrieves a specific IAM policy
|
|||
func (s *AdminServer) GetPolicy(name string) (*IAMPolicy, error) { |
|||
policyManager := s.GetPolicyManager() |
|||
if policyManager == nil { |
|||
return nil, fmt.Errorf("policy manager not available") |
|||
} |
|||
|
|||
ctx := context.Background() |
|||
policyDoc, err := policyManager.GetPolicy(ctx, name) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
if policyDoc == nil { |
|||
return nil, nil |
|||
} |
|||
|
|||
// Convert PolicyDocument to IAMPolicy
|
|||
policy := &IAMPolicy{ |
|||
Name: name, |
|||
Document: *policyDoc, |
|||
DocumentJSON: "", // Will be populated if needed
|
|||
CreatedAt: time.Now(), |
|||
UpdatedAt: time.Now(), |
|||
} |
|||
|
|||
return policy, nil |
|||
} |
@ -0,0 +1,273 @@ |
|||
package handlers |
|||
|
|||
import ( |
|||
"fmt" |
|||
"net/http" |
|||
"time" |
|||
|
|||
"github.com/gin-gonic/gin" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/view/app" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout" |
|||
"github.com/seaweedfs/seaweedfs/weed/credential" |
|||
"github.com/seaweedfs/seaweedfs/weed/glog" |
|||
) |
|||
|
|||
// PolicyHandlers contains all the HTTP handlers for policy management
|
|||
type PolicyHandlers struct { |
|||
adminServer *dash.AdminServer |
|||
} |
|||
|
|||
// NewPolicyHandlers creates a new instance of PolicyHandlers
|
|||
func NewPolicyHandlers(adminServer *dash.AdminServer) *PolicyHandlers { |
|||
return &PolicyHandlers{ |
|||
adminServer: adminServer, |
|||
} |
|||
} |
|||
|
|||
// ShowPolicies renders the policies management page
|
|||
func (h *PolicyHandlers) ShowPolicies(c *gin.Context) { |
|||
// Get policies data from the server
|
|||
policiesData := h.getPoliciesData(c) |
|||
|
|||
// Render HTML template
|
|||
c.Header("Content-Type", "text/html") |
|||
policiesComponent := app.Policies(policiesData) |
|||
layoutComponent := layout.Layout(c, policiesComponent) |
|||
err := layoutComponent.Render(c.Request.Context(), c.Writer) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) |
|||
return |
|||
} |
|||
} |
|||
|
|||
// GetPolicies returns the list of policies as JSON
|
|||
func (h *PolicyHandlers) GetPolicies(c *gin.Context) { |
|||
policies, err := h.adminServer.GetPolicies() |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get policies: " + err.Error()}) |
|||
return |
|||
} |
|||
c.JSON(http.StatusOK, gin.H{"policies": policies}) |
|||
} |
|||
|
|||
// CreatePolicy handles policy creation
|
|||
func (h *PolicyHandlers) CreatePolicy(c *gin.Context) { |
|||
var req dash.CreatePolicyRequest |
|||
if err := c.ShouldBindJSON(&req); err != nil { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Validate policy name
|
|||
if req.Name == "" { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Policy name is required"}) |
|||
return |
|||
} |
|||
|
|||
// Check if policy already exists
|
|||
existingPolicy, err := h.adminServer.GetPolicy(req.Name) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing policy: " + err.Error()}) |
|||
return |
|||
} |
|||
if existingPolicy != nil { |
|||
c.JSON(http.StatusConflict, gin.H{"error": "Policy with this name already exists"}) |
|||
return |
|||
} |
|||
|
|||
// Create the policy
|
|||
err = h.adminServer.CreatePolicy(req.Name, req.Document) |
|||
if err != nil { |
|||
glog.Errorf("Failed to create policy %s: %v", req.Name, err) |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create policy: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
c.JSON(http.StatusCreated, gin.H{ |
|||
"success": true, |
|||
"message": "Policy created successfully", |
|||
"policy": req.Name, |
|||
}) |
|||
} |
|||
|
|||
// GetPolicy returns a specific policy
|
|||
func (h *PolicyHandlers) GetPolicy(c *gin.Context) { |
|||
policyName := c.Param("name") |
|||
if policyName == "" { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Policy name is required"}) |
|||
return |
|||
} |
|||
|
|||
policy, err := h.adminServer.GetPolicy(policyName) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get policy: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
if policy == nil { |
|||
c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"}) |
|||
return |
|||
} |
|||
|
|||
c.JSON(http.StatusOK, policy) |
|||
} |
|||
|
|||
// UpdatePolicy handles policy updates
|
|||
func (h *PolicyHandlers) UpdatePolicy(c *gin.Context) { |
|||
policyName := c.Param("name") |
|||
if policyName == "" { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Policy name is required"}) |
|||
return |
|||
} |
|||
|
|||
var req dash.UpdatePolicyRequest |
|||
if err := c.ShouldBindJSON(&req); err != nil { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Check if policy exists
|
|||
existingPolicy, err := h.adminServer.GetPolicy(policyName) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing policy: " + err.Error()}) |
|||
return |
|||
} |
|||
if existingPolicy == nil { |
|||
c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"}) |
|||
return |
|||
} |
|||
|
|||
// Update the policy
|
|||
err = h.adminServer.UpdatePolicy(policyName, req.Document) |
|||
if err != nil { |
|||
glog.Errorf("Failed to update policy %s: %v", policyName, err) |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update policy: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
c.JSON(http.StatusOK, gin.H{ |
|||
"success": true, |
|||
"message": "Policy updated successfully", |
|||
"policy": policyName, |
|||
}) |
|||
} |
|||
|
|||
// DeletePolicy handles policy deletion
|
|||
func (h *PolicyHandlers) DeletePolicy(c *gin.Context) { |
|||
policyName := c.Param("name") |
|||
if policyName == "" { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Policy name is required"}) |
|||
return |
|||
} |
|||
|
|||
// Check if policy exists
|
|||
existingPolicy, err := h.adminServer.GetPolicy(policyName) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing policy: " + err.Error()}) |
|||
return |
|||
} |
|||
if existingPolicy == nil { |
|||
c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"}) |
|||
return |
|||
} |
|||
|
|||
// Delete the policy
|
|||
err = h.adminServer.DeletePolicy(policyName) |
|||
if err != nil { |
|||
glog.Errorf("Failed to delete policy %s: %v", policyName, err) |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete policy: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
c.JSON(http.StatusOK, gin.H{ |
|||
"success": true, |
|||
"message": "Policy deleted successfully", |
|||
"policy": policyName, |
|||
}) |
|||
} |
|||
|
|||
// ValidatePolicy validates a policy document without saving it
|
|||
func (h *PolicyHandlers) ValidatePolicy(c *gin.Context) { |
|||
var req struct { |
|||
Document credential.PolicyDocument `json:"document" binding:"required"` |
|||
} |
|||
|
|||
if err := c.ShouldBindJSON(&req); err != nil { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Basic validation
|
|||
if req.Document.Version == "" { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Policy version is required"}) |
|||
return |
|||
} |
|||
|
|||
if len(req.Document.Statement) == 0 { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Policy must have at least one statement"}) |
|||
return |
|||
} |
|||
|
|||
// Validate each statement
|
|||
for i, statement := range req.Document.Statement { |
|||
if statement.Effect != "Allow" && statement.Effect != "Deny" { |
|||
c.JSON(http.StatusBadRequest, gin.H{ |
|||
"error": fmt.Sprintf("Statement %d: Effect must be 'Allow' or 'Deny'", i+1), |
|||
}) |
|||
return |
|||
} |
|||
|
|||
if len(statement.Action) == 0 { |
|||
c.JSON(http.StatusBadRequest, gin.H{ |
|||
"error": fmt.Sprintf("Statement %d: Action is required", i+1), |
|||
}) |
|||
return |
|||
} |
|||
|
|||
if len(statement.Resource) == 0 { |
|||
c.JSON(http.StatusBadRequest, gin.H{ |
|||
"error": fmt.Sprintf("Statement %d: Resource is required", i+1), |
|||
}) |
|||
return |
|||
} |
|||
} |
|||
|
|||
c.JSON(http.StatusOK, gin.H{ |
|||
"valid": true, |
|||
"message": "Policy document is valid", |
|||
}) |
|||
} |
|||
|
|||
// getPoliciesData retrieves policies data from the server
|
|||
func (h *PolicyHandlers) getPoliciesData(c *gin.Context) dash.PoliciesData { |
|||
username := c.GetString("username") |
|||
if username == "" { |
|||
username = "admin" |
|||
} |
|||
|
|||
// Get policies
|
|||
policies, err := h.adminServer.GetPolicies() |
|||
if err != nil { |
|||
glog.Errorf("Failed to get policies: %v", err) |
|||
// Return empty data on error
|
|||
return dash.PoliciesData{ |
|||
Username: username, |
|||
Policies: []dash.IAMPolicy{}, |
|||
TotalPolicies: 0, |
|||
LastUpdated: time.Now(), |
|||
} |
|||
} |
|||
|
|||
// Ensure policies is never nil
|
|||
if policies == nil { |
|||
policies = []dash.IAMPolicy{} |
|||
} |
|||
|
|||
return dash.PoliciesData{ |
|||
Username: username, |
|||
Policies: policies, |
|||
TotalPolicies: len(policies), |
|||
LastUpdated: time.Now(), |
|||
} |
|||
} |
87
weed/admin/view/app/cluster_collections_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
148
weed/admin/view/app/cluster_volume_servers_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
96
weed/admin/view/app/file_browser_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2
weed/admin/view/app/object_store_users_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,658 @@ |
|||
package app |
|||
|
|||
import ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
templ Policies(data dash.PoliciesData) { |
|||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> |
|||
<h1 class="h2"> |
|||
<i class="fas fa-shield-alt me-2"></i>IAM Policies |
|||
</h1> |
|||
<div class="btn-toolbar mb-2 mb-md-0"> |
|||
<div class="btn-group me-2"> |
|||
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#createPolicyModal"> |
|||
<i class="fas fa-plus me-1"></i>Create Policy |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div id="policies-content"> |
|||
<!-- Summary Cards --> |
|||
<div class="row mb-4"> |
|||
<div class="col-xl-4 col-md-6 mb-4"> |
|||
<div class="card border-left-primary shadow h-100 py-2"> |
|||
<div class="card-body"> |
|||
<div class="row no-gutters align-items-center"> |
|||
<div class="col mr-2"> |
|||
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1"> |
|||
Total Policies |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{fmt.Sprintf("%d", data.TotalPolicies)} |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-shield-alt fa-2x text-gray-300"></i> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-xl-4 col-md-6 mb-4"> |
|||
<div class="card border-left-success shadow h-100 py-2"> |
|||
<div class="card-body"> |
|||
<div class="row no-gutters align-items-center"> |
|||
<div class="col mr-2"> |
|||
<div class="text-xs font-weight-bold text-success text-uppercase mb-1"> |
|||
Active Policies |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{fmt.Sprintf("%d", data.TotalPolicies)} |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-check-circle fa-2x text-gray-300"></i> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-xl-4 col-md-6 mb-4"> |
|||
<div class="card border-left-info shadow h-100 py-2"> |
|||
<div class="card-body"> |
|||
<div class="row no-gutters align-items-center"> |
|||
<div class="col mr-2"> |
|||
<div class="text-xs font-weight-bold text-info text-uppercase mb-1"> |
|||
Last Updated |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{data.LastUpdated.Format("15:04")} |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-clock fa-2x text-gray-300"></i> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Policies Table --> |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<div class="card shadow mb-4"> |
|||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between"> |
|||
<h6 class="m-0 font-weight-bold text-primary"> |
|||
<i class="fas fa-shield-alt me-2"></i>IAM Policies |
|||
</h6> |
|||
<div class="dropdown no-arrow"> |
|||
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"> |
|||
<i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i> |
|||
</a> |
|||
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in"> |
|||
<div class="dropdown-header">Actions:</div> |
|||
<a class="dropdown-item" href="#"> |
|||
<i class="fas fa-download me-2"></i>Export List |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="table-responsive"> |
|||
<table class="table table-hover" width="100%" cellspacing="0"> |
|||
<thead> |
|||
<tr> |
|||
<th>Policy Name</th> |
|||
<th>Version</th> |
|||
<th>Statements</th> |
|||
<th>Created</th> |
|||
<th>Updated</th> |
|||
<th>Actions</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
for _, policy := range data.Policies { |
|||
<tr> |
|||
<td> |
|||
<strong>{policy.Name}</strong> |
|||
</td> |
|||
<td> |
|||
<span class="badge bg-info">{policy.Document.Version}</span> |
|||
</td> |
|||
<td> |
|||
<span class="badge bg-secondary">{fmt.Sprintf("%d statements", len(policy.Document.Statement))}</span> |
|||
</td> |
|||
<td> |
|||
<small class="text-muted">{policy.CreatedAt.Format("2006-01-02 15:04")}</small> |
|||
</td> |
|||
<td> |
|||
<small class="text-muted">{policy.UpdatedAt.Format("2006-01-02 15:04")}</small> |
|||
</td> |
|||
<td> |
|||
<div class="btn-group btn-group-sm" role="group"> |
|||
<button type="button" class="btn btn-outline-info view-policy-btn" title="View Policy" data-policy-name={policy.Name}> |
|||
<i class="fas fa-eye"></i> |
|||
</button> |
|||
<button type="button" class="btn btn-outline-primary edit-policy-btn" title="Edit Policy" data-policy-name={policy.Name}> |
|||
<i class="fas fa-edit"></i> |
|||
</button> |
|||
<button type="button" class="btn btn-outline-danger delete-policy-btn" title="Delete Policy" data-policy-name={policy.Name}> |
|||
<i class="fas fa-trash"></i> |
|||
</button> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
} |
|||
if len(data.Policies) == 0 { |
|||
<tr> |
|||
<td colspan="6" class="text-center text-muted py-4"> |
|||
<i class="fas fa-shield-alt fa-3x mb-3 text-muted"></i> |
|||
<div> |
|||
<h5>No IAM policies found</h5> |
|||
<p>Create your first policy to manage access permissions.</p> |
|||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createPolicyModal"> |
|||
<i class="fas fa-plus me-1"></i>Create Policy |
|||
</button> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Create Policy Modal --> |
|||
<div class="modal fade" id="createPolicyModal" tabindex="-1" aria-labelledby="createPolicyModalLabel" aria-hidden="true"> |
|||
<div class="modal-dialog modal-lg"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h5 class="modal-title" id="createPolicyModalLabel"> |
|||
<i class="fas fa-shield-alt me-2"></i>Create IAM Policy |
|||
</h5> |
|||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|||
</div> |
|||
<div class="modal-body"> |
|||
<form id="createPolicyForm"> |
|||
<div class="mb-3"> |
|||
<label for="policyName" class="form-label">Policy Name</label> |
|||
<input type="text" class="form-control" id="policyName" name="name" required placeholder="e.g., S3ReadOnlyPolicy"> |
|||
<div class="form-text">Enter a unique name for this policy (alphanumeric and underscores only)</div> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<label for="policyDocument" class="form-label">Policy Document</label> |
|||
<textarea class="form-control" id="policyDocument" name="document" rows="15" required placeholder="Enter IAM policy JSON document..."></textarea> |
|||
<div class="form-text">Enter the policy document in AWS IAM JSON format</div> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<div class="row"> |
|||
<div class="col-md-6"> |
|||
<button type="button" class="btn btn-outline-info btn-sm" onclick="insertSamplePolicy()"> |
|||
<i class="fas fa-file-alt me-1"></i>Use Sample Policy |
|||
</button> |
|||
</div> |
|||
<div class="col-md-6 text-end"> |
|||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="validatePolicyDocument()"> |
|||
<i class="fas fa-check me-1"></i>Validate JSON |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
<div class="modal-footer"> |
|||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
|||
<button type="button" class="btn btn-primary" onclick="createPolicy()"> |
|||
<i class="fas fa-plus me-1"></i>Create Policy |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- View Policy Modal --> |
|||
<div class="modal fade" id="viewPolicyModal" tabindex="-1" aria-labelledby="viewPolicyModalLabel" aria-hidden="true"> |
|||
<div class="modal-dialog modal-lg"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h5 class="modal-title" id="viewPolicyModalLabel"> |
|||
<i class="fas fa-eye me-2"></i>View IAM Policy |
|||
</h5> |
|||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|||
</div> |
|||
<div class="modal-body"> |
|||
<div id="viewPolicyContent"> |
|||
<div class="text-center"> |
|||
<div class="spinner-border" role="status"> |
|||
<span class="visually-hidden">Loading...</span> |
|||
</div> |
|||
<p class="mt-2">Loading policy...</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="modal-footer"> |
|||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
|||
<button type="button" class="btn btn-primary" id="editFromViewBtn"> |
|||
<i class="fas fa-edit me-1"></i>Edit Policy |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Edit Policy Modal --> |
|||
<div class="modal fade" id="editPolicyModal" tabindex="-1" aria-labelledby="editPolicyModalLabel" aria-hidden="true"> |
|||
<div class="modal-dialog modal-lg"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h5 class="modal-title" id="editPolicyModalLabel"> |
|||
<i class="fas fa-edit me-2"></i>Edit IAM Policy |
|||
</h5> |
|||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|||
</div> |
|||
<div class="modal-body"> |
|||
<form id="editPolicyForm"> |
|||
<div class="mb-3"> |
|||
<label for="editPolicyName" class="form-label">Policy Name</label> |
|||
<input type="text" class="form-control" id="editPolicyName" name="name" readonly> |
|||
<div class="form-text">Policy name cannot be changed</div> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<label for="editPolicyDocument" class="form-label">Policy Document</label> |
|||
<textarea class="form-control" id="editPolicyDocument" name="document" rows="15" required></textarea> |
|||
<div class="form-text">Edit the policy document in AWS IAM JSON format</div> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<div class="row"> |
|||
<div class="col-md-6"> |
|||
<button type="button" class="btn btn-outline-info btn-sm" onclick="insertSamplePolicyEdit()"> |
|||
<i class="fas fa-file-alt me-1"></i>Reset to Sample |
|||
</button> |
|||
</div> |
|||
<div class="col-md-6 text-end"> |
|||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="validateEditPolicyDocument()"> |
|||
<i class="fas fa-check me-1"></i>Validate JSON |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
<div class="modal-footer"> |
|||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
|||
<button type="button" class="btn btn-primary" onclick="updatePolicy()"> |
|||
<i class="fas fa-save me-1"></i>Save Changes |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- JavaScript for Policy Management --> |
|||
<script> |
|||
// Current policy being viewed/edited |
|||
let currentPolicy = null; |
|||
|
|||
// Event listeners for policy actions |
|||
document.addEventListener('DOMContentLoaded', function() { |
|||
// View policy buttons |
|||
document.querySelectorAll('.view-policy-btn').forEach(button => { |
|||
button.addEventListener('click', function() { |
|||
const policyName = this.getAttribute('data-policy-name'); |
|||
viewPolicy(policyName); |
|||
}); |
|||
}); |
|||
|
|||
// Edit policy buttons |
|||
document.querySelectorAll('.edit-policy-btn').forEach(button => { |
|||
button.addEventListener('click', function() { |
|||
const policyName = this.getAttribute('data-policy-name'); |
|||
editPolicy(policyName); |
|||
}); |
|||
}); |
|||
|
|||
// Delete policy buttons |
|||
document.querySelectorAll('.delete-policy-btn').forEach(button => { |
|||
button.addEventListener('click', function() { |
|||
const policyName = this.getAttribute('data-policy-name'); |
|||
deletePolicy(policyName); |
|||
}); |
|||
}); |
|||
|
|||
// Edit from view button |
|||
document.getElementById('editFromViewBtn').addEventListener('click', function() { |
|||
if (currentPolicy) { |
|||
const viewModal = bootstrap.Modal.getInstance(document.getElementById('viewPolicyModal')); |
|||
if (viewModal) viewModal.hide(); |
|||
editPolicy(currentPolicy.name); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
function createPolicy() { |
|||
const form = document.getElementById('createPolicyForm'); |
|||
const formData = new FormData(form); |
|||
|
|||
const policyName = formData.get('name'); |
|||
const policyDocumentText = formData.get('document'); |
|||
|
|||
if (!policyName || !policyDocumentText) { |
|||
alert('Please fill in all required fields'); |
|||
return; |
|||
} |
|||
|
|||
let policyDocument; |
|||
try { |
|||
policyDocument = JSON.parse(policyDocumentText); |
|||
} catch (e) { |
|||
alert('Invalid JSON in policy document: ' + e.message); |
|||
return; |
|||
} |
|||
|
|||
const requestData = { |
|||
name: policyName, |
|||
document: policyDocument |
|||
}; |
|||
|
|||
fetch('/api/object-store/policies', { |
|||
method: 'POST', |
|||
headers: { |
|||
'Content-Type': 'application/json', |
|||
}, |
|||
body: JSON.stringify(requestData) |
|||
}) |
|||
.then(response => response.json()) |
|||
.then(data => { |
|||
if (data.success) { |
|||
alert('Policy created successfully!'); |
|||
const modal = bootstrap.Modal.getInstance(document.getElementById('createPolicyModal')); |
|||
if (modal) modal.hide(); |
|||
location.reload(); // Refresh the page to show the new policy |
|||
} else { |
|||
alert('Error creating policy: ' + (data.error || 'Unknown error')); |
|||
} |
|||
}) |
|||
.catch(error => { |
|||
console.error('Error:', error); |
|||
alert('Error creating policy: ' + error.message); |
|||
}); |
|||
} |
|||
|
|||
function viewPolicy(policyName) { |
|||
// Show the modal first |
|||
const modal = new bootstrap.Modal(document.getElementById('viewPolicyModal')); |
|||
modal.show(); |
|||
|
|||
// Reset content to loading state |
|||
document.getElementById('viewPolicyContent').innerHTML = ` |
|||
<div class="text-center"> |
|||
<div class="spinner-border" role="status"> |
|||
<span class="visually-hidden">Loading...</span> |
|||
</div> |
|||
<p class="mt-2">Loading policy...</p> |
|||
</div> |
|||
`; |
|||
|
|||
// Fetch policy data |
|||
fetch('/api/object-store/policies/' + encodeURIComponent(policyName)) |
|||
.then(response => { |
|||
if (!response.ok) { |
|||
throw new Error('Policy not found'); |
|||
} |
|||
return response.json(); |
|||
}) |
|||
.then(policy => { |
|||
currentPolicy = policy; |
|||
displayPolicyDetails(policy); |
|||
}) |
|||
.catch(error => { |
|||
console.error('Error:', error); |
|||
document.getElementById('viewPolicyContent').innerHTML = ` |
|||
<div class="alert alert-danger" role="alert"> |
|||
<i class="fas fa-exclamation-triangle me-2"></i> |
|||
Error loading policy: ${error.message} |
|||
</div> |
|||
`; |
|||
}); |
|||
} |
|||
|
|||
function displayPolicyDetails(policy) { |
|||
const content = document.getElementById('viewPolicyContent'); |
|||
|
|||
let statementsHtml = ''; |
|||
if (policy.document && policy.document.Statement) { |
|||
statementsHtml = policy.document.Statement.map((stmt, index) => ` |
|||
<div class="card mb-2"> |
|||
<div class="card-header py-2"> |
|||
<h6 class="mb-0">Statement ${index + 1}</h6> |
|||
</div> |
|||
<div class="card-body py-2"> |
|||
<div class="row"> |
|||
<div class="col-md-6"> |
|||
<strong>Effect:</strong> |
|||
<span class="badge ${stmt.Effect === 'Allow' ? 'bg-success' : 'bg-danger'}">${stmt.Effect}</span> |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<strong>Actions:</strong> ${Array.isArray(stmt.Action) ? stmt.Action.join(', ') : stmt.Action} |
|||
</div> |
|||
</div> |
|||
<div class="row mt-2"> |
|||
<div class="col-12"> |
|||
<strong>Resources:</strong> ${Array.isArray(stmt.Resource) ? stmt.Resource.join(', ') : stmt.Resource} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
`).join(''); |
|||
} |
|||
|
|||
content.innerHTML = ` |
|||
<div class="row mb-3"> |
|||
<div class="col-md-6"> |
|||
<strong>Policy Name:</strong> ${policy.name || 'Unknown'} |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<strong>Version:</strong> <span class="badge bg-info">${policy.document?.Version || 'Unknown'}</span> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<strong>Statements:</strong> |
|||
<div class="mt-2"> |
|||
${statementsHtml || '<p class="text-muted">No statements found</p>'} |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<strong>Raw Policy Document:</strong> |
|||
<pre class="bg-light p-3 border rounded mt-2"><code>${JSON.stringify(policy.document, null, 2)}</code></pre> |
|||
</div> |
|||
`; |
|||
} |
|||
|
|||
function editPolicy(policyName) { |
|||
// Show the modal first |
|||
const modal = new bootstrap.Modal(document.getElementById('editPolicyModal')); |
|||
modal.show(); |
|||
|
|||
// Set policy name |
|||
document.getElementById('editPolicyName').value = policyName; |
|||
document.getElementById('editPolicyDocument').value = 'Loading...'; |
|||
|
|||
// Fetch policy data |
|||
fetch('/api/object-store/policies/' + encodeURIComponent(policyName)) |
|||
.then(response => { |
|||
if (!response.ok) { |
|||
throw new Error('Policy not found'); |
|||
} |
|||
return response.json(); |
|||
}) |
|||
.then(policy => { |
|||
currentPolicy = policy; |
|||
document.getElementById('editPolicyDocument').value = JSON.stringify(policy.document, null, 2); |
|||
}) |
|||
.catch(error => { |
|||
console.error('Error:', error); |
|||
alert('Error loading policy for editing: ' + error.message); |
|||
const editModal = bootstrap.Modal.getInstance(document.getElementById('editPolicyModal')); |
|||
if (editModal) editModal.hide(); |
|||
}); |
|||
} |
|||
|
|||
function updatePolicy() { |
|||
const policyName = document.getElementById('editPolicyName').value; |
|||
const policyDocumentText = document.getElementById('editPolicyDocument').value; |
|||
|
|||
if (!policyName || !policyDocumentText) { |
|||
alert('Please fill in all required fields'); |
|||
return; |
|||
} |
|||
|
|||
let policyDocument; |
|||
try { |
|||
policyDocument = JSON.parse(policyDocumentText); |
|||
} catch (e) { |
|||
alert('Invalid JSON in policy document: ' + e.message); |
|||
return; |
|||
} |
|||
|
|||
const requestData = { |
|||
document: policyDocument |
|||
}; |
|||
|
|||
fetch('/api/object-store/policies/' + encodeURIComponent(policyName), { |
|||
method: 'PUT', |
|||
headers: { |
|||
'Content-Type': 'application/json', |
|||
}, |
|||
body: JSON.stringify(requestData) |
|||
}) |
|||
.then(response => response.json()) |
|||
.then(data => { |
|||
if (data.success) { |
|||
alert('Policy updated successfully!'); |
|||
const modal = bootstrap.Modal.getInstance(document.getElementById('editPolicyModal')); |
|||
if (modal) modal.hide(); |
|||
location.reload(); // Refresh the page to show the updated policy |
|||
} else { |
|||
alert('Error updating policy: ' + (data.error || 'Unknown error')); |
|||
} |
|||
}) |
|||
.catch(error => { |
|||
console.error('Error:', error); |
|||
alert('Error updating policy: ' + error.message); |
|||
}); |
|||
} |
|||
|
|||
function insertSamplePolicy() { |
|||
const samplePolicy = { |
|||
"Version": "2012-10-17", |
|||
"Statement": [ |
|||
{ |
|||
"Effect": "Allow", |
|||
"Action": [ |
|||
"s3:GetObject", |
|||
"s3:PutObject" |
|||
], |
|||
"Resource": [ |
|||
"arn:aws:s3:::my-bucket/*" |
|||
] |
|||
} |
|||
] |
|||
}; |
|||
|
|||
document.getElementById('policyDocument').value = JSON.stringify(samplePolicy, null, 2); |
|||
} |
|||
|
|||
function insertSamplePolicyEdit() { |
|||
const samplePolicy = { |
|||
"Version": "2012-10-17", |
|||
"Statement": [ |
|||
{ |
|||
"Effect": "Allow", |
|||
"Action": [ |
|||
"s3:GetObject", |
|||
"s3:PutObject" |
|||
], |
|||
"Resource": [ |
|||
"arn:aws:s3:::my-bucket/*" |
|||
] |
|||
} |
|||
] |
|||
}; |
|||
|
|||
document.getElementById('editPolicyDocument').value = JSON.stringify(samplePolicy, null, 2); |
|||
} |
|||
|
|||
function validatePolicyDocument() { |
|||
const policyText = document.getElementById('policyDocument').value; |
|||
validatePolicyJSON(policyText); |
|||
} |
|||
|
|||
function validateEditPolicyDocument() { |
|||
const policyText = document.getElementById('editPolicyDocument').value; |
|||
validatePolicyJSON(policyText); |
|||
} |
|||
|
|||
function validatePolicyJSON(policyText) { |
|||
if (!policyText) { |
|||
alert('Please enter a policy document first'); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
const policy = JSON.parse(policyText); |
|||
|
|||
// Basic validation |
|||
if (!policy.Version) { |
|||
alert('Policy must have a Version field'); |
|||
return; |
|||
} |
|||
|
|||
if (!policy.Statement || !Array.isArray(policy.Statement)) { |
|||
alert('Policy must have a Statement array'); |
|||
return; |
|||
} |
|||
|
|||
alert('Policy document is valid JSON!'); |
|||
} catch (e) { |
|||
alert('Invalid JSON: ' + e.message); |
|||
} |
|||
} |
|||
|
|||
function deletePolicy(policyName) { |
|||
if (confirm('Are you sure you want to delete policy "' + policyName + '"?')) { |
|||
fetch('/api/object-store/policies/' + encodeURIComponent(policyName), { |
|||
method: 'DELETE' |
|||
}) |
|||
.then(response => response.json()) |
|||
.then(data => { |
|||
if (data.success) { |
|||
alert('Policy deleted successfully!'); |
|||
location.reload(); // Refresh the page |
|||
} else { |
|||
alert('Error deleting policy: ' + (data.error || 'Unknown error')); |
|||
} |
|||
}) |
|||
.catch(error => { |
|||
console.error('Error:', error); |
|||
alert('Error deleting policy: ' + error.message); |
|||
}); |
|||
} |
|||
} |
|||
</script> |
|||
} |
204
weed/admin/view/app/policies_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
22
weed/admin/view/app/s3_buckets_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,188 @@ |
|||
package filer_etc |
|||
|
|||
import ( |
|||
"bytes" |
|||
"context" |
|||
"fmt" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/credential" |
|||
"github.com/seaweedfs/seaweedfs/weed/filer" |
|||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
|||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" |
|||
) |
|||
|
|||
func (store *FilerEtcStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) { |
|||
s3cfg := &iam_pb.S3ApiConfiguration{} |
|||
|
|||
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { |
|||
var buf bytes.Buffer |
|||
if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamIdentityFile, &buf); err != nil { |
|||
if err != filer_pb.ErrNotFound { |
|||
return err |
|||
} |
|||
} |
|||
if buf.Len() > 0 { |
|||
return filer.ParseS3ConfigurationFromBytes(buf.Bytes(), s3cfg) |
|||
} |
|||
return nil |
|||
}) |
|||
|
|||
return s3cfg, err |
|||
} |
|||
|
|||
func (store *FilerEtcStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error { |
|||
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { |
|||
var buf bytes.Buffer |
|||
if err := filer.ProtoToText(&buf, config); err != nil { |
|||
return fmt.Errorf("failed to marshal configuration: %v", err) |
|||
} |
|||
return filer.SaveInsideFiler(client, filer.IamConfigDirectory, filer.IamIdentityFile, buf.Bytes()) |
|||
}) |
|||
} |
|||
|
|||
func (store *FilerEtcStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error { |
|||
// Load existing configuration
|
|||
config, err := store.LoadConfiguration(ctx) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to load configuration: %v", err) |
|||
} |
|||
|
|||
// Check if user already exists
|
|||
for _, existingIdentity := range config.Identities { |
|||
if existingIdentity.Name == identity.Name { |
|||
return credential.ErrUserAlreadyExists |
|||
} |
|||
} |
|||
|
|||
// Add new identity
|
|||
config.Identities = append(config.Identities, identity) |
|||
|
|||
// Save configuration
|
|||
return store.SaveConfiguration(ctx, config) |
|||
} |
|||
|
|||
func (store *FilerEtcStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) { |
|||
config, err := store.LoadConfiguration(ctx) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("failed to load configuration: %v", err) |
|||
} |
|||
|
|||
for _, identity := range config.Identities { |
|||
if identity.Name == username { |
|||
return identity, nil |
|||
} |
|||
} |
|||
|
|||
return nil, credential.ErrUserNotFound |
|||
} |
|||
|
|||
func (store *FilerEtcStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error { |
|||
config, err := store.LoadConfiguration(ctx) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to load configuration: %v", err) |
|||
} |
|||
|
|||
// Find and update the user
|
|||
for i, existingIdentity := range config.Identities { |
|||
if existingIdentity.Name == username { |
|||
config.Identities[i] = identity |
|||
return store.SaveConfiguration(ctx, config) |
|||
} |
|||
} |
|||
|
|||
return credential.ErrUserNotFound |
|||
} |
|||
|
|||
func (store *FilerEtcStore) DeleteUser(ctx context.Context, username string) error { |
|||
config, err := store.LoadConfiguration(ctx) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to load configuration: %v", err) |
|||
} |
|||
|
|||
// Find and remove the user
|
|||
for i, identity := range config.Identities { |
|||
if identity.Name == username { |
|||
config.Identities = append(config.Identities[:i], config.Identities[i+1:]...) |
|||
return store.SaveConfiguration(ctx, config) |
|||
} |
|||
} |
|||
|
|||
return credential.ErrUserNotFound |
|||
} |
|||
|
|||
func (store *FilerEtcStore) ListUsers(ctx context.Context) ([]string, error) { |
|||
config, err := store.LoadConfiguration(ctx) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("failed to load configuration: %v", err) |
|||
} |
|||
|
|||
var usernames []string |
|||
for _, identity := range config.Identities { |
|||
usernames = append(usernames, identity.Name) |
|||
} |
|||
|
|||
return usernames, nil |
|||
} |
|||
|
|||
func (store *FilerEtcStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) { |
|||
config, err := store.LoadConfiguration(ctx) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("failed to load configuration: %v", err) |
|||
} |
|||
|
|||
for _, identity := range config.Identities { |
|||
for _, credential := range identity.Credentials { |
|||
if credential.AccessKey == accessKey { |
|||
return identity, nil |
|||
} |
|||
} |
|||
} |
|||
|
|||
return nil, credential.ErrAccessKeyNotFound |
|||
} |
|||
|
|||
func (store *FilerEtcStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) error { |
|||
config, err := store.LoadConfiguration(ctx) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to load configuration: %v", err) |
|||
} |
|||
|
|||
// Find the user and add the credential
|
|||
for _, identity := range config.Identities { |
|||
if identity.Name == username { |
|||
// Check if access key already exists
|
|||
for _, existingCred := range identity.Credentials { |
|||
if existingCred.AccessKey == cred.AccessKey { |
|||
return fmt.Errorf("access key %s already exists", cred.AccessKey) |
|||
} |
|||
} |
|||
|
|||
identity.Credentials = append(identity.Credentials, cred) |
|||
return store.SaveConfiguration(ctx, config) |
|||
} |
|||
} |
|||
|
|||
return credential.ErrUserNotFound |
|||
} |
|||
|
|||
func (store *FilerEtcStore) DeleteAccessKey(ctx context.Context, username string, accessKey string) error { |
|||
config, err := store.LoadConfiguration(ctx) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to load configuration: %v", err) |
|||
} |
|||
|
|||
// Find the user and remove the credential
|
|||
for _, identity := range config.Identities { |
|||
if identity.Name == username { |
|||
for i, cred := range identity.Credentials { |
|||
if cred.AccessKey == accessKey { |
|||
identity.Credentials = append(identity.Credentials[:i], identity.Credentials[i+1:]...) |
|||
return store.SaveConfiguration(ctx, config) |
|||
} |
|||
} |
|||
return credential.ErrAccessKeyNotFound |
|||
} |
|||
} |
|||
|
|||
return credential.ErrUserNotFound |
|||
} |
@ -0,0 +1,114 @@ |
|||
package filer_etc |
|||
|
|||
import ( |
|||
"bytes" |
|||
"context" |
|||
"encoding/json" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/credential" |
|||
"github.com/seaweedfs/seaweedfs/weed/filer" |
|||
"github.com/seaweedfs/seaweedfs/weed/glog" |
|||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
|||
) |
|||
|
|||
type PoliciesCollection struct { |
|||
Policies map[string]credential.PolicyDocument `json:"policies"` |
|||
} |
|||
|
|||
// GetPolicies retrieves all IAM policies from the filer
|
|||
func (store *FilerEtcStore) GetPolicies(ctx context.Context) (map[string]credential.PolicyDocument, error) { |
|||
policiesCollection := &PoliciesCollection{ |
|||
Policies: make(map[string]credential.PolicyDocument), |
|||
} |
|||
|
|||
// Check if filer client is configured
|
|||
if store.filerGrpcAddress == "" { |
|||
glog.V(1).Infof("Filer client not configured for policy retrieval, returning empty policies") |
|||
// Return empty policies if filer client is not configured
|
|||
return policiesCollection.Policies, nil |
|||
} |
|||
|
|||
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { |
|||
var buf bytes.Buffer |
|||
if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamPoliciesFile, &buf); err != nil { |
|||
if err == filer_pb.ErrNotFound { |
|||
glog.V(1).Infof("Policies file not found at %s/%s, returning empty policies", filer.IamConfigDirectory, filer.IamPoliciesFile) |
|||
// If file doesn't exist, return empty collection
|
|||
return nil |
|||
} |
|||
return err |
|||
} |
|||
|
|||
if buf.Len() > 0 { |
|||
return json.Unmarshal(buf.Bytes(), policiesCollection) |
|||
} |
|||
return nil |
|||
}) |
|||
|
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return policiesCollection.Policies, nil |
|||
} |
|||
|
|||
// CreatePolicy creates a new IAM policy in the filer
|
|||
func (store *FilerEtcStore) CreatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { |
|||
return store.updatePolicies(ctx, func(policies map[string]credential.PolicyDocument) { |
|||
policies[name] = document |
|||
}) |
|||
} |
|||
|
|||
// UpdatePolicy updates an existing IAM policy in the filer
|
|||
func (store *FilerEtcStore) UpdatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { |
|||
return store.updatePolicies(ctx, func(policies map[string]credential.PolicyDocument) { |
|||
policies[name] = document |
|||
}) |
|||
} |
|||
|
|||
// DeletePolicy deletes an IAM policy from the filer
|
|||
func (store *FilerEtcStore) DeletePolicy(ctx context.Context, name string) error { |
|||
return store.updatePolicies(ctx, func(policies map[string]credential.PolicyDocument) { |
|||
delete(policies, name) |
|||
}) |
|||
} |
|||
|
|||
// updatePolicies is a helper method to update policies atomically
|
|||
func (store *FilerEtcStore) updatePolicies(ctx context.Context, updateFunc func(map[string]credential.PolicyDocument)) error { |
|||
// Load existing policies
|
|||
policies, err := store.GetPolicies(ctx) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// Apply update
|
|||
updateFunc(policies) |
|||
|
|||
// Save back to filer
|
|||
policiesCollection := &PoliciesCollection{ |
|||
Policies: policies, |
|||
} |
|||
|
|||
data, err := json.Marshal(policiesCollection) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { |
|||
return filer.SaveInsideFiler(client, filer.IamConfigDirectory, filer.IamPoliciesFile, data) |
|||
}) |
|||
} |
|||
|
|||
// GetPolicy retrieves a specific IAM policy by name from the filer
|
|||
func (store *FilerEtcStore) GetPolicy(ctx context.Context, name string) (*credential.PolicyDocument, error) { |
|||
policies, err := store.GetPolicies(ctx) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
if policy, exists := policies[name]; exists { |
|||
return &policy, nil |
|||
} |
|||
|
|||
return nil, nil // Policy not found
|
|||
} |
@ -0,0 +1,302 @@ |
|||
package memory |
|||
|
|||
import ( |
|||
"context" |
|||
"encoding/json" |
|||
"fmt" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/credential" |
|||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" |
|||
) |
|||
|
|||
func (store *MemoryStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) { |
|||
store.mu.RLock() |
|||
defer store.mu.RUnlock() |
|||
|
|||
if !store.initialized { |
|||
return nil, fmt.Errorf("store not initialized") |
|||
} |
|||
|
|||
config := &iam_pb.S3ApiConfiguration{} |
|||
|
|||
// Convert all users to identities
|
|||
for _, user := range store.users { |
|||
// Deep copy the identity to avoid mutation issues
|
|||
identityCopy := store.deepCopyIdentity(user) |
|||
config.Identities = append(config.Identities, identityCopy) |
|||
} |
|||
|
|||
return config, nil |
|||
} |
|||
|
|||
func (store *MemoryStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error { |
|||
store.mu.Lock() |
|||
defer store.mu.Unlock() |
|||
|
|||
if !store.initialized { |
|||
return fmt.Errorf("store not initialized") |
|||
} |
|||
|
|||
// Clear existing data
|
|||
store.users = make(map[string]*iam_pb.Identity) |
|||
store.accessKeys = make(map[string]string) |
|||
|
|||
// Add all identities
|
|||
for _, identity := range config.Identities { |
|||
// Deep copy to avoid mutation issues
|
|||
identityCopy := store.deepCopyIdentity(identity) |
|||
store.users[identity.Name] = identityCopy |
|||
|
|||
// Index access keys
|
|||
for _, credential := range identity.Credentials { |
|||
store.accessKeys[credential.AccessKey] = identity.Name |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (store *MemoryStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error { |
|||
store.mu.Lock() |
|||
defer store.mu.Unlock() |
|||
|
|||
if !store.initialized { |
|||
return fmt.Errorf("store not initialized") |
|||
} |
|||
|
|||
if _, exists := store.users[identity.Name]; exists { |
|||
return credential.ErrUserAlreadyExists |
|||
} |
|||
|
|||
// Check for duplicate access keys
|
|||
for _, cred := range identity.Credentials { |
|||
if _, exists := store.accessKeys[cred.AccessKey]; exists { |
|||
return fmt.Errorf("access key %s already exists", cred.AccessKey) |
|||
} |
|||
} |
|||
|
|||
// Deep copy to avoid mutation issues
|
|||
identityCopy := store.deepCopyIdentity(identity) |
|||
store.users[identity.Name] = identityCopy |
|||
|
|||
// Index access keys
|
|||
for _, cred := range identity.Credentials { |
|||
store.accessKeys[cred.AccessKey] = identity.Name |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (store *MemoryStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) { |
|||
store.mu.RLock() |
|||
defer store.mu.RUnlock() |
|||
|
|||
if !store.initialized { |
|||
return nil, fmt.Errorf("store not initialized") |
|||
} |
|||
|
|||
user, exists := store.users[username] |
|||
if !exists { |
|||
return nil, credential.ErrUserNotFound |
|||
} |
|||
|
|||
// Return a deep copy to avoid mutation issues
|
|||
return store.deepCopyIdentity(user), nil |
|||
} |
|||
|
|||
func (store *MemoryStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error { |
|||
store.mu.Lock() |
|||
defer store.mu.Unlock() |
|||
|
|||
if !store.initialized { |
|||
return fmt.Errorf("store not initialized") |
|||
} |
|||
|
|||
existingUser, exists := store.users[username] |
|||
if !exists { |
|||
return credential.ErrUserNotFound |
|||
} |
|||
|
|||
// Remove old access keys from index
|
|||
for _, cred := range existingUser.Credentials { |
|||
delete(store.accessKeys, cred.AccessKey) |
|||
} |
|||
|
|||
// Check for duplicate access keys (excluding current user)
|
|||
for _, cred := range identity.Credentials { |
|||
if existingUsername, exists := store.accessKeys[cred.AccessKey]; exists && existingUsername != username { |
|||
return fmt.Errorf("access key %s already exists", cred.AccessKey) |
|||
} |
|||
} |
|||
|
|||
// Deep copy to avoid mutation issues
|
|||
identityCopy := store.deepCopyIdentity(identity) |
|||
store.users[username] = identityCopy |
|||
|
|||
// Re-index access keys
|
|||
for _, cred := range identity.Credentials { |
|||
store.accessKeys[cred.AccessKey] = username |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (store *MemoryStore) DeleteUser(ctx context.Context, username string) error { |
|||
store.mu.Lock() |
|||
defer store.mu.Unlock() |
|||
|
|||
if !store.initialized { |
|||
return fmt.Errorf("store not initialized") |
|||
} |
|||
|
|||
user, exists := store.users[username] |
|||
if !exists { |
|||
return credential.ErrUserNotFound |
|||
} |
|||
|
|||
// Remove access keys from index
|
|||
for _, cred := range user.Credentials { |
|||
delete(store.accessKeys, cred.AccessKey) |
|||
} |
|||
|
|||
// Remove user
|
|||
delete(store.users, username) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (store *MemoryStore) ListUsers(ctx context.Context) ([]string, error) { |
|||
store.mu.RLock() |
|||
defer store.mu.RUnlock() |
|||
|
|||
if !store.initialized { |
|||
return nil, fmt.Errorf("store not initialized") |
|||
} |
|||
|
|||
var usernames []string |
|||
for username := range store.users { |
|||
usernames = append(usernames, username) |
|||
} |
|||
|
|||
return usernames, nil |
|||
} |
|||
|
|||
func (store *MemoryStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) { |
|||
store.mu.RLock() |
|||
defer store.mu.RUnlock() |
|||
|
|||
if !store.initialized { |
|||
return nil, fmt.Errorf("store not initialized") |
|||
} |
|||
|
|||
username, exists := store.accessKeys[accessKey] |
|||
if !exists { |
|||
return nil, credential.ErrAccessKeyNotFound |
|||
} |
|||
|
|||
user, exists := store.users[username] |
|||
if !exists { |
|||
// This should not happen, but handle it gracefully
|
|||
return nil, credential.ErrUserNotFound |
|||
} |
|||
|
|||
// Return a deep copy to avoid mutation issues
|
|||
return store.deepCopyIdentity(user), nil |
|||
} |
|||
|
|||
func (store *MemoryStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) error { |
|||
store.mu.Lock() |
|||
defer store.mu.Unlock() |
|||
|
|||
if !store.initialized { |
|||
return fmt.Errorf("store not initialized") |
|||
} |
|||
|
|||
user, exists := store.users[username] |
|||
if !exists { |
|||
return credential.ErrUserNotFound |
|||
} |
|||
|
|||
// Check if access key already exists
|
|||
if _, exists := store.accessKeys[cred.AccessKey]; exists { |
|||
return fmt.Errorf("access key %s already exists", cred.AccessKey) |
|||
} |
|||
|
|||
// Add credential to user
|
|||
user.Credentials = append(user.Credentials, &iam_pb.Credential{ |
|||
AccessKey: cred.AccessKey, |
|||
SecretKey: cred.SecretKey, |
|||
}) |
|||
|
|||
// Index the access key
|
|||
store.accessKeys[cred.AccessKey] = username |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (store *MemoryStore) DeleteAccessKey(ctx context.Context, username string, accessKey string) error { |
|||
store.mu.Lock() |
|||
defer store.mu.Unlock() |
|||
|
|||
if !store.initialized { |
|||
return fmt.Errorf("store not initialized") |
|||
} |
|||
|
|||
user, exists := store.users[username] |
|||
if !exists { |
|||
return credential.ErrUserNotFound |
|||
} |
|||
|
|||
// Find and remove the credential
|
|||
var newCredentials []*iam_pb.Credential |
|||
found := false |
|||
for _, cred := range user.Credentials { |
|||
if cred.AccessKey == accessKey { |
|||
found = true |
|||
// Remove from access key index
|
|||
delete(store.accessKeys, accessKey) |
|||
} else { |
|||
newCredentials = append(newCredentials, cred) |
|||
} |
|||
} |
|||
|
|||
if !found { |
|||
return credential.ErrAccessKeyNotFound |
|||
} |
|||
|
|||
user.Credentials = newCredentials |
|||
return nil |
|||
} |
|||
|
|||
// deepCopyIdentity creates a deep copy of an identity to avoid mutation issues
|
|||
func (store *MemoryStore) deepCopyIdentity(identity *iam_pb.Identity) *iam_pb.Identity { |
|||
if identity == nil { |
|||
return nil |
|||
} |
|||
|
|||
// Use JSON marshaling/unmarshaling for deep copy
|
|||
// This is simple and safe for protobuf messages
|
|||
data, err := json.Marshal(identity) |
|||
if err != nil { |
|||
// Fallback to shallow copy if JSON fails
|
|||
return &iam_pb.Identity{ |
|||
Name: identity.Name, |
|||
Account: identity.Account, |
|||
Credentials: identity.Credentials, |
|||
Actions: identity.Actions, |
|||
} |
|||
} |
|||
|
|||
var copy iam_pb.Identity |
|||
if err := json.Unmarshal(data, ©); err != nil { |
|||
// Fallback to shallow copy if JSON fails
|
|||
return &iam_pb.Identity{ |
|||
Name: identity.Name, |
|||
Account: identity.Account, |
|||
Credentials: identity.Credentials, |
|||
Actions: identity.Actions, |
|||
} |
|||
} |
|||
|
|||
return © |
|||
} |
@ -0,0 +1,77 @@ |
|||
package memory |
|||
|
|||
import ( |
|||
"context" |
|||
"fmt" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/credential" |
|||
) |
|||
|
|||
// GetPolicies retrieves all IAM policies from memory
|
|||
func (store *MemoryStore) GetPolicies(ctx context.Context) (map[string]credential.PolicyDocument, error) { |
|||
store.mu.RLock() |
|||
defer store.mu.RUnlock() |
|||
|
|||
if !store.initialized { |
|||
return nil, fmt.Errorf("store not initialized") |
|||
} |
|||
|
|||
// Create a copy of the policies map to avoid mutation issues
|
|||
policies := make(map[string]credential.PolicyDocument) |
|||
for name, doc := range store.policies { |
|||
policies[name] = doc |
|||
} |
|||
|
|||
return policies, nil |
|||
} |
|||
|
|||
// GetPolicy retrieves a specific IAM policy by name from memory
|
|||
func (store *MemoryStore) GetPolicy(ctx context.Context, name string) (*credential.PolicyDocument, error) { |
|||
store.mu.RLock() |
|||
defer store.mu.RUnlock() |
|||
|
|||
if policy, exists := store.policies[name]; exists { |
|||
return &policy, nil |
|||
} |
|||
|
|||
return nil, nil // Policy not found
|
|||
} |
|||
|
|||
// CreatePolicy creates a new IAM policy in memory
|
|||
func (store *MemoryStore) CreatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { |
|||
store.mu.Lock() |
|||
defer store.mu.Unlock() |
|||
|
|||
if !store.initialized { |
|||
return fmt.Errorf("store not initialized") |
|||
} |
|||
|
|||
store.policies[name] = document |
|||
return nil |
|||
} |
|||
|
|||
// UpdatePolicy updates an existing IAM policy in memory
|
|||
func (store *MemoryStore) UpdatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { |
|||
store.mu.Lock() |
|||
defer store.mu.Unlock() |
|||
|
|||
if !store.initialized { |
|||
return fmt.Errorf("store not initialized") |
|||
} |
|||
|
|||
store.policies[name] = document |
|||
return nil |
|||
} |
|||
|
|||
// DeletePolicy deletes an IAM policy from memory
|
|||
func (store *MemoryStore) DeletePolicy(ctx context.Context, name string) error { |
|||
store.mu.Lock() |
|||
defer store.mu.Unlock() |
|||
|
|||
if !store.initialized { |
|||
return fmt.Errorf("store not initialized") |
|||
} |
|||
|
|||
delete(store.policies, name) |
|||
return nil |
|||
} |
@ -0,0 +1,446 @@ |
|||
package postgres |
|||
|
|||
import ( |
|||
"context" |
|||
"database/sql" |
|||
"encoding/json" |
|||
"fmt" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/credential" |
|||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" |
|||
) |
|||
|
|||
func (store *PostgresStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) { |
|||
if !store.configured { |
|||
return nil, fmt.Errorf("store not configured") |
|||
} |
|||
|
|||
config := &iam_pb.S3ApiConfiguration{} |
|||
|
|||
// Query all users
|
|||
rows, err := store.db.QueryContext(ctx, "SELECT username, email, account_data, actions FROM users") |
|||
if err != nil { |
|||
return nil, fmt.Errorf("failed to query users: %v", err) |
|||
} |
|||
defer rows.Close() |
|||
|
|||
for rows.Next() { |
|||
var username, email string |
|||
var accountDataJSON, actionsJSON []byte |
|||
|
|||
if err := rows.Scan(&username, &email, &accountDataJSON, &actionsJSON); err != nil { |
|||
return nil, fmt.Errorf("failed to scan user row: %v", err) |
|||
} |
|||
|
|||
identity := &iam_pb.Identity{ |
|||
Name: username, |
|||
} |
|||
|
|||
// Parse account data
|
|||
if len(accountDataJSON) > 0 { |
|||
if err := json.Unmarshal(accountDataJSON, &identity.Account); err != nil { |
|||
return nil, fmt.Errorf("failed to unmarshal account data for user %s: %v", username, err) |
|||
} |
|||
} |
|||
|
|||
// Parse actions
|
|||
if len(actionsJSON) > 0 { |
|||
if err := json.Unmarshal(actionsJSON, &identity.Actions); err != nil { |
|||
return nil, fmt.Errorf("failed to unmarshal actions for user %s: %v", username, err) |
|||
} |
|||
} |
|||
|
|||
// Query credentials for this user
|
|||
credRows, err := store.db.QueryContext(ctx, "SELECT access_key, secret_key FROM credentials WHERE username = $1", username) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("failed to query credentials for user %s: %v", username, err) |
|||
} |
|||
|
|||
for credRows.Next() { |
|||
var accessKey, secretKey string |
|||
if err := credRows.Scan(&accessKey, &secretKey); err != nil { |
|||
credRows.Close() |
|||
return nil, fmt.Errorf("failed to scan credential row for user %s: %v", username, err) |
|||
} |
|||
|
|||
identity.Credentials = append(identity.Credentials, &iam_pb.Credential{ |
|||
AccessKey: accessKey, |
|||
SecretKey: secretKey, |
|||
}) |
|||
} |
|||
credRows.Close() |
|||
|
|||
config.Identities = append(config.Identities, identity) |
|||
} |
|||
|
|||
return config, nil |
|||
} |
|||
|
|||
func (store *PostgresStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error { |
|||
if !store.configured { |
|||
return fmt.Errorf("store not configured") |
|||
} |
|||
|
|||
// Start transaction
|
|||
tx, err := store.db.BeginTx(ctx, nil) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to begin transaction: %v", err) |
|||
} |
|||
defer tx.Rollback() |
|||
|
|||
// Clear existing data
|
|||
if _, err := tx.ExecContext(ctx, "DELETE FROM credentials"); err != nil { |
|||
return fmt.Errorf("failed to clear credentials: %v", err) |
|||
} |
|||
if _, err := tx.ExecContext(ctx, "DELETE FROM users"); err != nil { |
|||
return fmt.Errorf("failed to clear users: %v", err) |
|||
} |
|||
|
|||
// Insert all identities
|
|||
for _, identity := range config.Identities { |
|||
// Marshal account data
|
|||
var accountDataJSON []byte |
|||
if identity.Account != nil { |
|||
accountDataJSON, err = json.Marshal(identity.Account) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to marshal account data for user %s: %v", identity.Name, err) |
|||
} |
|||
} |
|||
|
|||
// Marshal actions
|
|||
var actionsJSON []byte |
|||
if identity.Actions != nil { |
|||
actionsJSON, err = json.Marshal(identity.Actions) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to marshal actions for user %s: %v", identity.Name, err) |
|||
} |
|||
} |
|||
|
|||
// Insert user
|
|||
_, err := tx.ExecContext(ctx, |
|||
"INSERT INTO users (username, email, account_data, actions) VALUES ($1, $2, $3, $4)", |
|||
identity.Name, "", accountDataJSON, actionsJSON) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to insert user %s: %v", identity.Name, err) |
|||
} |
|||
|
|||
// Insert credentials
|
|||
for _, cred := range identity.Credentials { |
|||
_, err := tx.ExecContext(ctx, |
|||
"INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", |
|||
identity.Name, cred.AccessKey, cred.SecretKey) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to insert credential for user %s: %v", identity.Name, err) |
|||
} |
|||
} |
|||
} |
|||
|
|||
return tx.Commit() |
|||
} |
|||
|
|||
func (store *PostgresStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error { |
|||
if !store.configured { |
|||
return fmt.Errorf("store not configured") |
|||
} |
|||
|
|||
// Check if user already exists
|
|||
var count int |
|||
err := store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", identity.Name).Scan(&count) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to check user existence: %v", err) |
|||
} |
|||
if count > 0 { |
|||
return credential.ErrUserAlreadyExists |
|||
} |
|||
|
|||
// Start transaction
|
|||
tx, err := store.db.BeginTx(ctx, nil) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to begin transaction: %v", err) |
|||
} |
|||
defer tx.Rollback() |
|||
|
|||
// Marshal account data
|
|||
var accountDataJSON []byte |
|||
if identity.Account != nil { |
|||
accountDataJSON, err = json.Marshal(identity.Account) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to marshal account data: %v", err) |
|||
} |
|||
} |
|||
|
|||
// Marshal actions
|
|||
var actionsJSON []byte |
|||
if identity.Actions != nil { |
|||
actionsJSON, err = json.Marshal(identity.Actions) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to marshal actions: %v", err) |
|||
} |
|||
} |
|||
|
|||
// Insert user
|
|||
_, err = tx.ExecContext(ctx, |
|||
"INSERT INTO users (username, email, account_data, actions) VALUES ($1, $2, $3, $4)", |
|||
identity.Name, "", accountDataJSON, actionsJSON) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to insert user: %v", err) |
|||
} |
|||
|
|||
// Insert credentials
|
|||
for _, cred := range identity.Credentials { |
|||
_, err = tx.ExecContext(ctx, |
|||
"INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", |
|||
identity.Name, cred.AccessKey, cred.SecretKey) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to insert credential: %v", err) |
|||
} |
|||
} |
|||
|
|||
return tx.Commit() |
|||
} |
|||
|
|||
func (store *PostgresStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) { |
|||
if !store.configured { |
|||
return nil, fmt.Errorf("store not configured") |
|||
} |
|||
|
|||
var email string |
|||
var accountDataJSON, actionsJSON []byte |
|||
|
|||
err := store.db.QueryRowContext(ctx, |
|||
"SELECT email, account_data, actions FROM users WHERE username = $1", |
|||
username).Scan(&email, &accountDataJSON, &actionsJSON) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return nil, credential.ErrUserNotFound |
|||
} |
|||
return nil, fmt.Errorf("failed to query user: %v", err) |
|||
} |
|||
|
|||
identity := &iam_pb.Identity{ |
|||
Name: username, |
|||
} |
|||
|
|||
// Parse account data
|
|||
if len(accountDataJSON) > 0 { |
|||
if err := json.Unmarshal(accountDataJSON, &identity.Account); err != nil { |
|||
return nil, fmt.Errorf("failed to unmarshal account data: %v", err) |
|||
} |
|||
} |
|||
|
|||
// Parse actions
|
|||
if len(actionsJSON) > 0 { |
|||
if err := json.Unmarshal(actionsJSON, &identity.Actions); err != nil { |
|||
return nil, fmt.Errorf("failed to unmarshal actions: %v", err) |
|||
} |
|||
} |
|||
|
|||
// Query credentials
|
|||
rows, err := store.db.QueryContext(ctx, "SELECT access_key, secret_key FROM credentials WHERE username = $1", username) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("failed to query credentials: %v", err) |
|||
} |
|||
defer rows.Close() |
|||
|
|||
for rows.Next() { |
|||
var accessKey, secretKey string |
|||
if err := rows.Scan(&accessKey, &secretKey); err != nil { |
|||
return nil, fmt.Errorf("failed to scan credential: %v", err) |
|||
} |
|||
|
|||
identity.Credentials = append(identity.Credentials, &iam_pb.Credential{ |
|||
AccessKey: accessKey, |
|||
SecretKey: secretKey, |
|||
}) |
|||
} |
|||
|
|||
return identity, nil |
|||
} |
|||
|
|||
func (store *PostgresStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error { |
|||
if !store.configured { |
|||
return fmt.Errorf("store not configured") |
|||
} |
|||
|
|||
// Start transaction
|
|||
tx, err := store.db.BeginTx(ctx, nil) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to begin transaction: %v", err) |
|||
} |
|||
defer tx.Rollback() |
|||
|
|||
// Check if user exists
|
|||
var count int |
|||
err = tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", username).Scan(&count) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to check user existence: %v", err) |
|||
} |
|||
if count == 0 { |
|||
return credential.ErrUserNotFound |
|||
} |
|||
|
|||
// Marshal account data
|
|||
var accountDataJSON []byte |
|||
if identity.Account != nil { |
|||
accountDataJSON, err = json.Marshal(identity.Account) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to marshal account data: %v", err) |
|||
} |
|||
} |
|||
|
|||
// Marshal actions
|
|||
var actionsJSON []byte |
|||
if identity.Actions != nil { |
|||
actionsJSON, err = json.Marshal(identity.Actions) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to marshal actions: %v", err) |
|||
} |
|||
} |
|||
|
|||
// Update user
|
|||
_, err = tx.ExecContext(ctx, |
|||
"UPDATE users SET email = $2, account_data = $3, actions = $4, updated_at = CURRENT_TIMESTAMP WHERE username = $1", |
|||
username, "", accountDataJSON, actionsJSON) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to update user: %v", err) |
|||
} |
|||
|
|||
// Delete existing credentials
|
|||
_, err = tx.ExecContext(ctx, "DELETE FROM credentials WHERE username = $1", username) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to delete existing credentials: %v", err) |
|||
} |
|||
|
|||
// Insert new credentials
|
|||
for _, cred := range identity.Credentials { |
|||
_, err = tx.ExecContext(ctx, |
|||
"INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", |
|||
username, cred.AccessKey, cred.SecretKey) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to insert credential: %v", err) |
|||
} |
|||
} |
|||
|
|||
return tx.Commit() |
|||
} |
|||
|
|||
func (store *PostgresStore) DeleteUser(ctx context.Context, username string) error { |
|||
if !store.configured { |
|||
return fmt.Errorf("store not configured") |
|||
} |
|||
|
|||
result, err := store.db.ExecContext(ctx, "DELETE FROM users WHERE username = $1", username) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to delete user: %v", err) |
|||
} |
|||
|
|||
rowsAffected, err := result.RowsAffected() |
|||
if err != nil { |
|||
return fmt.Errorf("failed to get rows affected: %v", err) |
|||
} |
|||
|
|||
if rowsAffected == 0 { |
|||
return credential.ErrUserNotFound |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (store *PostgresStore) ListUsers(ctx context.Context) ([]string, error) { |
|||
if !store.configured { |
|||
return nil, fmt.Errorf("store not configured") |
|||
} |
|||
|
|||
rows, err := store.db.QueryContext(ctx, "SELECT username FROM users ORDER BY username") |
|||
if err != nil { |
|||
return nil, fmt.Errorf("failed to query users: %v", err) |
|||
} |
|||
defer rows.Close() |
|||
|
|||
var usernames []string |
|||
for rows.Next() { |
|||
var username string |
|||
if err := rows.Scan(&username); err != nil { |
|||
return nil, fmt.Errorf("failed to scan username: %v", err) |
|||
} |
|||
usernames = append(usernames, username) |
|||
} |
|||
|
|||
return usernames, nil |
|||
} |
|||
|
|||
func (store *PostgresStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) { |
|||
if !store.configured { |
|||
return nil, fmt.Errorf("store not configured") |
|||
} |
|||
|
|||
var username string |
|||
err := store.db.QueryRowContext(ctx, "SELECT username FROM credentials WHERE access_key = $1", accessKey).Scan(&username) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return nil, credential.ErrAccessKeyNotFound |
|||
} |
|||
return nil, fmt.Errorf("failed to query access key: %v", err) |
|||
} |
|||
|
|||
return store.GetUser(ctx, username) |
|||
} |
|||
|
|||
func (store *PostgresStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) error { |
|||
if !store.configured { |
|||
return fmt.Errorf("store not configured") |
|||
} |
|||
|
|||
// Check if user exists
|
|||
var count int |
|||
err := store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", username).Scan(&count) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to check user existence: %v", err) |
|||
} |
|||
if count == 0 { |
|||
return credential.ErrUserNotFound |
|||
} |
|||
|
|||
// Insert credential
|
|||
_, err = store.db.ExecContext(ctx, |
|||
"INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", |
|||
username, cred.AccessKey, cred.SecretKey) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to insert credential: %v", err) |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (store *PostgresStore) DeleteAccessKey(ctx context.Context, username string, accessKey string) error { |
|||
if !store.configured { |
|||
return fmt.Errorf("store not configured") |
|||
} |
|||
|
|||
result, err := store.db.ExecContext(ctx, |
|||
"DELETE FROM credentials WHERE username = $1 AND access_key = $2", |
|||
username, accessKey) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to delete access key: %v", err) |
|||
} |
|||
|
|||
rowsAffected, err := result.RowsAffected() |
|||
if err != nil { |
|||
return fmt.Errorf("failed to get rows affected: %v", err) |
|||
} |
|||
|
|||
if rowsAffected == 0 { |
|||
// Check if user exists
|
|||
var count int |
|||
err = store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", username).Scan(&count) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to check user existence: %v", err) |
|||
} |
|||
if count == 0 { |
|||
return credential.ErrUserNotFound |
|||
} |
|||
return credential.ErrAccessKeyNotFound |
|||
} |
|||
|
|||
return nil |
|||
} |
@ -0,0 +1,130 @@ |
|||
package postgres |
|||
|
|||
import ( |
|||
"context" |
|||
"encoding/json" |
|||
"fmt" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/credential" |
|||
) |
|||
|
|||
// GetPolicies retrieves all IAM policies from PostgreSQL
|
|||
func (store *PostgresStore) GetPolicies(ctx context.Context) (map[string]credential.PolicyDocument, error) { |
|||
if !store.configured { |
|||
return nil, fmt.Errorf("store not configured") |
|||
} |
|||
|
|||
policies := make(map[string]credential.PolicyDocument) |
|||
|
|||
rows, err := store.db.QueryContext(ctx, "SELECT name, document FROM policies") |
|||
if err != nil { |
|||
return nil, fmt.Errorf("failed to query policies: %v", err) |
|||
} |
|||
defer rows.Close() |
|||
|
|||
for rows.Next() { |
|||
var name string |
|||
var documentJSON []byte |
|||
|
|||
if err := rows.Scan(&name, &documentJSON); err != nil { |
|||
return nil, fmt.Errorf("failed to scan policy row: %v", err) |
|||
} |
|||
|
|||
var document credential.PolicyDocument |
|||
if err := json.Unmarshal(documentJSON, &document); err != nil { |
|||
return nil, fmt.Errorf("failed to unmarshal policy document for %s: %v", name, err) |
|||
} |
|||
|
|||
policies[name] = document |
|||
} |
|||
|
|||
return policies, nil |
|||
} |
|||
|
|||
// CreatePolicy creates a new IAM policy in PostgreSQL
|
|||
func (store *PostgresStore) CreatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { |
|||
if !store.configured { |
|||
return fmt.Errorf("store not configured") |
|||
} |
|||
|
|||
documentJSON, err := json.Marshal(document) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to marshal policy document: %v", err) |
|||
} |
|||
|
|||
_, err = store.db.ExecContext(ctx, |
|||
"INSERT INTO policies (name, document) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET document = $2, updated_at = CURRENT_TIMESTAMP", |
|||
name, documentJSON) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to insert policy: %v", err) |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// UpdatePolicy updates an existing IAM policy in PostgreSQL
|
|||
func (store *PostgresStore) UpdatePolicy(ctx context.Context, name string, document credential.PolicyDocument) error { |
|||
if !store.configured { |
|||
return fmt.Errorf("store not configured") |
|||
} |
|||
|
|||
documentJSON, err := json.Marshal(document) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to marshal policy document: %v", err) |
|||
} |
|||
|
|||
result, err := store.db.ExecContext(ctx, |
|||
"UPDATE policies SET document = $2, updated_at = CURRENT_TIMESTAMP WHERE name = $1", |
|||
name, documentJSON) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to update policy: %v", err) |
|||
} |
|||
|
|||
rowsAffected, err := result.RowsAffected() |
|||
if err != nil { |
|||
return fmt.Errorf("failed to get rows affected: %v", err) |
|||
} |
|||
|
|||
if rowsAffected == 0 { |
|||
return fmt.Errorf("policy %s not found", name) |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// DeletePolicy deletes an IAM policy from PostgreSQL
|
|||
func (store *PostgresStore) DeletePolicy(ctx context.Context, name string) error { |
|||
if !store.configured { |
|||
return fmt.Errorf("store not configured") |
|||
} |
|||
|
|||
result, err := store.db.ExecContext(ctx, "DELETE FROM policies WHERE name = $1", name) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to delete policy: %v", err) |
|||
} |
|||
|
|||
rowsAffected, err := result.RowsAffected() |
|||
if err != nil { |
|||
return fmt.Errorf("failed to get rows affected: %v", err) |
|||
} |
|||
|
|||
if rowsAffected == 0 { |
|||
return fmt.Errorf("policy %s not found", name) |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// GetPolicy retrieves a specific IAM policy by name from PostgreSQL
|
|||
func (store *PostgresStore) GetPolicy(ctx context.Context, name string) (*credential.PolicyDocument, error) { |
|||
policies, err := store.GetPolicies(ctx) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
if policy, exists := policies[name]; exists { |
|||
return &policy, nil |
|||
} |
|||
|
|||
return nil, nil // Policy not found
|
|||
} |
@ -0,0 +1,146 @@ |
|||
package test |
|||
|
|||
import ( |
|||
"context" |
|||
"testing" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/credential" |
|||
"github.com/seaweedfs/seaweedfs/weed/credential/memory" |
|||
|
|||
// Import all store implementations to register them
|
|||
_ "github.com/seaweedfs/seaweedfs/weed/credential/filer_etc" |
|||
_ "github.com/seaweedfs/seaweedfs/weed/credential/memory" |
|||
_ "github.com/seaweedfs/seaweedfs/weed/credential/postgres" |
|||
) |
|||
|
|||
// TestPolicyManagement tests policy management across all credential stores
|
|||
func TestPolicyManagement(t *testing.T) { |
|||
ctx := context.Background() |
|||
|
|||
// Test with memory store (easiest to test)
|
|||
credentialManager, err := credential.NewCredentialManager(credential.StoreTypeMemory, nil, "") |
|||
if err != nil { |
|||
t.Fatalf("Failed to create credential manager: %v", err) |
|||
} |
|||
|
|||
// Test policy operations
|
|||
testPolicyOperations(t, ctx, credentialManager) |
|||
} |
|||
|
|||
func testPolicyOperations(t *testing.T, ctx context.Context, credentialManager *credential.CredentialManager) { |
|||
store := credentialManager.GetStore() |
|||
|
|||
// Cast to memory store to access policy methods
|
|||
memoryStore, ok := store.(*memory.MemoryStore) |
|||
if !ok { |
|||
t.Skip("Store is not a memory store") |
|||
} |
|||
|
|||
// Test GetPolicies (should be empty initially)
|
|||
policies, err := memoryStore.GetPolicies(ctx) |
|||
if err != nil { |
|||
t.Fatalf("Failed to get policies: %v", err) |
|||
} |
|||
if len(policies) != 0 { |
|||
t.Errorf("Expected 0 policies, got %d", len(policies)) |
|||
} |
|||
|
|||
// Test CreatePolicy
|
|||
testPolicy := credential.PolicyDocument{ |
|||
Version: "2012-10-17", |
|||
Statement: []*credential.PolicyStatement{ |
|||
{ |
|||
Effect: "Allow", |
|||
Action: []string{"s3:GetObject"}, |
|||
Resource: []string{"arn:aws:s3:::test-bucket/*"}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
err = memoryStore.CreatePolicy(ctx, "test-policy", testPolicy) |
|||
if err != nil { |
|||
t.Fatalf("Failed to create policy: %v", err) |
|||
} |
|||
|
|||
// Test GetPolicies (should have 1 policy now)
|
|||
policies, err = memoryStore.GetPolicies(ctx) |
|||
if err != nil { |
|||
t.Fatalf("Failed to get policies: %v", err) |
|||
} |
|||
if len(policies) != 1 { |
|||
t.Errorf("Expected 1 policy, got %d", len(policies)) |
|||
} |
|||
|
|||
// Verify policy content
|
|||
policy, exists := policies["test-policy"] |
|||
if !exists { |
|||
t.Error("test-policy not found") |
|||
} |
|||
if policy.Version != "2012-10-17" { |
|||
t.Errorf("Expected policy version '2012-10-17', got '%s'", policy.Version) |
|||
} |
|||
if len(policy.Statement) != 1 { |
|||
t.Errorf("Expected 1 statement, got %d", len(policy.Statement)) |
|||
} |
|||
|
|||
// Test UpdatePolicy
|
|||
updatedPolicy := credential.PolicyDocument{ |
|||
Version: "2012-10-17", |
|||
Statement: []*credential.PolicyStatement{ |
|||
{ |
|||
Effect: "Allow", |
|||
Action: []string{"s3:GetObject", "s3:PutObject"}, |
|||
Resource: []string{"arn:aws:s3:::test-bucket/*"}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
err = memoryStore.UpdatePolicy(ctx, "test-policy", updatedPolicy) |
|||
if err != nil { |
|||
t.Fatalf("Failed to update policy: %v", err) |
|||
} |
|||
|
|||
// Verify the update
|
|||
policies, err = memoryStore.GetPolicies(ctx) |
|||
if err != nil { |
|||
t.Fatalf("Failed to get policies after update: %v", err) |
|||
} |
|||
|
|||
updatedPolicyResult, exists := policies["test-policy"] |
|||
if !exists { |
|||
t.Error("test-policy not found after update") |
|||
} |
|||
if len(updatedPolicyResult.Statement) != 1 { |
|||
t.Errorf("Expected 1 statement after update, got %d", len(updatedPolicyResult.Statement)) |
|||
} |
|||
if len(updatedPolicyResult.Statement[0].Action) != 2 { |
|||
t.Errorf("Expected 2 actions after update, got %d", len(updatedPolicyResult.Statement[0].Action)) |
|||
} |
|||
|
|||
// Test DeletePolicy
|
|||
err = memoryStore.DeletePolicy(ctx, "test-policy") |
|||
if err != nil { |
|||
t.Fatalf("Failed to delete policy: %v", err) |
|||
} |
|||
|
|||
// Verify deletion
|
|||
policies, err = memoryStore.GetPolicies(ctx) |
|||
if err != nil { |
|||
t.Fatalf("Failed to get policies after deletion: %v", err) |
|||
} |
|||
if len(policies) != 0 { |
|||
t.Errorf("Expected 0 policies after deletion, got %d", len(policies)) |
|||
} |
|||
} |
|||
|
|||
// TestPolicyManagementWithFilerEtc tests policy management with filer_etc store
|
|||
func TestPolicyManagementWithFilerEtc(t *testing.T) { |
|||
// Skip this test if we can't connect to a filer
|
|||
t.Skip("Filer connection required for filer_etc store testing") |
|||
} |
|||
|
|||
// TestPolicyManagementWithPostgres tests policy management with postgres store
|
|||
func TestPolicyManagementWithPostgres(t *testing.T) { |
|||
// Skip this test if we can't connect to PostgreSQL
|
|||
t.Skip("PostgreSQL connection required for postgres store testing") |
|||
} |
@ -1,369 +0,0 @@ |
|||
package balance |
|||
|
|||
import ( |
|||
"fmt" |
|||
"strconv" |
|||
"time" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/admin/view/components" |
|||
"github.com/seaweedfs/seaweedfs/weed/glog" |
|||
"github.com/seaweedfs/seaweedfs/weed/worker/types" |
|||
) |
|||
|
|||
// Helper function to format seconds as duration string
|
|||
func formatDurationFromSeconds(seconds int) string { |
|||
d := time.Duration(seconds) * time.Second |
|||
return d.String() |
|||
} |
|||
|
|||
// Helper functions to convert between seconds and value+unit format
|
|||
func secondsToValueAndUnit(seconds int) (float64, string) { |
|||
if seconds == 0 { |
|||
return 0, "minutes" |
|||
} |
|||
|
|||
// Try days first
|
|||
if seconds%(24*3600) == 0 && seconds >= 24*3600 { |
|||
return float64(seconds / (24 * 3600)), "days" |
|||
} |
|||
|
|||
// Try hours
|
|||
if seconds%3600 == 0 && seconds >= 3600 { |
|||
return float64(seconds / 3600), "hours" |
|||
} |
|||
|
|||
// Default to minutes
|
|||
return float64(seconds / 60), "minutes" |
|||
} |
|||
|
|||
func valueAndUnitToSeconds(value float64, unit string) int { |
|||
switch unit { |
|||
case "days": |
|||
return int(value * 24 * 3600) |
|||
case "hours": |
|||
return int(value * 3600) |
|||
case "minutes": |
|||
return int(value * 60) |
|||
default: |
|||
return int(value * 60) // Default to minutes
|
|||
} |
|||
} |
|||
|
|||
// UITemplProvider provides the templ-based UI for balance task configuration
|
|||
type UITemplProvider struct { |
|||
detector *BalanceDetector |
|||
scheduler *BalanceScheduler |
|||
} |
|||
|
|||
// NewUITemplProvider creates a new balance templ UI provider
|
|||
func NewUITemplProvider(detector *BalanceDetector, scheduler *BalanceScheduler) *UITemplProvider { |
|||
return &UITemplProvider{ |
|||
detector: detector, |
|||
scheduler: scheduler, |
|||
} |
|||
} |
|||
|
|||
// GetTaskType returns the task type
|
|||
func (ui *UITemplProvider) GetTaskType() types.TaskType { |
|||
return types.TaskTypeBalance |
|||
} |
|||
|
|||
// GetDisplayName returns the human-readable name
|
|||
func (ui *UITemplProvider) GetDisplayName() string { |
|||
return "Volume Balance" |
|||
} |
|||
|
|||
// GetDescription returns a description of what this task does
|
|||
func (ui *UITemplProvider) GetDescription() string { |
|||
return "Redistributes volumes across volume servers to optimize storage utilization and performance" |
|||
} |
|||
|
|||
// GetIcon returns the icon CSS class for this task type
|
|||
func (ui *UITemplProvider) GetIcon() string { |
|||
return "fas fa-balance-scale text-secondary" |
|||
} |
|||
|
|||
// RenderConfigSections renders the configuration as templ section data
|
|||
func (ui *UITemplProvider) RenderConfigSections(currentConfig interface{}) ([]components.ConfigSectionData, error) { |
|||
config := ui.getCurrentBalanceConfig() |
|||
|
|||
// Detection settings section
|
|||
detectionSection := components.ConfigSectionData{ |
|||
Title: "Detection Settings", |
|||
Icon: "fas fa-search", |
|||
Description: "Configure when balance tasks should be triggered", |
|||
Fields: []interface{}{ |
|||
components.CheckboxFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "enabled", |
|||
Label: "Enable Balance Tasks", |
|||
Description: "Whether balance tasks should be automatically created", |
|||
}, |
|||
Checked: config.Enabled, |
|||
}, |
|||
components.NumberFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "imbalance_threshold", |
|||
Label: "Imbalance Threshold", |
|||
Description: "Trigger balance when storage imbalance exceeds this percentage (0.0-1.0)", |
|||
Required: true, |
|||
}, |
|||
Value: config.ImbalanceThreshold, |
|||
Step: "0.01", |
|||
Min: floatPtr(0.0), |
|||
Max: floatPtr(1.0), |
|||
}, |
|||
components.DurationInputFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "scan_interval", |
|||
Label: "Scan Interval", |
|||
Description: "How often to scan for imbalanced volumes", |
|||
Required: true, |
|||
}, |
|||
Seconds: config.ScanIntervalSeconds, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
// Scheduling settings section
|
|||
schedulingSection := components.ConfigSectionData{ |
|||
Title: "Scheduling Settings", |
|||
Icon: "fas fa-clock", |
|||
Description: "Configure task scheduling and concurrency", |
|||
Fields: []interface{}{ |
|||
components.NumberFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "max_concurrent", |
|||
Label: "Max Concurrent Tasks", |
|||
Description: "Maximum number of balance tasks that can run simultaneously", |
|||
Required: true, |
|||
}, |
|||
Value: float64(config.MaxConcurrent), |
|||
Step: "1", |
|||
Min: floatPtr(1), |
|||
}, |
|||
components.NumberFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "min_server_count", |
|||
Label: "Minimum Server Count", |
|||
Description: "Only balance when at least this many servers are available", |
|||
Required: true, |
|||
}, |
|||
Value: float64(config.MinServerCount), |
|||
Step: "1", |
|||
Min: floatPtr(1), |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
// Timing constraints section
|
|||
timingSection := components.ConfigSectionData{ |
|||
Title: "Timing Constraints", |
|||
Icon: "fas fa-calendar-clock", |
|||
Description: "Configure when balance operations are allowed", |
|||
Fields: []interface{}{ |
|||
components.CheckboxFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "move_during_off_hours", |
|||
Label: "Restrict to Off-Hours", |
|||
Description: "Only perform balance operations during off-peak hours", |
|||
}, |
|||
Checked: config.MoveDuringOffHours, |
|||
}, |
|||
components.TextFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "off_hours_start", |
|||
Label: "Off-Hours Start Time", |
|||
Description: "Start time for off-hours window (e.g., 23:00)", |
|||
}, |
|||
Value: config.OffHoursStart, |
|||
}, |
|||
components.TextFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "off_hours_end", |
|||
Label: "Off-Hours End Time", |
|||
Description: "End time for off-hours window (e.g., 06:00)", |
|||
}, |
|||
Value: config.OffHoursEnd, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
// Performance impact info section
|
|||
performanceSection := components.ConfigSectionData{ |
|||
Title: "Performance Considerations", |
|||
Icon: "fas fa-exclamation-triangle", |
|||
Description: "Important information about balance operations", |
|||
Fields: []interface{}{ |
|||
components.TextFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "performance_info", |
|||
Label: "Performance Impact", |
|||
Description: "Volume balancing involves data movement and can impact cluster performance", |
|||
}, |
|||
Value: "Enable off-hours restriction to minimize impact on production workloads", |
|||
}, |
|||
components.TextFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "safety_info", |
|||
Label: "Safety Requirements", |
|||
Description: fmt.Sprintf("Requires at least %d servers to ensure data safety during moves", config.MinServerCount), |
|||
}, |
|||
Value: "Maintains data safety during volume moves between servers", |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
return []components.ConfigSectionData{detectionSection, schedulingSection, timingSection, performanceSection}, nil |
|||
} |
|||
|
|||
// ParseConfigForm parses form data into configuration
|
|||
func (ui *UITemplProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) { |
|||
config := &BalanceConfig{} |
|||
|
|||
// Parse enabled checkbox
|
|||
config.Enabled = len(formData["enabled"]) > 0 && formData["enabled"][0] == "on" |
|||
|
|||
// Parse imbalance threshold
|
|||
if thresholdStr := formData["imbalance_threshold"]; len(thresholdStr) > 0 { |
|||
if threshold, err := strconv.ParseFloat(thresholdStr[0], 64); err != nil { |
|||
return nil, fmt.Errorf("invalid imbalance threshold: %v", err) |
|||
} else if threshold < 0 || threshold > 1 { |
|||
return nil, fmt.Errorf("imbalance threshold must be between 0.0 and 1.0") |
|||
} else { |
|||
config.ImbalanceThreshold = threshold |
|||
} |
|||
} |
|||
|
|||
// Parse scan interval
|
|||
if valueStr := formData["scan_interval"]; len(valueStr) > 0 { |
|||
if value, err := strconv.ParseFloat(valueStr[0], 64); err != nil { |
|||
return nil, fmt.Errorf("invalid scan interval value: %v", err) |
|||
} else { |
|||
unit := "minutes" // default
|
|||
if unitStr := formData["scan_interval_unit"]; len(unitStr) > 0 { |
|||
unit = unitStr[0] |
|||
} |
|||
config.ScanIntervalSeconds = valueAndUnitToSeconds(value, unit) |
|||
} |
|||
} |
|||
|
|||
// Parse max concurrent
|
|||
if concurrentStr := formData["max_concurrent"]; len(concurrentStr) > 0 { |
|||
if concurrent, err := strconv.Atoi(concurrentStr[0]); err != nil { |
|||
return nil, fmt.Errorf("invalid max concurrent: %v", err) |
|||
} else if concurrent < 1 { |
|||
return nil, fmt.Errorf("max concurrent must be at least 1") |
|||
} else { |
|||
config.MaxConcurrent = concurrent |
|||
} |
|||
} |
|||
|
|||
// Parse min server count
|
|||
if serverCountStr := formData["min_server_count"]; len(serverCountStr) > 0 { |
|||
if serverCount, err := strconv.Atoi(serverCountStr[0]); err != nil { |
|||
return nil, fmt.Errorf("invalid min server count: %v", err) |
|||
} else if serverCount < 1 { |
|||
return nil, fmt.Errorf("min server count must be at least 1") |
|||
} else { |
|||
config.MinServerCount = serverCount |
|||
} |
|||
} |
|||
|
|||
// Parse move during off hours
|
|||
config.MoveDuringOffHours = len(formData["move_during_off_hours"]) > 0 && formData["move_during_off_hours"][0] == "on" |
|||
|
|||
// Parse off hours start time
|
|||
if startStr := formData["off_hours_start"]; len(startStr) > 0 { |
|||
config.OffHoursStart = startStr[0] |
|||
} |
|||
|
|||
// Parse off hours end time
|
|||
if endStr := formData["off_hours_end"]; len(endStr) > 0 { |
|||
config.OffHoursEnd = endStr[0] |
|||
} |
|||
|
|||
return config, nil |
|||
} |
|||
|
|||
// GetCurrentConfig returns the current configuration
|
|||
func (ui *UITemplProvider) GetCurrentConfig() interface{} { |
|||
return ui.getCurrentBalanceConfig() |
|||
} |
|||
|
|||
// ApplyConfig applies the new configuration
|
|||
func (ui *UITemplProvider) ApplyConfig(config interface{}) error { |
|||
balanceConfig, ok := config.(*BalanceConfig) |
|||
if !ok { |
|||
return fmt.Errorf("invalid config type, expected *BalanceConfig") |
|||
} |
|||
|
|||
// Apply to detector
|
|||
if ui.detector != nil { |
|||
ui.detector.SetEnabled(balanceConfig.Enabled) |
|||
ui.detector.SetThreshold(balanceConfig.ImbalanceThreshold) |
|||
ui.detector.SetMinCheckInterval(time.Duration(balanceConfig.ScanIntervalSeconds) * time.Second) |
|||
} |
|||
|
|||
// Apply to scheduler
|
|||
if ui.scheduler != nil { |
|||
ui.scheduler.SetEnabled(balanceConfig.Enabled) |
|||
ui.scheduler.SetMaxConcurrent(balanceConfig.MaxConcurrent) |
|||
ui.scheduler.SetMinServerCount(balanceConfig.MinServerCount) |
|||
ui.scheduler.SetMoveDuringOffHours(balanceConfig.MoveDuringOffHours) |
|||
ui.scheduler.SetOffHoursStart(balanceConfig.OffHoursStart) |
|||
ui.scheduler.SetOffHoursEnd(balanceConfig.OffHoursEnd) |
|||
} |
|||
|
|||
glog.V(1).Infof("Applied balance configuration: enabled=%v, threshold=%.1f%%, max_concurrent=%d, min_servers=%d, off_hours=%v", |
|||
balanceConfig.Enabled, balanceConfig.ImbalanceThreshold*100, balanceConfig.MaxConcurrent, |
|||
balanceConfig.MinServerCount, balanceConfig.MoveDuringOffHours) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// getCurrentBalanceConfig gets the current configuration from detector and scheduler
|
|||
func (ui *UITemplProvider) getCurrentBalanceConfig() *BalanceConfig { |
|||
config := &BalanceConfig{ |
|||
// Default values (fallback if detectors/schedulers are nil)
|
|||
Enabled: true, |
|||
ImbalanceThreshold: 0.1, // 10% imbalance
|
|||
ScanIntervalSeconds: int((4 * time.Hour).Seconds()), |
|||
MaxConcurrent: 1, |
|||
MinServerCount: 3, |
|||
MoveDuringOffHours: true, |
|||
OffHoursStart: "23:00", |
|||
OffHoursEnd: "06:00", |
|||
} |
|||
|
|||
// Get current values from detector
|
|||
if ui.detector != nil { |
|||
config.Enabled = ui.detector.IsEnabled() |
|||
config.ImbalanceThreshold = ui.detector.GetThreshold() |
|||
config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds()) |
|||
} |
|||
|
|||
// Get current values from scheduler
|
|||
if ui.scheduler != nil { |
|||
config.MaxConcurrent = ui.scheduler.GetMaxConcurrent() |
|||
config.MinServerCount = ui.scheduler.GetMinServerCount() |
|||
config.MoveDuringOffHours = ui.scheduler.GetMoveDuringOffHours() |
|||
config.OffHoursStart = ui.scheduler.GetOffHoursStart() |
|||
config.OffHoursEnd = ui.scheduler.GetOffHoursEnd() |
|||
} |
|||
|
|||
return config |
|||
} |
|||
|
|||
// floatPtr is a helper function to create float64 pointers
|
|||
func floatPtr(f float64) *float64 { |
|||
return &f |
|||
} |
|||
|
|||
// RegisterUITempl registers the balance templ UI provider with the UI registry
|
|||
func RegisterUITempl(uiRegistry *types.UITemplRegistry, detector *BalanceDetector, scheduler *BalanceScheduler) { |
|||
uiProvider := NewUITemplProvider(detector, scheduler) |
|||
uiRegistry.RegisterUI(uiProvider) |
|||
|
|||
glog.V(1).Infof("✅ Registered balance task templ UI provider") |
|||
} |
@ -1,319 +0,0 @@ |
|||
package erasure_coding |
|||
|
|||
import ( |
|||
"fmt" |
|||
"strconv" |
|||
"time" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/admin/view/components" |
|||
"github.com/seaweedfs/seaweedfs/weed/glog" |
|||
"github.com/seaweedfs/seaweedfs/weed/worker/types" |
|||
) |
|||
|
|||
// Helper function to format seconds as duration string
|
|||
func formatDurationFromSeconds(seconds int) string { |
|||
d := time.Duration(seconds) * time.Second |
|||
return d.String() |
|||
} |
|||
|
|||
// Helper function to convert value and unit to seconds
|
|||
func valueAndUnitToSeconds(value float64, unit string) int { |
|||
switch unit { |
|||
case "days": |
|||
return int(value * 24 * 60 * 60) |
|||
case "hours": |
|||
return int(value * 60 * 60) |
|||
case "minutes": |
|||
return int(value * 60) |
|||
default: |
|||
return int(value * 60) // Default to minutes
|
|||
} |
|||
} |
|||
|
|||
// UITemplProvider provides the templ-based UI for erasure coding task configuration
|
|||
type UITemplProvider struct { |
|||
detector *EcDetector |
|||
scheduler *Scheduler |
|||
} |
|||
|
|||
// NewUITemplProvider creates a new erasure coding templ UI provider
|
|||
func NewUITemplProvider(detector *EcDetector, scheduler *Scheduler) *UITemplProvider { |
|||
return &UITemplProvider{ |
|||
detector: detector, |
|||
scheduler: scheduler, |
|||
} |
|||
} |
|||
|
|||
// ErasureCodingConfig is defined in ui.go - we reuse it
|
|||
|
|||
// GetTaskType returns the task type
|
|||
func (ui *UITemplProvider) GetTaskType() types.TaskType { |
|||
return types.TaskTypeErasureCoding |
|||
} |
|||
|
|||
// GetDisplayName returns the human-readable name
|
|||
func (ui *UITemplProvider) GetDisplayName() string { |
|||
return "Erasure Coding" |
|||
} |
|||
|
|||
// GetDescription returns a description of what this task does
|
|||
func (ui *UITemplProvider) GetDescription() string { |
|||
return "Converts replicated volumes to erasure-coded format for efficient storage" |
|||
} |
|||
|
|||
// GetIcon returns the icon CSS class for this task type
|
|||
func (ui *UITemplProvider) GetIcon() string { |
|||
return "fas fa-shield-alt text-info" |
|||
} |
|||
|
|||
// RenderConfigSections renders the configuration as templ section data
|
|||
func (ui *UITemplProvider) RenderConfigSections(currentConfig interface{}) ([]components.ConfigSectionData, error) { |
|||
config := ui.getCurrentECConfig() |
|||
|
|||
// Detection settings section
|
|||
detectionSection := components.ConfigSectionData{ |
|||
Title: "Detection Settings", |
|||
Icon: "fas fa-search", |
|||
Description: "Configure when erasure coding tasks should be triggered", |
|||
Fields: []interface{}{ |
|||
components.CheckboxFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "enabled", |
|||
Label: "Enable Erasure Coding Tasks", |
|||
Description: "Whether erasure coding tasks should be automatically created", |
|||
}, |
|||
Checked: config.Enabled, |
|||
}, |
|||
components.DurationInputFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "scan_interval", |
|||
Label: "Scan Interval", |
|||
Description: "How often to scan for volumes needing erasure coding", |
|||
Required: true, |
|||
}, |
|||
Seconds: config.ScanIntervalSeconds, |
|||
}, |
|||
components.DurationInputFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "volume_age_threshold", |
|||
Label: "Volume Age Threshold", |
|||
Description: "Only apply erasure coding to volumes older than this age", |
|||
Required: true, |
|||
}, |
|||
Seconds: config.VolumeAgeHoursSeconds, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
// Erasure coding parameters section
|
|||
paramsSection := components.ConfigSectionData{ |
|||
Title: "Erasure Coding Parameters", |
|||
Icon: "fas fa-cogs", |
|||
Description: "Configure erasure coding scheme and performance", |
|||
Fields: []interface{}{ |
|||
components.NumberFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "data_shards", |
|||
Label: "Data Shards", |
|||
Description: "Number of data shards in the erasure coding scheme", |
|||
Required: true, |
|||
}, |
|||
Value: float64(config.ShardCount), |
|||
Step: "1", |
|||
Min: floatPtr(1), |
|||
Max: floatPtr(16), |
|||
}, |
|||
components.NumberFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "parity_shards", |
|||
Label: "Parity Shards", |
|||
Description: "Number of parity shards (determines fault tolerance)", |
|||
Required: true, |
|||
}, |
|||
Value: float64(config.ParityCount), |
|||
Step: "1", |
|||
Min: floatPtr(1), |
|||
Max: floatPtr(16), |
|||
}, |
|||
components.NumberFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "max_concurrent", |
|||
Label: "Max Concurrent Tasks", |
|||
Description: "Maximum number of erasure coding tasks that can run simultaneously", |
|||
Required: true, |
|||
}, |
|||
Value: float64(config.MaxConcurrent), |
|||
Step: "1", |
|||
Min: floatPtr(1), |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
// Performance impact info section
|
|||
infoSection := components.ConfigSectionData{ |
|||
Title: "Performance Impact", |
|||
Icon: "fas fa-info-circle", |
|||
Description: "Important information about erasure coding operations", |
|||
Fields: []interface{}{ |
|||
components.TextFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "durability_info", |
|||
Label: "Durability", |
|||
Description: fmt.Sprintf("With %d+%d configuration, can tolerate up to %d shard failures", |
|||
config.ShardCount, config.ParityCount, config.ParityCount), |
|||
}, |
|||
Value: "High durability with space efficiency", |
|||
}, |
|||
components.TextFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "performance_info", |
|||
Label: "Performance Note", |
|||
Description: "Erasure coding is CPU and I/O intensive. Consider running during off-peak hours", |
|||
}, |
|||
Value: "Schedule during low-traffic periods", |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
return []components.ConfigSectionData{detectionSection, paramsSection, infoSection}, nil |
|||
} |
|||
|
|||
// ParseConfigForm parses form data into configuration
|
|||
func (ui *UITemplProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) { |
|||
config := &ErasureCodingConfig{} |
|||
|
|||
// Parse enabled checkbox
|
|||
config.Enabled = len(formData["enabled"]) > 0 && formData["enabled"][0] == "on" |
|||
|
|||
// Parse volume age threshold
|
|||
if valueStr := formData["volume_age_threshold"]; len(valueStr) > 0 { |
|||
if value, err := strconv.ParseFloat(valueStr[0], 64); err != nil { |
|||
return nil, fmt.Errorf("invalid volume age threshold value: %v", err) |
|||
} else { |
|||
unit := "hours" // default
|
|||
if unitStr := formData["volume_age_threshold_unit"]; len(unitStr) > 0 { |
|||
unit = unitStr[0] |
|||
} |
|||
config.VolumeAgeHoursSeconds = valueAndUnitToSeconds(value, unit) |
|||
} |
|||
} |
|||
|
|||
// Parse scan interval
|
|||
if valueStr := formData["scan_interval"]; len(valueStr) > 0 { |
|||
if value, err := strconv.ParseFloat(valueStr[0], 64); err != nil { |
|||
return nil, fmt.Errorf("invalid scan interval value: %v", err) |
|||
} else { |
|||
unit := "hours" // default
|
|||
if unitStr := formData["scan_interval_unit"]; len(unitStr) > 0 { |
|||
unit = unitStr[0] |
|||
} |
|||
config.ScanIntervalSeconds = valueAndUnitToSeconds(value, unit) |
|||
} |
|||
} |
|||
|
|||
// Parse data shards
|
|||
if shardsStr := formData["data_shards"]; len(shardsStr) > 0 { |
|||
if shards, err := strconv.Atoi(shardsStr[0]); err != nil { |
|||
return nil, fmt.Errorf("invalid data shards: %v", err) |
|||
} else if shards < 1 || shards > 16 { |
|||
return nil, fmt.Errorf("data shards must be between 1 and 16") |
|||
} else { |
|||
config.ShardCount = shards |
|||
} |
|||
} |
|||
|
|||
// Parse parity shards
|
|||
if shardsStr := formData["parity_shards"]; len(shardsStr) > 0 { |
|||
if shards, err := strconv.Atoi(shardsStr[0]); err != nil { |
|||
return nil, fmt.Errorf("invalid parity shards: %v", err) |
|||
} else if shards < 1 || shards > 16 { |
|||
return nil, fmt.Errorf("parity shards must be between 1 and 16") |
|||
} else { |
|||
config.ParityCount = shards |
|||
} |
|||
} |
|||
|
|||
// Parse max concurrent
|
|||
if concurrentStr := formData["max_concurrent"]; len(concurrentStr) > 0 { |
|||
if concurrent, err := strconv.Atoi(concurrentStr[0]); err != nil { |
|||
return nil, fmt.Errorf("invalid max concurrent: %v", err) |
|||
} else if concurrent < 1 { |
|||
return nil, fmt.Errorf("max concurrent must be at least 1") |
|||
} else { |
|||
config.MaxConcurrent = concurrent |
|||
} |
|||
} |
|||
|
|||
return config, nil |
|||
} |
|||
|
|||
// GetCurrentConfig returns the current configuration
|
|||
func (ui *UITemplProvider) GetCurrentConfig() interface{} { |
|||
return ui.getCurrentECConfig() |
|||
} |
|||
|
|||
// ApplyConfig applies the new configuration
|
|||
func (ui *UITemplProvider) ApplyConfig(config interface{}) error { |
|||
ecConfig, ok := config.(*ErasureCodingConfig) |
|||
if !ok { |
|||
return fmt.Errorf("invalid config type, expected *ErasureCodingConfig") |
|||
} |
|||
|
|||
// Apply to detector
|
|||
if ui.detector != nil { |
|||
ui.detector.SetEnabled(ecConfig.Enabled) |
|||
ui.detector.SetVolumeAgeHours(ecConfig.VolumeAgeHoursSeconds) |
|||
ui.detector.SetScanInterval(time.Duration(ecConfig.ScanIntervalSeconds) * time.Second) |
|||
} |
|||
|
|||
// Apply to scheduler
|
|||
if ui.scheduler != nil { |
|||
ui.scheduler.SetMaxConcurrent(ecConfig.MaxConcurrent) |
|||
ui.scheduler.SetEnabled(ecConfig.Enabled) |
|||
} |
|||
|
|||
glog.V(1).Infof("Applied erasure coding configuration: enabled=%v, age_threshold=%ds, max_concurrent=%d", |
|||
ecConfig.Enabled, ecConfig.VolumeAgeHoursSeconds, ecConfig.MaxConcurrent) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// getCurrentECConfig gets the current configuration from detector and scheduler
|
|||
func (ui *UITemplProvider) getCurrentECConfig() *ErasureCodingConfig { |
|||
config := &ErasureCodingConfig{ |
|||
// Default values (fallback if detectors/schedulers are nil)
|
|||
Enabled: true, |
|||
VolumeAgeHoursSeconds: int((24 * time.Hour).Seconds()), |
|||
ScanIntervalSeconds: int((2 * time.Hour).Seconds()), |
|||
MaxConcurrent: 1, |
|||
ShardCount: 10, |
|||
ParityCount: 4, |
|||
} |
|||
|
|||
// Get current values from detector
|
|||
if ui.detector != nil { |
|||
config.Enabled = ui.detector.IsEnabled() |
|||
config.VolumeAgeHoursSeconds = ui.detector.GetVolumeAgeHours() |
|||
config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds()) |
|||
} |
|||
|
|||
// Get current values from scheduler
|
|||
if ui.scheduler != nil { |
|||
config.MaxConcurrent = ui.scheduler.GetMaxConcurrent() |
|||
} |
|||
|
|||
return config |
|||
} |
|||
|
|||
// floatPtr is a helper function to create float64 pointers
|
|||
func floatPtr(f float64) *float64 { |
|||
return &f |
|||
} |
|||
|
|||
// RegisterUITempl registers the erasure coding templ UI provider with the UI registry
|
|||
func RegisterUITempl(uiRegistry *types.UITemplRegistry, detector *EcDetector, scheduler *Scheduler) { |
|||
uiProvider := NewUITemplProvider(detector, scheduler) |
|||
uiRegistry.RegisterUI(uiProvider) |
|||
|
|||
glog.V(1).Infof("✅ Registered erasure coding task templ UI provider") |
|||
} |
@ -1,330 +0,0 @@ |
|||
package vacuum |
|||
|
|||
import ( |
|||
"fmt" |
|||
"strconv" |
|||
"time" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/admin/view/components" |
|||
"github.com/seaweedfs/seaweedfs/weed/glog" |
|||
"github.com/seaweedfs/seaweedfs/weed/worker/types" |
|||
) |
|||
|
|||
// Helper function to format seconds as duration string
|
|||
func formatDurationFromSeconds(seconds int) string { |
|||
d := time.Duration(seconds) * time.Second |
|||
return d.String() |
|||
} |
|||
|
|||
// Helper functions to convert between seconds and value+unit format
|
|||
func secondsToValueAndUnit(seconds int) (float64, string) { |
|||
if seconds == 0 { |
|||
return 0, "minutes" |
|||
} |
|||
|
|||
// Try days first
|
|||
if seconds%(24*3600) == 0 && seconds >= 24*3600 { |
|||
return float64(seconds / (24 * 3600)), "days" |
|||
} |
|||
|
|||
// Try hours
|
|||
if seconds%3600 == 0 && seconds >= 3600 { |
|||
return float64(seconds / 3600), "hours" |
|||
} |
|||
|
|||
// Default to minutes
|
|||
return float64(seconds / 60), "minutes" |
|||
} |
|||
|
|||
func valueAndUnitToSeconds(value float64, unit string) int { |
|||
switch unit { |
|||
case "days": |
|||
return int(value * 24 * 3600) |
|||
case "hours": |
|||
return int(value * 3600) |
|||
case "minutes": |
|||
return int(value * 60) |
|||
default: |
|||
return int(value * 60) // Default to minutes
|
|||
} |
|||
} |
|||
|
|||
// UITemplProvider provides the templ-based UI for vacuum task configuration
|
|||
type UITemplProvider struct { |
|||
detector *VacuumDetector |
|||
scheduler *VacuumScheduler |
|||
} |
|||
|
|||
// NewUITemplProvider creates a new vacuum templ UI provider
|
|||
func NewUITemplProvider(detector *VacuumDetector, scheduler *VacuumScheduler) *UITemplProvider { |
|||
return &UITemplProvider{ |
|||
detector: detector, |
|||
scheduler: scheduler, |
|||
} |
|||
} |
|||
|
|||
// GetTaskType returns the task type
|
|||
func (ui *UITemplProvider) GetTaskType() types.TaskType { |
|||
return types.TaskTypeVacuum |
|||
} |
|||
|
|||
// GetDisplayName returns the human-readable name
|
|||
func (ui *UITemplProvider) GetDisplayName() string { |
|||
return "Volume Vacuum" |
|||
} |
|||
|
|||
// GetDescription returns a description of what this task does
|
|||
func (ui *UITemplProvider) GetDescription() string { |
|||
return "Reclaims disk space by removing deleted files from volumes" |
|||
} |
|||
|
|||
// GetIcon returns the icon CSS class for this task type
|
|||
func (ui *UITemplProvider) GetIcon() string { |
|||
return "fas fa-broom text-primary" |
|||
} |
|||
|
|||
// RenderConfigSections renders the configuration as templ section data
|
|||
func (ui *UITemplProvider) RenderConfigSections(currentConfig interface{}) ([]components.ConfigSectionData, error) { |
|||
config := ui.getCurrentVacuumConfig() |
|||
|
|||
// Detection settings section
|
|||
detectionSection := components.ConfigSectionData{ |
|||
Title: "Detection Settings", |
|||
Icon: "fas fa-search", |
|||
Description: "Configure when vacuum tasks should be triggered", |
|||
Fields: []interface{}{ |
|||
components.CheckboxFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "enabled", |
|||
Label: "Enable Vacuum Tasks", |
|||
Description: "Whether vacuum tasks should be automatically created", |
|||
}, |
|||
Checked: config.Enabled, |
|||
}, |
|||
components.NumberFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "garbage_threshold", |
|||
Label: "Garbage Threshold", |
|||
Description: "Trigger vacuum when garbage ratio exceeds this percentage (0.0-1.0)", |
|||
Required: true, |
|||
}, |
|||
Value: config.GarbageThreshold, |
|||
Step: "0.01", |
|||
Min: floatPtr(0.0), |
|||
Max: floatPtr(1.0), |
|||
}, |
|||
components.DurationInputFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "scan_interval", |
|||
Label: "Scan Interval", |
|||
Description: "How often to scan for volumes needing vacuum", |
|||
Required: true, |
|||
}, |
|||
Seconds: config.ScanIntervalSeconds, |
|||
}, |
|||
components.DurationInputFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "min_volume_age", |
|||
Label: "Minimum Volume Age", |
|||
Description: "Only vacuum volumes older than this duration", |
|||
Required: true, |
|||
}, |
|||
Seconds: config.MinVolumeAgeSeconds, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
// Scheduling settings section
|
|||
schedulingSection := components.ConfigSectionData{ |
|||
Title: "Scheduling Settings", |
|||
Icon: "fas fa-clock", |
|||
Description: "Configure task scheduling and concurrency", |
|||
Fields: []interface{}{ |
|||
components.NumberFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "max_concurrent", |
|||
Label: "Max Concurrent Tasks", |
|||
Description: "Maximum number of vacuum tasks that can run simultaneously", |
|||
Required: true, |
|||
}, |
|||
Value: float64(config.MaxConcurrent), |
|||
Step: "1", |
|||
Min: floatPtr(1), |
|||
}, |
|||
components.DurationInputFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "min_interval", |
|||
Label: "Minimum Interval", |
|||
Description: "Minimum time between vacuum operations on the same volume", |
|||
Required: true, |
|||
}, |
|||
Seconds: config.MinIntervalSeconds, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
// Performance impact info section
|
|||
performanceSection := components.ConfigSectionData{ |
|||
Title: "Performance Impact", |
|||
Icon: "fas fa-exclamation-triangle", |
|||
Description: "Important information about vacuum operations", |
|||
Fields: []interface{}{ |
|||
components.TextFieldData{ |
|||
FormFieldData: components.FormFieldData{ |
|||
Name: "info_impact", |
|||
Label: "Impact", |
|||
Description: "Volume vacuum operations are I/O intensive and should be scheduled appropriately", |
|||
}, |
|||
Value: "Configure thresholds and intervals based on your storage usage patterns", |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
return []components.ConfigSectionData{detectionSection, schedulingSection, performanceSection}, nil |
|||
} |
|||
|
|||
// ParseConfigForm parses form data into configuration
|
|||
func (ui *UITemplProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) { |
|||
config := &VacuumConfig{} |
|||
|
|||
// Parse enabled checkbox
|
|||
config.Enabled = len(formData["enabled"]) > 0 && formData["enabled"][0] == "on" |
|||
|
|||
// Parse garbage threshold
|
|||
if thresholdStr := formData["garbage_threshold"]; len(thresholdStr) > 0 { |
|||
if threshold, err := strconv.ParseFloat(thresholdStr[0], 64); err != nil { |
|||
return nil, fmt.Errorf("invalid garbage threshold: %v", err) |
|||
} else if threshold < 0 || threshold > 1 { |
|||
return nil, fmt.Errorf("garbage threshold must be between 0.0 and 1.0") |
|||
} else { |
|||
config.GarbageThreshold = threshold |
|||
} |
|||
} |
|||
|
|||
// Parse scan interval
|
|||
if valueStr := formData["scan_interval"]; len(valueStr) > 0 { |
|||
if value, err := strconv.ParseFloat(valueStr[0], 64); err != nil { |
|||
return nil, fmt.Errorf("invalid scan interval value: %v", err) |
|||
} else { |
|||
unit := "minutes" // default
|
|||
if unitStr := formData["scan_interval_unit"]; len(unitStr) > 0 { |
|||
unit = unitStr[0] |
|||
} |
|||
config.ScanIntervalSeconds = valueAndUnitToSeconds(value, unit) |
|||
} |
|||
} |
|||
|
|||
// Parse min volume age
|
|||
if valueStr := formData["min_volume_age"]; len(valueStr) > 0 { |
|||
if value, err := strconv.ParseFloat(valueStr[0], 64); err != nil { |
|||
return nil, fmt.Errorf("invalid min volume age value: %v", err) |
|||
} else { |
|||
unit := "minutes" // default
|
|||
if unitStr := formData["min_volume_age_unit"]; len(unitStr) > 0 { |
|||
unit = unitStr[0] |
|||
} |
|||
config.MinVolumeAgeSeconds = valueAndUnitToSeconds(value, unit) |
|||
} |
|||
} |
|||
|
|||
// Parse max concurrent
|
|||
if concurrentStr := formData["max_concurrent"]; len(concurrentStr) > 0 { |
|||
if concurrent, err := strconv.Atoi(concurrentStr[0]); err != nil { |
|||
return nil, fmt.Errorf("invalid max concurrent: %v", err) |
|||
} else if concurrent < 1 { |
|||
return nil, fmt.Errorf("max concurrent must be at least 1") |
|||
} else { |
|||
config.MaxConcurrent = concurrent |
|||
} |
|||
} |
|||
|
|||
// Parse min interval
|
|||
if valueStr := formData["min_interval"]; len(valueStr) > 0 { |
|||
if value, err := strconv.ParseFloat(valueStr[0], 64); err != nil { |
|||
return nil, fmt.Errorf("invalid min interval value: %v", err) |
|||
} else { |
|||
unit := "minutes" // default
|
|||
if unitStr := formData["min_interval_unit"]; len(unitStr) > 0 { |
|||
unit = unitStr[0] |
|||
} |
|||
config.MinIntervalSeconds = valueAndUnitToSeconds(value, unit) |
|||
} |
|||
} |
|||
|
|||
return config, nil |
|||
} |
|||
|
|||
// GetCurrentConfig returns the current configuration
|
|||
func (ui *UITemplProvider) GetCurrentConfig() interface{} { |
|||
return ui.getCurrentVacuumConfig() |
|||
} |
|||
|
|||
// ApplyConfig applies the new configuration
|
|||
func (ui *UITemplProvider) ApplyConfig(config interface{}) error { |
|||
vacuumConfig, ok := config.(*VacuumConfig) |
|||
if !ok { |
|||
return fmt.Errorf("invalid config type, expected *VacuumConfig") |
|||
} |
|||
|
|||
// Apply to detector
|
|||
if ui.detector != nil { |
|||
ui.detector.SetEnabled(vacuumConfig.Enabled) |
|||
ui.detector.SetGarbageThreshold(vacuumConfig.GarbageThreshold) |
|||
ui.detector.SetScanInterval(time.Duration(vacuumConfig.ScanIntervalSeconds) * time.Second) |
|||
ui.detector.SetMinVolumeAge(time.Duration(vacuumConfig.MinVolumeAgeSeconds) * time.Second) |
|||
} |
|||
|
|||
// Apply to scheduler
|
|||
if ui.scheduler != nil { |
|||
ui.scheduler.SetEnabled(vacuumConfig.Enabled) |
|||
ui.scheduler.SetMaxConcurrent(vacuumConfig.MaxConcurrent) |
|||
ui.scheduler.SetMinInterval(time.Duration(vacuumConfig.MinIntervalSeconds) * time.Second) |
|||
} |
|||
|
|||
glog.V(1).Infof("Applied vacuum configuration: enabled=%v, threshold=%.1f%%, scan_interval=%s, max_concurrent=%d", |
|||
vacuumConfig.Enabled, vacuumConfig.GarbageThreshold*100, formatDurationFromSeconds(vacuumConfig.ScanIntervalSeconds), vacuumConfig.MaxConcurrent) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// getCurrentVacuumConfig gets the current configuration from detector and scheduler
|
|||
func (ui *UITemplProvider) getCurrentVacuumConfig() *VacuumConfig { |
|||
config := &VacuumConfig{ |
|||
// Default values (fallback if detectors/schedulers are nil)
|
|||
Enabled: true, |
|||
GarbageThreshold: 0.3, |
|||
ScanIntervalSeconds: int((30 * time.Minute).Seconds()), |
|||
MinVolumeAgeSeconds: int((1 * time.Hour).Seconds()), |
|||
MaxConcurrent: 2, |
|||
MinIntervalSeconds: int((6 * time.Hour).Seconds()), |
|||
} |
|||
|
|||
// Get current values from detector
|
|||
if ui.detector != nil { |
|||
config.Enabled = ui.detector.IsEnabled() |
|||
config.GarbageThreshold = ui.detector.GetGarbageThreshold() |
|||
config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds()) |
|||
config.MinVolumeAgeSeconds = int(ui.detector.GetMinVolumeAge().Seconds()) |
|||
} |
|||
|
|||
// Get current values from scheduler
|
|||
if ui.scheduler != nil { |
|||
config.MaxConcurrent = ui.scheduler.GetMaxConcurrent() |
|||
config.MinIntervalSeconds = int(ui.scheduler.GetMinInterval().Seconds()) |
|||
} |
|||
|
|||
return config |
|||
} |
|||
|
|||
// floatPtr is a helper function to create float64 pointers
|
|||
func floatPtr(f float64) *float64 { |
|||
return &f |
|||
} |
|||
|
|||
// RegisterUITempl registers the vacuum templ UI provider with the UI registry
|
|||
func RegisterUITempl(uiRegistry *types.UITemplRegistry, detector *VacuumDetector, scheduler *VacuumScheduler) { |
|||
uiProvider := NewUITemplProvider(detector, scheduler) |
|||
uiRegistry.RegisterUI(uiProvider) |
|||
|
|||
glog.V(1).Infof("✅ Registered vacuum task templ UI provider") |
|||
} |
@ -1,63 +0,0 @@ |
|||
package types |
|||
|
|||
import ( |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/view/components" |
|||
) |
|||
|
|||
// TaskUITemplProvider defines how tasks provide their configuration UI using templ components
|
|||
type TaskUITemplProvider interface { |
|||
// GetTaskType returns the task type
|
|||
GetTaskType() TaskType |
|||
|
|||
// GetDisplayName returns the human-readable name
|
|||
GetDisplayName() string |
|||
|
|||
// GetDescription returns a description of what this task does
|
|||
GetDescription() string |
|||
|
|||
// GetIcon returns the icon CSS class or HTML for this task type
|
|||
GetIcon() string |
|||
|
|||
// RenderConfigSections renders the configuration as templ section data
|
|||
RenderConfigSections(currentConfig interface{}) ([]components.ConfigSectionData, error) |
|||
|
|||
// ParseConfigForm parses form data into configuration
|
|||
ParseConfigForm(formData map[string][]string) (interface{}, error) |
|||
|
|||
// GetCurrentConfig returns the current configuration
|
|||
GetCurrentConfig() interface{} |
|||
|
|||
// ApplyConfig applies the new configuration
|
|||
ApplyConfig(config interface{}) error |
|||
} |
|||
|
|||
// UITemplRegistry manages task UI providers that use templ components
|
|||
type UITemplRegistry struct { |
|||
providers map[TaskType]TaskUITemplProvider |
|||
} |
|||
|
|||
// NewUITemplRegistry creates a new templ-based UI registry
|
|||
func NewUITemplRegistry() *UITemplRegistry { |
|||
return &UITemplRegistry{ |
|||
providers: make(map[TaskType]TaskUITemplProvider), |
|||
} |
|||
} |
|||
|
|||
// RegisterUI registers a task UI provider
|
|||
func (r *UITemplRegistry) RegisterUI(provider TaskUITemplProvider) { |
|||
r.providers[provider.GetTaskType()] = provider |
|||
} |
|||
|
|||
// GetProvider returns the UI provider for a task type
|
|||
func (r *UITemplRegistry) GetProvider(taskType TaskType) TaskUITemplProvider { |
|||
return r.providers[taskType] |
|||
} |
|||
|
|||
// GetAllProviders returns all registered UI providers
|
|||
func (r *UITemplRegistry) GetAllProviders() map[TaskType]TaskUITemplProvider { |
|||
result := make(map[TaskType]TaskUITemplProvider) |
|||
for k, v := range r.providers { |
|||
result[k] = v |
|||
} |
|||
return result |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue