Files
calypso/backend/internal/object_storage/service.go
2026-01-10 05:36:15 +00:00

298 lines
8.1 KiB
Go

package object_storage
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/atlasos/calypso/internal/common/logger"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
madmin "github.com/minio/madmin-go/v3"
)
// Service handles MinIO object storage operations
type Service struct {
client *minio.Client
adminClient *madmin.AdminClient
logger *logger.Logger
endpoint string
accessKey string
secretKey string
}
// NewService creates a new MinIO service
func NewService(endpoint, accessKey, secretKey string, log *logger.Logger) (*Service, error) {
// Create MinIO client
minioClient, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
Secure: false, // Set to true if using HTTPS
})
if err != nil {
return nil, fmt.Errorf("failed to create MinIO client: %w", err)
}
// Create MinIO Admin client
adminClient, err := madmin.New(endpoint, accessKey, secretKey, false)
if err != nil {
return nil, fmt.Errorf("failed to create MinIO admin client: %w", err)
}
return &Service{
client: minioClient,
adminClient: adminClient,
logger: log,
endpoint: endpoint,
accessKey: accessKey,
secretKey: secretKey,
}, nil
}
// Bucket represents a MinIO bucket
type Bucket struct {
Name string `json:"name"`
CreationDate time.Time `json:"creation_date"`
Size int64 `json:"size"` // Total size in bytes
Objects int64 `json:"objects"` // Number of objects
AccessPolicy string `json:"access_policy"` // private, public-read, public-read-write
}
// ListBuckets lists all buckets in MinIO
func (s *Service) ListBuckets(ctx context.Context) ([]*Bucket, error) {
buckets, err := s.client.ListBuckets(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list buckets: %w", err)
}
result := make([]*Bucket, 0, len(buckets))
for _, bucket := range buckets {
bucketInfo, err := s.getBucketInfo(ctx, bucket.Name)
if err != nil {
s.logger.Warn("Failed to get bucket info", "bucket", bucket.Name, "error", err)
// Continue with basic info
result = append(result, &Bucket{
Name: bucket.Name,
CreationDate: bucket.CreationDate,
Size: 0,
Objects: 0,
AccessPolicy: "private",
})
continue
}
result = append(result, bucketInfo)
}
return result, nil
}
// getBucketInfo gets detailed information about a bucket
func (s *Service) getBucketInfo(ctx context.Context, bucketName string) (*Bucket, error) {
// Get bucket creation date
buckets, err := s.client.ListBuckets(ctx)
if err != nil {
return nil, err
}
var creationDate time.Time
for _, b := range buckets {
if b.Name == bucketName {
creationDate = b.CreationDate
break
}
}
// Get bucket size and object count by listing objects
var size int64
var objects int64
// List objects in bucket to calculate size and count
objectCh := s.client.ListObjects(ctx, bucketName, minio.ListObjectsOptions{
Recursive: true,
})
for object := range objectCh {
if object.Err != nil {
s.logger.Warn("Error listing object", "bucket", bucketName, "error", object.Err)
continue
}
objects++
size += object.Size
}
return &Bucket{
Name: bucketName,
CreationDate: creationDate,
Size: size,
Objects: objects,
AccessPolicy: s.getBucketPolicy(ctx, bucketName),
}, nil
}
// getBucketPolicy gets the access policy for a bucket
func (s *Service) getBucketPolicy(ctx context.Context, bucketName string) string {
policy, err := s.client.GetBucketPolicy(ctx, bucketName)
if err != nil {
return "private"
}
// Parse policy JSON to determine access type
// For simplicity, check if policy allows public read
if policy != "" {
// Check if policy contains public read access
if strings.Contains(policy, "s3:GetObject") && strings.Contains(policy, "Principal") && strings.Contains(policy, "*") {
if strings.Contains(policy, "s3:PutObject") {
return "public-read-write"
}
return "public-read"
}
}
return "private"
}
// CreateBucket creates a new bucket
func (s *Service) CreateBucket(ctx context.Context, bucketName string) error {
err := s.client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{})
if err != nil {
return fmt.Errorf("failed to create bucket: %w", err)
}
return nil
}
// DeleteBucket deletes a bucket
func (s *Service) DeleteBucket(ctx context.Context, bucketName string) error {
err := s.client.RemoveBucket(ctx, bucketName)
if err != nil {
return fmt.Errorf("failed to delete bucket: %w", err)
}
return nil
}
// GetBucketStats gets statistics for a bucket
func (s *Service) GetBucketStats(ctx context.Context, bucketName string) (*Bucket, error) {
return s.getBucketInfo(ctx, bucketName)
}
// User represents a MinIO IAM user
type User struct {
AccessKey string `json:"access_key"`
Status string `json:"status"` // "enabled" or "disabled"
CreatedAt time.Time `json:"created_at"`
}
// ListUsers lists all IAM users in MinIO
func (s *Service) ListUsers(ctx context.Context) ([]*User, error) {
users, err := s.adminClient.ListUsers(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list users: %w", err)
}
result := make([]*User, 0, len(users))
for accessKey, userInfo := range users {
status := "enabled"
if userInfo.Status == madmin.AccountDisabled {
status = "disabled"
}
// MinIO doesn't provide creation date, use current time
result = append(result, &User{
AccessKey: accessKey,
Status: status,
CreatedAt: time.Now(),
})
}
return result, nil
}
// CreateUser creates a new IAM user in MinIO
func (s *Service) CreateUser(ctx context.Context, accessKey, secretKey string) error {
err := s.adminClient.AddUser(ctx, accessKey, secretKey)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
return nil
}
// DeleteUser deletes an IAM user from MinIO
func (s *Service) DeleteUser(ctx context.Context, accessKey string) error {
err := s.adminClient.RemoveUser(ctx, accessKey)
if err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
return nil
}
// ServiceAccount represents a MinIO service account (access key)
type ServiceAccount struct {
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key,omitempty"` // Only returned on creation
ParentUser string `json:"parent_user"`
Expiration time.Time `json:"expiration,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// ListServiceAccounts lists all service accounts in MinIO
func (s *Service) ListServiceAccounts(ctx context.Context) ([]*ServiceAccount, error) {
accounts, err := s.adminClient.ListServiceAccounts(ctx, "")
if err != nil {
return nil, fmt.Errorf("failed to list service accounts: %w", err)
}
result := make([]*ServiceAccount, 0, len(accounts.Accounts))
for _, account := range accounts.Accounts {
var expiration time.Time
if account.Expiration != nil {
expiration = *account.Expiration
}
result = append(result, &ServiceAccount{
AccessKey: account.AccessKey,
ParentUser: account.ParentUser,
Expiration: expiration,
CreatedAt: time.Now(), // MinIO doesn't provide creation date
})
}
return result, nil
}
// CreateServiceAccount creates a new service account (access key) in MinIO
func (s *Service) CreateServiceAccount(ctx context.Context, parentUser string, policy string, expiration *time.Time) (*ServiceAccount, error) {
opts := madmin.AddServiceAccountReq{
TargetUser: parentUser,
}
if policy != "" {
opts.Policy = json.RawMessage(policy)
}
if expiration != nil {
opts.Expiration = expiration
}
creds, err := s.adminClient.AddServiceAccount(ctx, opts)
if err != nil {
return nil, fmt.Errorf("failed to create service account: %w", err)
}
return &ServiceAccount{
AccessKey: creds.AccessKey,
SecretKey: creds.SecretKey,
ParentUser: parentUser,
Expiration: creds.Expiration,
CreatedAt: time.Now(),
}, nil
}
// DeleteServiceAccount deletes a service account from MinIO
func (s *Service) DeleteServiceAccount(ctx context.Context, accessKey string) error {
err := s.adminClient.DeleteServiceAccount(ctx, accessKey)
if err != nil {
return fmt.Errorf("failed to delete service account: %w", err)
}
return nil
}