diff --git a/internal/cmd/billing.go b/internal/cmd/billing.go new file mode 100644 index 0000000..7c17c30 --- /dev/null +++ b/internal/cmd/billing.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var billingCmd = &cobra.Command{ + Use: "billing", + Short: "Billing and orders", +} + +var ordersCmd = &cobra.Command{ + Use: "orders", + Short: "List orders", + RunE: func(cmd *cobra.Command, args []string) error { + svc := prov.Billing() + if svc == nil { + return fmt.Errorf("billing not supported by current provider") + } + orders, err := svc.Orders(cmd.Context()) + if err != nil { + return err + } + return out.Print(orders) + }, +} + +func init() { + billingCmd.AddCommand(ordersCmd) + rootCmd.AddCommand(billingCmd) +} diff --git a/internal/cmd/config_cmd.go b/internal/cmd/config_cmd.go new file mode 100644 index 0000000..b27d07f --- /dev/null +++ b/internal/cmd/config_cmd.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "fmt" + "strings" + + "git.yoresee.cc/nightstar/ry/internal/config" + + "github.com/spf13/cobra" +) + +var configCmd = &cobra.Command{ + Use: "config", + Short: "Manage ry configuration", +} + +var configShowCmd = &cobra.Command{ + Use: "show", + Short: "Show current configuration", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Printf("Config file: %s\n", cfgPath) + fmt.Printf("Current provider: %s\n", cfg.CurrentProvider) + fmt.Printf("Output format: %s\n", cfg.Output) + fmt.Println("Providers:") + for name, pc := range cfg.Providers { + masked := pc.APIKey + if len(masked) > 4 { + masked = masked[:2] + strings.Repeat("*", len(masked)-4) + masked[len(masked)-2:] + } + marker := " " + if name == cfg.CurrentProvider { + marker = "*" + } + fmt.Printf(" %s %s: api_key=%s\n", marker, name, masked) + } + return nil + }, +} + +var configSetCmd = &cobra.Command{ + Use: "set ", + Short: "Set a config value", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + key, value := args[0], args[1] + switch key { + case "output": + cfg.Output = value + default: + return fmt.Errorf("unknown config key: %s", key) + } + if err := config.Save(cfgPath, cfg); err != nil { + return fmt.Errorf("save config: %w", err) + } + fmt.Printf("Set %s = %s\n", key, value) + return nil + }, +} + +var configPathCmd = &cobra.Command{ + Use: "path", + Short: "Print config file path", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println(cfgPath) + return nil + }, +} + +func init() { + configCmd.AddCommand(configShowCmd) + configCmd.AddCommand(configSetCmd) + configCmd.AddCommand(configPathCmd) + rootCmd.AddCommand(configCmd) +} diff --git a/internal/cmd/domain.go b/internal/cmd/domain.go new file mode 100644 index 0000000..9c61323 --- /dev/null +++ b/internal/cmd/domain.go @@ -0,0 +1,97 @@ +package cmd + +import ( + "fmt" + + "git.yoresee.cc/nightstar/ry/internal/provider" + + "github.com/spf13/cobra" +) + +var domainCmd = &cobra.Command{ + Use: "domain", + Short: "Manage domains", +} + +var domainListCmd = &cobra.Command{ + Use: "list", + Short: "List domains", + RunE: func(cmd *cobra.Command, args []string) error { + svc := prov.Domain() + if svc == nil { + return fmt.Errorf("domain management not supported by current provider") + } + domains, err := svc.List(cmd.Context()) + if err != nil { + return err + } + return out.Print(domains) + }, +} + +var dnsCmd = &cobra.Command{ + Use: "dns ", + Short: "Manage DNS records", + Args: cobra.MinimumNArgs(1), +} + +var dnsListCmd = &cobra.Command{ + Use: "list", + Short: "List DNS records", + RunE: func(cmd *cobra.Command, args []string) error { + svc := prov.Domain() + if svc == nil { + return fmt.Errorf("domain management not supported") + } + records, err := svc.DNSList(cmd.Context(), args[0]) + if err != nil { + return err + } + return out.Print(records) + }, +} + +var dnsAddCmd = &cobra.Command{ + Use: "add", + Short: "Add a DNS record", + RunE: func(cmd *cobra.Command, args []string) error { + svc := prov.Domain() + if svc == nil { + return fmt.Errorf("domain management not supported") + } + recType, _ := cmd.Flags().GetString("type") + name, _ := cmd.Flags().GetString("name") + value, _ := cmd.Flags().GetString("value") + ttl, _ := cmd.Flags().GetInt("ttl") + return svc.DNSAdd(cmd.Context(), args[0], + provider.DNSRecord{Type: recType, Name: name, Value: value, TTL: ttl}) + }, +} + +var dnsDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a DNS record", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + svc := prov.Domain() + if svc == nil { + return fmt.Errorf("domain management not supported") + } + return svc.DNSDelete(cmd.Context(), args[0], args[1]) + }, +} + +func init() { + dnsAddCmd.Flags().String("type", "A", "Record type (A, CNAME, TXT)") + dnsAddCmd.Flags().String("name", "@", "Record name") + dnsAddCmd.Flags().String("value", "", "Record value") + dnsAddCmd.Flags().Int("ttl", 600, "TTL in seconds") + dnsAddCmd.MarkFlagRequired("value") + + dnsCmd.AddCommand(dnsListCmd) + dnsCmd.AddCommand(dnsAddCmd) + dnsCmd.AddCommand(dnsDeleteCmd) + domainCmd.AddCommand(domainListCmd) + domainCmd.AddCommand(dnsCmd) + rootCmd.AddCommand(domainCmd) +} diff --git a/internal/cmd/storage.go b/internal/cmd/storage.go new file mode 100644 index 0000000..1e9655f --- /dev/null +++ b/internal/cmd/storage.go @@ -0,0 +1,75 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var storageCmd = &cobra.Command{ + Use: "storage", + Short: "Manage object storage", +} + +var storageListCmd = &cobra.Command{ + Use: "list", + Short: "List storage instances", + RunE: func(cmd *cobra.Command, args []string) error { + svc := prov.Storage() + if svc == nil { + return fmt.Errorf("storage management not supported by current provider") + } + instances, err := svc.List(cmd.Context()) + if err != nil { + return err + } + return out.Print(instances) + }, +} + +var bucketCmd = &cobra.Command{ + Use: "bucket ", + Short: "Manage buckets", + Args: cobra.ExactArgs(1), +} + +var bucketListCmd = &cobra.Command{ + Use: "list", + Short: "List buckets", + RunE: func(cmd *cobra.Command, args []string) error { + svc := prov.Storage() + if svc == nil { + return fmt.Errorf("storage management not supported") + } + buckets, err := svc.BucketList(cmd.Context(), args[0]) + if err != nil { + return err + } + return out.Print(buckets) + }, +} + +var bucketCreateCmd = &cobra.Command{ + Use: "create ", + Short: "Create a bucket", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + svc := prov.Storage() + if svc == nil { + return fmt.Errorf("storage management not supported") + } + if err := svc.BucketCreate(cmd.Context(), args[0], args[1]); err != nil { + return err + } + fmt.Printf("Bucket %q created\n", args[1]) + return nil + }, +} + +func init() { + bucketCmd.AddCommand(bucketListCmd) + bucketCmd.AddCommand(bucketCreateCmd) + storageCmd.AddCommand(storageListCmd) + storageCmd.AddCommand(bucketCmd) + rootCmd.AddCommand(storageCmd) +} diff --git a/internal/provider/rainyun/billing.go b/internal/provider/rainyun/billing.go new file mode 100644 index 0000000..70932c1 --- /dev/null +++ b/internal/provider/rainyun/billing.go @@ -0,0 +1,40 @@ +package rainyun + +import ( + "context" + "fmt" + "time" + + "git.yoresee.cc/nightstar/ry/internal/provider" +) + +type BillingService struct { + client *client +} + +func (b *BillingService) Orders(ctx context.Context) ([]provider.Order, error) { + type rainyunOrder struct { + ID int `json:"id"` + ProductName string `json:"product_name"` + Amount float64 `json:"amount"` + Status string `json:"status"` + CreateDate string `json:"create_date"` + } + var raw []rainyunOrder + if err := b.client.get(ctx, "/expense/orders/list", &raw); err != nil { + return nil, err + } + orders := make([]provider.Order, len(raw)) + for i, r := range raw { + t, _ := time.Parse("2006-01-02 15:04:05", r.CreateDate) + orders[i] = provider.Order{ + ID: fmt.Sprintf("%d", r.ID), + Product: r.ProductName, + Amount: r.Amount, + Status: r.Status, + Created: t, + Raw: r, + } + } + return orders, nil +} diff --git a/internal/provider/rainyun/domain.go b/internal/provider/rainyun/domain.go new file mode 100644 index 0000000..55ba8c6 --- /dev/null +++ b/internal/provider/rainyun/domain.go @@ -0,0 +1,76 @@ +package rainyun + +import ( + "context" + "fmt" + + "git.yoresee.cc/nightstar/ry/internal/provider" +) + +type DomainService struct { + client *client +} + +type rainyunDomain struct { + ID int `json:"id"` + DomainName string `json:"domain_name"` + Status string `json:"status"` + ExpireDate string `json:"expire_date"` +} + +func (d *DomainService) List(ctx context.Context) ([]provider.Domain, error) { + var raw []rainyunDomain + if err := d.client.get(ctx, "/product/domain/", &raw); err != nil { + return nil, err + } + domains := make([]provider.Domain, len(raw)) + for i, r := range raw { + domains[i] = provider.Domain{ + ID: fmt.Sprintf("%d", r.ID), + Name: r.DomainName, + } + } + return domains, nil +} + +func (d *DomainService) DNSList(ctx context.Context, domain string) ([]provider.DNSRecord, error) { + type rainyunDNSRecord struct { + ID int `json:"id"` + RecordType string `json:"record_type"` + Host string `json:"host"` + Value string `json:"value"` + TTL int `json:"ttl"` + } + var raw []rainyunDNSRecord + path := fmt.Sprintf("/product/domain/%s/dns/", domain) + if err := d.client.get(ctx, path, &raw); err != nil { + return nil, err + } + records := make([]provider.DNSRecord, len(raw)) + for i, r := range raw { + records[i] = provider.DNSRecord{ + ID: fmt.Sprintf("%d", r.ID), + Type: r.RecordType, + Name: r.Host, + Value: r.Value, + TTL: r.TTL, + } + } + return records, nil +} + +func (d *DomainService) DNSAdd(ctx context.Context, domain string, record provider.DNSRecord) error { + body := map[string]any{ + "record_type": record.Type, + "host": record.Name, + "value": record.Value, + "ttl": record.TTL, + } + path := fmt.Sprintf("/product/domain/%s/dns", domain) + return d.client.post(ctx, path, body, nil) +} + +func (d *DomainService) DNSDelete(ctx context.Context, domain string, recordID string) error { + path := fmt.Sprintf("/product/domain/%s/dns/%s", domain, recordID) + return d.client.delete(ctx, path, nil) +} diff --git a/internal/provider/rainyun/rainyun.go b/internal/provider/rainyun/rainyun.go index 09424e9..e827d20 100644 --- a/internal/provider/rainyun/rainyun.go +++ b/internal/provider/rainyun/rainyun.go @@ -12,8 +12,11 @@ func init() { // Rainyun provider — 雨云 cloud platform. type Rainyun struct { - client *client - server *ServerService + client *client + server *ServerService + domain *DomainService + storage *StorageService + billing *BillingService } // New creates a Rainyun provider from config. @@ -21,6 +24,9 @@ func New(cfg provider.ProviderConfig) (provider.Provider, error) { c := newClient(cfg.Endpoint, cfg.APIKey) r := &Rainyun{client: c} r.server = &ServerService{client: c} + r.domain = &DomainService{client: c} + r.storage = &StorageService{client: c} + r.billing = &BillingService{client: c} return r, nil } @@ -39,7 +45,7 @@ func (r *Rainyun) Server() provider.ServerService { } return r.server } -func (r *Rainyun) Domain() provider.DomainService { return nil } -func (r *Rainyun) Storage() provider.StorageService { return nil } +func (r *Rainyun) Domain() provider.DomainService { return r.domain } +func (r *Rainyun) Storage() provider.StorageService { return r.storage } func (r *Rainyun) CDN() provider.CDNService { return nil } -func (r *Rainyun) Billing() provider.BillingService { return nil } +func (r *Rainyun) Billing() provider.BillingService { return r.billing } diff --git a/internal/provider/rainyun/storage.go b/internal/provider/rainyun/storage.go new file mode 100644 index 0000000..c168485 --- /dev/null +++ b/internal/provider/rainyun/storage.go @@ -0,0 +1,59 @@ +package rainyun + +import ( + "context" + "fmt" + + "git.yoresee.cc/nightstar/ry/internal/provider" +) + +type StorageService struct { + client *client +} + +func (s *StorageService) List(ctx context.Context) ([]provider.StorageInstance, error) { + var raw []struct { + ID int `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + } + if err := s.client.get(ctx, "/product/ros/instance", &raw); err != nil { + return nil, err + } + instances := make([]provider.StorageInstance, len(raw)) + for i, r := range raw { + instances[i] = provider.StorageInstance{ + ID: fmt.Sprintf("%d", r.ID), + Name: r.Name, + Raw: r, + } + } + return instances, nil +} + +func (s *StorageService) BucketList(ctx context.Context, instanceID string) ([]provider.Bucket, error) { + type rainyunBucket struct { + ID int `json:"id"` + Name string `json:"name"` + Region string `json:"region"` + } + var raw []rainyunBucket + path := fmt.Sprintf("/product/ros/bucket/%s", instanceID) + if err := s.client.get(ctx, path, &raw); err != nil { + return nil, err + } + buckets := make([]provider.Bucket, len(raw)) + for i, r := range raw { + buckets[i] = provider.Bucket{ + ID: fmt.Sprintf("%d", r.ID), + Name: r.Name, + } + } + return buckets, nil +} + +func (s *StorageService) BucketCreate(ctx context.Context, instanceID, name string) error { + body := map[string]string{"name": name} + path := fmt.Sprintf("/product/ros/bucket/%s", instanceID) + return s.client.post(ctx, path, body, nil) +}