feat: domain/storage/billing/config cmds + rainyun domain/storage/billing services

This commit is contained in:
2026-07-28 16:11:03 +08:00
parent b0fbc3e710
commit 096492dd82
8 changed files with 465 additions and 5 deletions
+33
View File
@@ -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)
}
+74
View File
@@ -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 <key> <value>",
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)
}
+97
View File
@@ -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 <domain>",
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 <record-id>",
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)
}
+75
View File
@@ -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 <instance-id>",
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 <name>",
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)
}
+40
View File
@@ -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
}
+76
View File
@@ -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)
}
+9 -3
View File
@@ -14,6 +14,9 @@ func init() {
type Rainyun struct { type Rainyun struct {
client *client client *client
server *ServerService server *ServerService
domain *DomainService
storage *StorageService
billing *BillingService
} }
// New creates a Rainyun provider from config. // 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) c := newClient(cfg.Endpoint, cfg.APIKey)
r := &Rainyun{client: c} r := &Rainyun{client: c}
r.server = &ServerService{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 return r, nil
} }
@@ -39,7 +45,7 @@ func (r *Rainyun) Server() provider.ServerService {
} }
return r.server return r.server
} }
func (r *Rainyun) Domain() provider.DomainService { return nil } func (r *Rainyun) Domain() provider.DomainService { return r.domain }
func (r *Rainyun) Storage() provider.StorageService { return nil } func (r *Rainyun) Storage() provider.StorageService { return r.storage }
func (r *Rainyun) CDN() provider.CDNService { return nil } 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 }
+59
View File
@@ -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)
}