Skip to content
12 changes: 3 additions & 9 deletions cmd/bcloud/commands/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,18 @@ func runCreate(_ *cobra.Command, args []string) error {

refID := args[0]

credConfig, exists := cfg.Credentials[refID]
credEntry, exists := cfg.Credentials[refID]
if !exists {
return fmt.Errorf("credential '%s' not found in config", refID)
}

configMap := map[string]interface{}{
"provider": credConfig.Provider,
"api_key": credConfig.APIKey,
"ref_id": credConfig.RefID,
}

cred, err := providers.CreateCredential(refID, configMap)
cred, err := providers.CreateCredential(refID, credEntry)
if err != nil {
return fmt.Errorf("failed to create credential: %w", err)
}

if location == "" {
location = credConfig.DefaultLocation
location = providers.GetDefaultLocation(cred)
}
if location == "" {
return fmt.Errorf("location is required (use --location or set default_location in config)")
Expand Down
17 changes: 8 additions & 9 deletions cmd/bcloud/commands/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,23 @@ func runGet(_ *cobra.Command, args []string) error {
refID := args[0]
instanceID := v1.CloudProviderInstanceID(args[1])

credConfig, exists := cfg.Credentials[refID]
credEntry, exists := cfg.Credentials[refID]
if !exists {
return fmt.Errorf("credential '%s' not found in config", refID)
}

configMap := map[string]interface{}{
"provider": credConfig.Provider,
"api_key": credConfig.APIKey,
"ref_id": credConfig.RefID,
}

cred, err := providers.CreateCredential(refID, configMap)
cred, err := providers.CreateCredential(refID, credEntry)
if err != nil {
return fmt.Errorf("failed to create credential: %w", err)
}

defaultLocation := providers.GetDefaultLocation(cred)
if defaultLocation == "" {
return fmt.Errorf("default location is required in config")
}

ctx := context.Background()
client, err := cred.MakeClient(ctx, credConfig.DefaultLocation)
client, err := cred.MakeClient(ctx, defaultLocation)
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
Expand Down
12 changes: 3 additions & 9 deletions cmd/bcloud/commands/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,18 @@ func runList(_ *cobra.Command, args []string) error {

refID := args[0]

credConfig, exists := cfg.Credentials[refID]
credEntry, exists := cfg.Credentials[refID]
if !exists {
return fmt.Errorf("credential '%s' not found in config", refID)
}

configMap := map[string]interface{}{
"provider": credConfig.Provider,
"api_key": credConfig.APIKey,
"ref_id": credConfig.RefID,
}

cred, err := providers.CreateCredential(refID, configMap)
cred, err := providers.CreateCredential(refID, credEntry)
if err != nil {
return fmt.Errorf("failed to create credential: %w", err)
}

if listLocation == "" {
listLocation = credConfig.DefaultLocation
listLocation = providers.GetDefaultLocation(cred)
}
if listLocation == "" {
return fmt.Errorf("location is required (use --location or set default_location in config)")
Expand Down
17 changes: 8 additions & 9 deletions cmd/bcloud/commands/terminate.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,23 @@ func runTerminate(_ *cobra.Command, args []string) error {
refID := args[0]
instanceID := v1.CloudProviderInstanceID(args[1])

credConfig, exists := cfg.Credentials[refID]
credEntry, exists := cfg.Credentials[refID]
if !exists {
return fmt.Errorf("credential '%s' not found in config", refID)
}

configMap := map[string]interface{}{
"provider": credConfig.Provider,
"api_key": credConfig.APIKey,
"ref_id": credConfig.RefID,
}

cred, err := providers.CreateCredential(refID, configMap)
cred, err := providers.CreateCredential(refID, credEntry)
if err != nil {
return fmt.Errorf("failed to create credential: %w", err)
}

defaultLocation := providers.GetDefaultLocation(cred)
if defaultLocation == "" {
return fmt.Errorf("default location is required in config")
}

ctx := context.Background()
client, err := cred.MakeClient(ctx, credConfig.DefaultLocation)
client, err := cred.MakeClient(ctx, defaultLocation)
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
Expand Down
12 changes: 3 additions & 9 deletions cmd/bcloud/commands/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,18 @@ func runTypes(_ *cobra.Command, args []string) error {

refID := args[0]

credConfig, exists := cfg.Credentials[refID]
credEntry, exists := cfg.Credentials[refID]
if !exists {
return fmt.Errorf("credential '%s' not found in config", refID)
}

configMap := map[string]interface{}{
"provider": credConfig.Provider,
"api_key": credConfig.APIKey,
"ref_id": credConfig.RefID,
}

cred, err := providers.CreateCredential(refID, configMap)
cred, err := providers.CreateCredential(refID, credEntry)
if err != nil {
return fmt.Errorf("failed to create credential: %w", err)
}

if typesLocation == "" {
typesLocation = credConfig.DefaultLocation
typesLocation = providers.GetDefaultLocation(cred)
}
if typesLocation == "" {
return fmt.Errorf("location is required (use --location or set default_location in config)")
Expand Down
131 changes: 112 additions & 19 deletions cmd/bcloud/config/config.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,114 @@
package config

import (
"encoding/json"
"fmt"
"os"
"path/filepath"

v1 "github.com/brevdev/cloud/pkg/v1"
"gopkg.in/yaml.v3"
)

type Config struct {
Credentials map[string]CredentialConfig `yaml:"credentials"`
Settings Settings `yaml:"settings"`
var providerRegistry = map[string]func() v1.CloudCredential{}

func RegisterProvider(id string, factory func() v1.CloudCredential) {
providerRegistry[id] = factory
}

type CredentialConfig struct {
Provider string `yaml:"provider"`
RefID string `yaml:"ref_id,omitempty"`
APIKey string `yaml:"api_key,omitempty"`
ServiceAccountKey string `yaml:"service_account_key,omitempty"`
ProjectID string `yaml:"project_id,omitempty"`
DefaultLocation string `yaml:"default_location,omitempty"`
type CredentialEntry struct {
Provider string `json:"provider" yaml:"provider"`
Value v1.CloudCredential `json:"-" yaml:"-"`
}

type Settings struct {
OutputFormat string `yaml:"output_format"`
DefaultTimeout string `yaml:"default_timeout"`
func (c *CredentialEntry) decodeFromMap(m map[string]any, yamlKey string) error {
rawProv, ok := m["provider"]
if !ok {
return fmt.Errorf("missing 'provider'")
}
provider, ok := rawProv.(string)
if !ok || provider == "" {
return fmt.Errorf("invalid 'provider'")
}
factory, ok := providerRegistry[provider]
if !ok {
return fmt.Errorf("unknown provider: %s", provider)
}
cred := factory()

if _, hasRefID := m["ref_id"]; !hasRefID {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets write a test that says all registered providers must have a ref_id in struct

m["ref_id"] = yamlKey
}

b, err := json.Marshal(m)
if err != nil {
return err
}
if err := json.Unmarshal(b, cred); err != nil {
return err
}

c.Provider = provider
c.Value = cred
return nil
}

func (c CredentialEntry) encodeToMap() (map[string]any, error) {
if c.Value == nil {
return nil, fmt.Errorf("nil credential Value")
}
b, err := json.Marshal(c.Value) // serialize provider-specific fields
if err != nil {
return nil, err
}
out := map[string]any{}
if err := json.Unmarshal(b, &out); err != nil {
return nil, err
}
out["provider"] = string(c.Value.GetCloudProviderID())
return out, nil
}

func (c *CredentialEntry) UnmarshalJSON(b []byte) error {
m := map[string]any{}
if err := json.Unmarshal(b, &m); err != nil {
return err
}
return c.decodeFromMap(m, "")
}

func (c CredentialEntry) MarshalJSON() ([]byte, error) {
m, err := c.encodeToMap()
if err != nil {
return nil, err
}
return json.Marshal(m)
}

func (c *CredentialEntry) UnmarshalYAML(n *yaml.Node) error {
m := map[string]any{}
if err := n.Decode(&m); err != nil {
return err
}
return c.decodeFromMap(m, "")
}

func (c *CredentialConfig) GetRefID(yamlKey string) string {
if c.RefID != "" {
return c.RefID
func (c CredentialEntry) MarshalYAML() (interface{}, error) {
m, err := c.encodeToMap()
if err != nil {
return nil, err
}
return yamlKey
return m, nil // let yaml encode the map
}

type Config struct {
Credentials map[string]CredentialEntry `json:"credentials" yaml:"credentials"`
Settings Settings `json:"settings" yaml:"settings"`
}

type Settings struct {
OutputFormat string `yaml:"output_format"`
DefaultTimeout string `yaml:"default_timeout"`
}

func LoadConfig() (*Config, error) {
Expand All @@ -55,11 +132,27 @@ func LoadConfig() (*Config, error) {
return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err)
}

var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
var rawConfig struct {
Credentials map[string]map[string]any `yaml:"credentials"`
Settings Settings `yaml:"settings"`
}
if err := yaml.Unmarshal(data, &rawConfig); err != nil {
return nil, fmt.Errorf("failed to parse config file %s: %w", configPath, err)
}

config := Config{
Credentials: make(map[string]CredentialEntry),
Settings: rawConfig.Settings,
}

for yamlKey, credData := range rawConfig.Credentials {
var credEntry CredentialEntry
if err := credEntry.decodeFromMap(credData, yamlKey); err != nil {
return nil, fmt.Errorf("failed to parse credential '%s': %w", yamlKey, err)
}
config.Credentials[yamlKey] = credEntry
}

if config.Settings.OutputFormat == "" {
config.Settings.OutputFormat = "yaml"
}
Expand Down
Loading