diff --git a/docs/docs/choose-provider/index.md b/docs/docs/choose-provider/index.md index 6748319ff96..5d746b36719 100644 --- a/docs/docs/choose-provider/index.md +++ b/docs/docs/choose-provider/index.md @@ -68,7 +68,7 @@ Every EC2 instance is built using an image. This image specifies the operating s AWS VPC is a Virtual Network! It allows you to create an complete logical network infrastructure for your VMs. This opens up a world of possibilities for using multiple VMs together. If you don't require any special networking, Koding handles this automatically. However, if you want to create a custom network environment, you can configure it simply within your Stack. - [**AWS Documentation**][16] - - [**Using IAM in your Stack**][17] + - [**Using VPC in your Stack**][17] - [**Stack Reference**][18] *** @@ -162,7 +162,7 @@ Monitor your instances for performance and uptime without resorting to third-par ## Microsoft Azure -# Coming Soon... + Coming Soon... [1]: {{ site.url }}/docs/creating-an-aws-stack [2]: {{ site.url }}/docs/terraform/providers/aws/ diff --git a/go/src/koding/kites/kloud/provider/azure/azure.go b/go/src/koding/kites/kloud/provider/azure/azure.go index 089fbc43f66..247194a3688 100644 --- a/go/src/koding/kites/kloud/provider/azure/azure.go +++ b/go/src/koding/kites/kloud/provider/azure/azure.go @@ -67,7 +67,7 @@ func newMetadata(m *stack.Machine) interface{} { } if cred, ok := m.Credential.Credential.(*Cred); ok { - meta.Location = cred.Location + meta.Location = string(cred.Location) } return meta @@ -81,14 +81,53 @@ type Subscription struct { ID string `xml:"Id,attr"` } +type ( + StorageType string + LocationType string +) + +var ( + _ stack.Enumer = StorageType("") + _ stack.Enumer = LocationType("") +) + +var ( + Storages = []stack.Enum{ + {Title: "Locally redundant storage (LRS)", Value: "Standard_LRS"}, + {Title: "Zone-redundant storage (ZRS)", Value: "Standard_ZRS"}, + {Title: "Geo-redundant storage (GRS)", Value: "Standard_GRS"}, + {Title: "Read-access geo-redundant storage (RA-GRS)", Value: "Standard_RAGRS"}, + {Title: "Premium Locally redundant storage (P_LRS)", Value: "Premium_LRS"}, + } + + Locations = []stack.Enum{ + {Title: "East US", Value: "East US"}, + {Title: "East US 2", Value: "East US 2"}, + {Title: "West US", Value: "West US"}, + {Title: "Central US", Value: "Central US"}, + {Title: "South Central US", Value: "South Central US"}, + {Title: "North Europe", Value: "North Europe"}, + {Title: "West Europe", Value: "West Europe"}, + {Title: "East Asia", Value: "East Asia"}, + {Title: "Southeast Asia", Value: "Southeast Asia"}, + {Title: "Japan East", Value: "Japan East"}, + {Title: "Japan West", Value: "Japan West"}, + {Title: "North Central US", Value: "North Central US"}, + {Title: "Brazil South", Value: "Brazil South"}, + } +) + +func (StorageType) Enums() []stack.Enum { return Storages } +func (LocationType) Enums() []stack.Enum { return Locations } + // Cred represents jCredentialDatas.meta for "azure" provider. type Cred struct { - PublishSettings string `json:"publish_settings" bson:"publish_settings" hcl:"publish_settings"` // required - SubscriptionID string `json:"subscription_id,omitempty" bson:"subscription_id,omitempty" hcl:"subscription_id"` // required if PublishSettings contains multiple subscriptions - Location string `json:"location,omitempty" bson:"location,omitempty" hcl:"location"` // by default "East US 2" - Storage string `json:"storage,omitempty" bson:"storage,omitempty" hcl:"storage"` // by default "Standard_LRS" - SSHKeyThumbprint string `json:"ssh_key_thumbprint,omitempty" bson:"ssh_key_thumbprint" hcl:"ssh_key_thumbprint"` - Password string `json:"password" bson:"password" hcl:"password"` + PublishSettings string `json:"publish_settings" bson:"publish_settings" hcl:"publish_settings"` // required + SubscriptionID string `json:"subscription_id,omitempty" bson:"subscription_id,omitempty" hcl:"subscription_id"` // required if PublishSettings contains multiple subscriptions + Location LocationType `json:"location,omitempty" bson:"location,omitempty" hcl:"location"` // by default "East US 2" + Storage StorageType `json:"storage,omitempty" bson:"storage,omitempty" hcl:"storage"` // by default "Standard_LRS" + SSHKeyThumbprint string `json:"ssh_key_thumbprint,omitempty" bson:"ssh_key_thumbprint" hcl:"ssh_key_thumbprint"` + Password string `json:"password" bson:"password" hcl:"password"` } var _ stack.Validator = (*Cred)(nil) diff --git a/go/src/koding/kites/kloud/provider/azure/stack.go b/go/src/koding/kites/kloud/provider/azure/stack.go index 81e278a8219..55dd55e99ec 100644 --- a/go/src/koding/kites/kloud/provider/azure/stack.go +++ b/go/src/koding/kites/kloud/provider/azure/stack.go @@ -119,7 +119,7 @@ func (s *Stack) BootstrapTemplates(c *stack.Credential) ([]*stack.Template, erro cfg := &BootstrapConfig{ TeamSlug: s.BootstrapArg().GroupName, HostedServiceName: "koding-hs-" + c.Identifier, - StorageType: cred.Storage, + StorageType: string(cred.Storage), AddressSpace: boot.addressSpace(), StorageServiceName: substringN(strings.ToLower("kodings"+c.Identifier), 24), SecurityGroupName: "koding-sg-" + c.Identifier, diff --git a/go/src/koding/kites/kloud/provider/do/schema.go b/go/src/koding/kites/kloud/provider/do/schema.go index 322c3e11012..137a36a3f4a 100644 --- a/go/src/koding/kites/kloud/provider/do/schema.go +++ b/go/src/koding/kites/kloud/provider/do/schema.go @@ -48,15 +48,35 @@ type Bootstrap struct { KeyFingerprint string `json:"key_fingerprint" bson:"key_fingerprint" hcl:"key_fingerprint"` } -// Region represents a DigitalOcean region -type Region string +// RegionType represents a DigitalOcean region +type RegionType string + +var _ stack.Enumer = RegionType("") + +var Regions = []stack.Enum{ + {Title: "Amsterdam 1", Value: "ams1"}, + {Title: "Amsterdam 2", Value: "ams2"}, + {Title: "Amsterdam 3", Value: "ams3"}, + {Title: "Bangalore 1", Value: "blr1"}, + {Title: "France 1", Value: "fra1"}, + {Title: "London 1", Value: "lon1"}, + {Title: "New York 1", Value: "nyc1"}, + {Title: "New York 2", Value: "nyc2"}, + {Title: "New York 3", Value: "nyc3"}, + {Title: "San Francisco 1", Value: "sfo1"}, + {Title: "San Francisco 2", Value: "sfo2"}, + {Title: "Singapore 1", Value: "sgp1"}, + {Title: "Toronto 1", Value: "tor1"}, +} + +func (RegionType) Enums() []stack.Enum { return Regions } // Metadata represent the data that is stored on Koding's DB storage type Metadata struct { - DropletID int `json:"droplet_id" bson:"droplet_id" hcl:"droplet_id"` - Region Region `json:"region" bson:"region" hcl:"region"` - Size string `json:"size" bson:"size" hcl:"size"` - Image string `json:"image" bson:"image" hcl:"image"` + DropletID int `json:"droplet_id" bson:"droplet_id" hcl:"droplet_id"` + Region RegionType `json:"region" bson:"region" hcl:"region"` + Size string `json:"size" bson:"size" hcl:"size"` + Image string `json:"image" bson:"image" hcl:"image"` } func newSchema() *provider.Schema { @@ -83,7 +103,7 @@ func newMetadata(m *stack.Machine) interface{} { meta := &Metadata{ Size: m.Attributes["size"], Image: m.Attributes["image"], - Region: Region(m.Attributes["region"]), + Region: RegionType(m.Attributes["region"]), } if id, err := strconv.Atoi(m.Attributes["id"]); err == nil { @@ -115,9 +135,8 @@ func (m *Metadata) Valid() error { return errors.New("region cannot be empty") } - if !m.Region.isValid() { - return fmt.Errorf("region %q is not valid. Valid regions can be one of the following: %v", - m.Region, validRegions) + if err := m.Region.Valid(); err != nil { + return err } if m.Image == "" { @@ -148,26 +167,12 @@ func (b *Bootstrap) Valid() error { return nil } -// isValid checks whether the given region is valid or not -func (r Region) isValid() bool { - for _, validRegion := range validRegions { - if validRegion == string(r) { - return true +// Valid implements the stack.Validator interface. +func (r RegionType) Valid() error { + for _, region := range Regions { + if r == RegionType(region.Value.(string)) { + return nil } } - return false -} - -// Enum implements the stack.Enumer interface -func (Region) Enum() []interface{} { - // TODO: check whether the valid regions have metadata listed as their - // features and only show those regions, because User Data is currently - // only available in regions with metadata listed in their features. - regions := make([]interface{}, len(validRegions)) - - for i, region := range validRegions { - regions[i] = region - } - - return regions + return fmt.Errorf("region %q does not exist", r) } diff --git a/go/src/koding/kites/kloud/provider/google/google.go b/go/src/koding/kites/kloud/provider/google/google.go index 13d57e06ff0..b93de6bf8e2 100644 --- a/go/src/koding/kites/kloud/provider/google/google.go +++ b/go/src/koding/kites/kloud/provider/google/google.go @@ -31,45 +31,42 @@ func init() { provider.Register(p) } -// Region represents google's geographical region code. -type Region string - -var regions = []Region{ - "asia-east1", - "europe-west1", - "us-central1", - "us-east1", - "us-west1", +// RegionType represents google's geographical region code. +type RegionType string + +var _ stack.Enumer = RegionType("") + +var Regions = []stack.Enum{ + {Title: "Asia West 1", Value: "asia-east1"}, + {Title: "Europte West 1", Value: "europe-west1"}, + {Title: "US Central 1", Value: "us-central1"}, + {Title: "US East 1", Value: "us-east1"}, + {Title: "US West 1", Value: "us-west1"}, } // Enum returns all available regions for "google" provider. -func (Region) Enum() (rs []interface{}) { - for _, region := range regions { - rs = append(rs, region) - } - return rs -} +func (RegionType) Enums() []stack.Enum { return Regions } // Valid checks if stored region code is available in GCP. -func (r Region) Valid() error { +func (r RegionType) Valid() error { if r == "" { return fmt.Errorf("region name is not set") } - for _, region := range regions { - if r == region { + for _, region := range Regions { + if r == RegionType(region.Value.(string)) { return nil } } - return fmt.Errorf("unknown region name: %v", r) + return fmt.Errorf("region %q does not exist", r) } // Cred represents jCredentialDatas.meta for "google" provider. type Cred struct { - Credentials string `json:"credentials" bson:"credentials" hcl:"credentials" kloud:",secret"` - Project string `json:"project" bson:"project" hcl:"project"` - Region Region `json:"region" bson:"region" hcl:"region"` + Credentials string `json:"credentials" bson:"credentials" hcl:"credentials" kloud:",secret"` + Project string `json:"project" bson:"project" hcl:"project"` + Region RegionType `json:"region" bson:"region" hcl:"region"` } var _ stack.Validator = (*Cred)(nil) @@ -139,12 +136,12 @@ func (b *Bootstrap) Valid() error { } type Meta struct { - Name string `json:"name" bson:"name" hcl:"name"` - Region Region `json:"region" bson:"region" hcl:"region"` - Zone string `json:"zone" bson:"zone" hcl:"zone"` - Image string `json:"image" bson:"image" hcl:"image"` - StorageSize int `json:"storage_size" bson:"storage_size" hcl:"storage_size"` - MachineType string `json:"machine_type" bson:"machine_type" hcl:"machine_type"` + Name string `json:"name" bson:"name" hcl:"name"` + Region RegionType `json:"region" bson:"region" hcl:"region"` + Zone string `json:"zone" bson:"zone" hcl:"zone"` + Image string `json:"image" bson:"image" hcl:"image"` + StorageSize int `json:"storage_size" bson:"storage_size" hcl:"storage_size"` + MachineType string `json:"machine_type" bson:"machine_type" hcl:"machine_type"` } var _ stack.Validator = (*Meta)(nil) diff --git a/go/src/koding/kites/kloud/stack/credential.go b/go/src/koding/kites/kloud/stack/credential.go index 45958fde32a..694e13f2501 100644 --- a/go/src/koding/kites/kloud/stack/credential.go +++ b/go/src/koding/kites/kloud/stack/credential.go @@ -48,6 +48,37 @@ type Enum struct { Value interface{} `json:"value"` } +// Enums is an enum list. +type Enums []Enum + +// Contains gives true if enums contains the given value. +func (e Enums) Contains(value interface{}) bool { + for _, e := range e { + if e.Value == value { + return true + } + } + return false +} + +// Values gives all enums' values. +func (e Enums) Values() []interface{} { + v := make([]interface{}, len(e)) + for i := range e { + v[i] = e[i].Value + } + return v +} + +// Titles gives all enums' titles. +func (e Enums) Titles() []string { + t := make([]string, len(e)) + for i := range e { + t[i] = e[i].Title + } + return t +} + // Value represents a description of a single // field within Bootstrap or Credential struct. type Value struct { @@ -56,7 +87,7 @@ type Value struct { Label string `json:"label"` Secret bool `json:"secret"` ReadOnly bool `json:"readOnly"` - Values []Enum `json:"values,omitempty"` + Values Enums `json:"values,omitempty"` } // CredentialListRequest represents a request @@ -204,8 +235,10 @@ type CredentialAddResponse struct { func (k *Kloud) CredentialDescribe(r *kite.Request) (interface{}, error) { var req CredentialDescribeRequest - if err := r.Args.One().Unmarshal(&req); err != nil { - return nil, err + if r.Args != nil { + if err := r.Args.One().Unmarshal(&req); err != nil { + return nil, err + } } // TODO: add support for reading the provider names by parsing diff --git a/go/src/koding/kites/kloud/stack/provider/describe.go b/go/src/koding/kites/kloud/stack/provider/describe.go index f1c4d8cecc2..f256dc37f3a 100644 --- a/go/src/koding/kites/kloud/stack/provider/describe.go +++ b/go/src/koding/kites/kloud/stack/provider/describe.go @@ -1,14 +1,19 @@ package provider import ( + "bytes" "fmt" "koding/kites/kloud/stack" "reflect" "strings" + "time" + "unicode" ) var describeTags = []string{"bson", "hcl", "json"} +var durationTyp = reflect.TypeOf((*time.Duration)(nil)).Elem() + // Describe creates a schema definition for the given v value. // // It is used in "credential.describe" kloud method, for @@ -46,7 +51,7 @@ func Describe(v interface{}) ([]stack.Value, error) { v := stack.Value{ Name: f.Name, - Label: strings.Title(f.Name), + Label: title(f.Name), } switch fTyp.Kind() { @@ -64,6 +69,10 @@ func Describe(v interface{}) ([]stack.Value, error) { v.Type = "object" // TODO(rjeczalik): this may be not needed } + if fTyp == durationTyp { + v.Type = "duration" + } + if enumer, ok := val.Field(i).Interface().(stack.Enumer); ok { v.Type = "enum" v.Values = enumer.Enums() @@ -113,3 +122,30 @@ func mustDescribe(v interface{}) []stack.Value { } return desc } + +var titler = strings.NewReplacer("_", " ") + +func title(s string) string { + if s == "" { + return "" + } + + var buf bytes.Buffer + + runes := []rune(strings.Title(titler.Replace(s))) + buf.WriteRune(runes[0]) + + for i := 1; i < len(runes)-1; i++ { + if !unicode.IsSpace(runes[i]) && !unicode.IsSpace(runes[i-1]) && unicode.IsUpper(runes[i]) && (unicode.IsLower(runes[i+1]) || unicode.IsLower(runes[i-1])) { + buf.WriteRune(' ') + } + + buf.WriteRune(runes[i]) + } + + if len(runes) != 1 { + buf.WriteRune(runes[len(runes)-1]) + } + + return buf.String() +} diff --git a/go/src/koding/kites/kloud/stack/provider/describe_test.go b/go/src/koding/kites/kloud/stack/provider/describe_test.go index c999be94af4..64a146f874d 100644 --- a/go/src/koding/kites/kloud/stack/provider/describe_test.go +++ b/go/src/koding/kites/kloud/stack/provider/describe_test.go @@ -3,6 +3,7 @@ package provider_test import ( "reflect" "testing" + "time" "koding/kites/kloud/provider/aws" "koding/kites/kloud/stack" @@ -10,11 +11,13 @@ import ( ) type Foo struct { - File []byte `kloud:",secret"` - N int `kloud:",readOnly"` - S string - M map[string]interface{} `kloud:"someMap"` - E FooEnum `json:"enums" kloud:"someEnums"` + File []byte `kloud:",secret"` + N int `kloud:",readOnly"` + S string + M map[string]interface{} `kloud:"someMap"` + E FooEnum `json:"enums" kloud:"someEnums"` + ThisIsTimeout time.Duration + AlsoTimeout time.Duration `json:"also_timeout"` } type FooEnum string @@ -78,6 +81,14 @@ func TestDescribe(t *testing.T) { Type: "enum", Label: "someEnums", Values: FooEnums, + }, { + Name: "ThisIsTimeout", + Type: "duration", + Label: "This Is Timeout", + }, { + Name: "also_timeout", + Type: "duration", + Label: "Also Timeout", }}, }, } @@ -95,3 +106,23 @@ func TestDescribe(t *testing.T) { } } } + +func TestTitle(t *testing.T) { + cases := map[string]string{ + "this_is_some_title": "This Is Some Title", + "ThisIsSomeTitle": "This Is Some Title", + "ANDThisIsALSO": "AND This Is ALSO", + "me_too": "Me Too", + "same": "Same", + "s": "S", + "This Is a Title": "This Is A Title", + } + + for s, want := range cases { + got := provider.Title(s) + + if got != want { + t.Errorf("%s: got %q, want %q", s, got, want) + } + } +} diff --git a/go/src/koding/kites/kloud/stack/provider/export_test.go b/go/src/koding/kites/kloud/stack/provider/export_test.go new file mode 100644 index 00000000000..c5035dc6ea2 --- /dev/null +++ b/go/src/koding/kites/kloud/stack/provider/export_test.go @@ -0,0 +1,6 @@ +package provider + +// Title exports title func for test purpose. +func Title(s string) string { + return title(s) +} diff --git a/go/src/koding/klientctl/credential.go b/go/src/koding/klientctl/credential.go index 2f40a9cbdf9..3f7562ea2b0 100644 --- a/go/src/koding/klientctl/credential.go +++ b/go/src/koding/klientctl/credential.go @@ -1,18 +1,23 @@ package main import ( + "bufio" "encoding/json" - "errors" "fmt" "io/ioutil" "os" + "strconv" + "strings" "text/tabwriter" + "time" + "koding/kites/config" "koding/kites/kloud/stack" "koding/klientctl/kloud/credential" "github.com/codegangsta/cli" "github.com/koding/logging" + "golang.org/x/crypto/ssh/terminal" ) func CredentialList(c *cli.Context, log logging.Logger, _ string) (int, error) { @@ -47,15 +52,151 @@ func CredentialList(c *cli.Context, log logging.Logger, _ string) (int, error) { return 0, nil } +func ask(format string, args ...interface{}) (string, error) { + fmt.Printf(format, args...) + s, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil { + return "", err + } + return strings.TrimSpace(s), nil +} + +func askSecret(format string, args ...interface{}) (string, error) { + fmt.Printf(format, args...) + p, err := terminal.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + if err != nil { + return "", err + } + return string(p), nil +} + +func AskCredentialCreate(c *cli.Context) (*credential.CreateOptions, error) { + descs, err := credential.Describe() + if err != nil { + return nil, err + } + + opts := &credential.CreateOptions{ + Provider: c.String("provider"), + Team: c.String("team"), + Title: c.String("title"), + } + + if opts.Provider == "" { + opts.Provider, err = ask("Provider type []: ") + if err != nil { + return nil, err + } + } + + if opts.Title == "" { + opts.Title = config.CurrentUser.Username + " " + time.Now().Format(time.ANSIC) + opts.Title, err = ask("Title [%s]: ", opts.Title) + if err != nil { + return nil, err + } + } + + desc, ok := descs[opts.Provider] + if !ok { + return nil, fmt.Errorf("provider %q does not exist", opts.Provider) + } + + creds := make(map[string]interface{}, len(desc.Credential)) + + // TODO(rjeczalik): Add field.OmitEmpty so we validate required + // fields client-side. + // + // TODO(rjeczalik): Refactor part which validates credential + // input on kloud/provider side to a separate library + // and use it here. + for _, field := range desc.Credential { + var value string + + if field.Secret { + value, err = askSecret("%s [***]: ", field.Label) + } else { + var defaultValue string + + switch { + case len(field.Values) > 0: + if s, ok := field.Values[0].Value.(string); ok { + defaultValue = s + } + case field.Type == "duration": + defaultValue = "0s" + case field.Type == "integer": + defaultValue = "0" + } + + value, err = ask("%s [%s]: ", field.Label, defaultValue) + + if value == "" { + value = defaultValue + } + } + + if err != nil { + return nil, err + } + + switch field.Type { + case "integer": + n, err := strconv.Atoi(value) + if err != nil { + return nil, fmt.Errorf("invalid integer for %q field: %s", field.Label, err) + } + + creds[field.Name] = n + case "duration": + d, err := time.ParseDuration(value) + if err != nil { + return nil, fmt.Errorf("invalid time duration for %q field: %s", field.Label, err) + } + + creds[field.Name] = d + case "enum": + if !field.Values.Contains(value) { + return nil, fmt.Errorf("invalid %q enumeration value for %q field - valid values are: %v", value, field.Label, field.Values.Values()) + } + + creds[field.Name] = value + default: + creds[field.Name] = value + } + } + + // TODO(rjeczalik): remove when support for generic team is implemented + if opts.Team == "" { + opts.Team, err = ask("Team name []: ") + if err != nil { + return nil, err + } + } + + p, err := json.Marshal(creds) + if err != nil { + return nil, err + } + + opts.Data = p + + return opts, nil +} + func CredentialCreate(c *cli.Context, log logging.Logger, _ string) (int, error) { var p []byte var err error + var opts *credential.CreateOptions switch file := c.String("file"); file { case "": - // TODO(rjeczalik): remove once interactive mode is implemented - fmt.Fprintln(os.Stderr, "No credential file was provided.") - return 1, errors.New("no credential file was provided") + opts, err = AskCredentialCreate(c) + if err != nil { + fmt.Fprintln(os.Stderr, "Error building credential data:", err) + return 1, err + } case "-": p, err = ioutil.ReadAll(os.Stdin) default: @@ -63,17 +204,19 @@ func CredentialCreate(c *cli.Context, log logging.Logger, _ string) (int, error) } if err != nil { - fmt.Fprintln(os.Stderr, "Error reading credential file: ", err) + fmt.Fprintln(os.Stderr, "Error reading credential file:", err) return 1, err } fmt.Fprintln(os.Stderr, "Creating credential... ") - opts := &credential.CreateOptions{ - Provider: c.String("provider"), - Team: c.String("team"), - Title: c.String("title"), - Data: p, + if opts == nil { + opts = &credential.CreateOptions{ + Provider: c.String("provider"), + Team: c.String("team"), + Title: c.String("title"), + Data: p, + } } cred, err := credential.Create(opts) diff --git a/go/src/koding/klientctl/kloud/credential/credential.go b/go/src/koding/klientctl/kloud/credential/credential.go index afe5620bf03..b84a348098d 100644 --- a/go/src/koding/klientctl/kloud/credential/credential.go +++ b/go/src/koding/klientctl/kloud/credential/credential.go @@ -44,9 +44,10 @@ func (opts *CreateOptions) Valid() error { type Client struct { Kloud *kloud.Client - once sync.Once // for c.init() - cached stack.Credentials - used map[string]string + once sync.Once // for c.init() + cached stack.Credentials + used map[string]string + describe map[string]*stack.Description } // List @@ -143,12 +144,28 @@ func (c *Client) Provider(identifier string) (provider string, err error) { return "", fmt.Errorf("credential: %q does not exist or is not shared with the user", identifier) } +func (c *Client) Describe() (map[string]*stack.Description, error) { + c.init() + + var req stack.CredentialDescribeRequest + var resp stack.CredentialDescribeResponse + + if err := c.kloud().Call("credential.describe", &req, &resp); err != nil { + return nil, err + } + + c.describe = resp.Description + + return c.describe, nil +} + // Close func (c *Client) Close() (err error) { if len(c.cached) != 0 { err = nonil( c.kloud().Cache().SetValue("credentials", c.cached), c.kloud().Cache().SetValue("credentials.used", c.used), + c.kloud().Cache().SetValue("credentials.describe", c.describe), ) } @@ -189,11 +206,13 @@ func (c *Client) init() { func (c *Client) readCache() { c.cached = make(stack.Credentials) c.used = make(map[string]string) + c.describe = make(map[string]*stack.Description) // Ignoring read error, if it's non-nil then empty cache is going to // be used instead. _ = c.kloud().Cache().GetValue("credentials", &c.cached) _ = c.kloud().Cache().GetValue("credentials.used", &c.used) + _ = c.kloud().Cache().GetValue("credentials.describe", &c.describe) // Flush cache on exit. ctlcli.CloseOnExit(c) @@ -219,6 +238,7 @@ func nonil(err ...error) error { func List(opts *ListOptions) (stack.Credentials, error) { return DefaultClient.List(opts) } func Create(opts *CreateOptions) (*stack.CredentialItem, error) { return DefaultClient.Create(opts) } +func Describe() (map[string]*stack.Description, error) { return DefaultClient.Describe() } func Use(identifier string) error { return DefaultClient.Use(identifier) } func Used() map[string]string { return DefaultClient.Used() } func Provider(identifier string) (string, error) { return DefaultClient.Provider(identifier) }