Skip to content

Commit

Permalink
Adds interactive add-cloud command
Browse files Browse the repository at this point in the history
  • Loading branch information
natefinch committed Nov 2, 2016
1 parent bc10615 commit fc1e670
Show file tree
Hide file tree
Showing 22 changed files with 1,678 additions and 58 deletions.
21 changes: 21 additions & 0 deletions cloud/clouds.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ const (
// EmptyAuthType is the authentication type used for providers
// that require no credentials, e.g. "lxd", and "manual".
EmptyAuthType AuthType = "empty"

// AuthTypesKey is the name of the key in a cloud config or cloud schema
// that holds the cloud's auth types.
AuthTypesKey = "auth-types"

// EndpointKey is the name of the key in a cloud config or cloud schema
// that holds the cloud's endpoint url.
EndpointKey = "endpoint"

// RegionsKey is the name of the key in a cloud schema that holds the list
// of regions a cloud supports.
RegionsKey = "regions"
)

// Attrs serves as a map to hold regions specific configuration attributes.
Expand Down Expand Up @@ -266,6 +278,15 @@ func PublicCloudMetadata(searchPath ...string) (result map[string]Cloud, fallbac
return clouds, true, err
}

// ParseOneCloud parses the given yaml bytes into a single Cloud metadata.
func ParseOneCloud(data []byte) (Cloud, error) {
c := &cloud{}
if err := yaml.Unmarshal(data, &c); err != nil {
return Cloud{}, errors.Annotate(err, "cannot unmarshal yaml cloud metadata")
}
return cloudFromInternal(c), nil
}

// ParseCloudMetadata parses the given yaml bytes into Clouds metadata.
func ParseCloudMetadata(data []byte) (map[string]Cloud, error) {
var metadata cloudSet
Expand Down
195 changes: 181 additions & 14 deletions cmd/juju/cloud/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,22 @@
package cloud

import (
"fmt"
"sort"
"strings"

yaml "gopkg.in/yaml.v1"

"github.com/juju/ansiterm"
"github.com/juju/cmd"
"github.com/juju/errors"
"github.com/juju/gnuflag"
"github.com/juju/jsonschema"

"github.com/juju/juju/cloud"
"github.com/juju/juju/cmd/juju/common"
"github.com/juju/juju/cmd/juju/interact"
"github.com/juju/juju/environs"
)

var usageAddCloudSummary = `
Expand Down Expand Up @@ -70,15 +80,22 @@ func (c *addCloudCommand) SetFlags(f *gnuflag.FlagSet) {
}

func (c *addCloudCommand) Init(args []string) (err error) {
if len(args) < 2 {
return errors.New("Usage: juju add-cloud <cloud name> <cloud definition file>")
if len(args) > 0 {
c.Cloud = args[0]
}
if len(args) > 1 {
c.CloudFile = args[1]
}
if len(args) > 2 {
return cmd.CheckEmpty(args[2:])
}
c.Cloud = args[0]
c.CloudFile = args[1]
return cmd.CheckEmpty(args[2:])
return nil
}

func (c *addCloudCommand) Run(ctxt *cmd.Context) error {
if c.CloudFile == "" {
return c.runInteractive(ctxt)
}
specifiedClouds, err := cloud.ParseCloudMetadataFile(c.CloudFile)
if err != nil {
return err
Expand All @@ -90,27 +107,177 @@ func (c *addCloudCommand) Run(ctxt *cmd.Context) error {
if !ok {
return errors.Errorf("cloud %q not found in file %q", c.Cloud, c.CloudFile)
}
publicClouds, _, err := cloud.PublicCloudMetadata()

if err := c.verifyName(c.Cloud); err != nil {
return errors.Trace(err)
}

return addCloud(c.Cloud, newCloud)
}

func (c *addCloudCommand) runInteractive(ctxt *cmd.Context) error {
allproviders := environs.RegisteredProviders()

public, _, err := cloud.PublicCloudMetadata()
if err != nil {
return err
}
if _, ok = publicClouds[c.Cloud]; ok && !c.Replace {
return errors.Errorf("%q is the name of a public cloud; use --replace to use your cloud definition instead", c.Cloud)
personal, err := cloud.PersonalCloudMetadata()
if err != nil {
return err
}

var unsupported []string
var providers []string
for _, name := range allproviders {
provider, err := environs.Provider(name)
if err != nil {
// should be impossible
return errors.Trace(err)
}

if provider.CloudSchema() != nil {
providers = append(providers, name)
} else {
unsupported = append(unsupported, name)
}
}
sort.Strings(providers)

supportedCloud := interact.VerifyOptions("cloud type", providers, false)

cloudVerify := func(s string) (ok bool, errmsg string, err error) {
ok, errmsg, err = supportedCloud(s)
if err != nil {
return false, "", errors.Trace(err)
}
if ok {
return true, "", nil
}
// Print out a different message if they entered a valid provider that
// just isn't something we want people to add (like ec2).
for _, name := range unsupported {
if strings.ToLower(name) == strings.ToLower(s) {
return false, fmt.Sprintf("Cloud type %q not supported for interactive add-cloud.", s), nil
}
}
return false, errmsg, nil
}

w := ansiterm.NewWriter(ctxt.Stdout)
pollster := interact.New(ctxt.Stdin, ctxt.Stdout, errWriter{w})
cloudType, err := pollster.SelectVerify(interact.List{
Singular: "cloud type",
Plural: "cloud types",
Options: providers,
}, cloudVerify)

if err != nil {
return errors.Trace(err)
}

var name string
for {
name, err = pollster.Enter("a name for the cloud")
if err != nil {
return errors.Trace(err)
}
err = nameExists(name, public, personal)
if err == nil {
break
}
// we do this instead of returning the error message
override, err2 := pollster.YN(err.Error()+", do you want to override that definition", false)
if err2 != nil {
return errors.Trace(err)
}
if override {
break
}
// else, ask again
}

provider, err := environs.Provider(cloudType)
if err != nil {
return errors.Trace(err)
}

schema := provider.CloudSchema()
v, err := pollster.QuerySchema(schema)
if err != nil {
return errors.Trace(err)
}
b, err := yaml.Marshal(v)
if err != nil {
return errors.Trace(err)
}
newCloud, err := cloud.ParseOneCloud(b)
if err != nil {
return errors.Trace(err)
}
newCloud.Type = cloudType
err = addCloud(name, newCloud)
if err == nil {
ctxt.Infof("Cloud %q successfully added", name)
ctxt.Infof("You may bootstrap with 'juju bootstrap %s'", name)
}
return err
}

type errWriter struct {
w *ansiterm.Writer
}

func (w errWriter) Write(b []byte) (n int, err error) {
w.w.SetForeground(ansiterm.BrightRed)
defer w.w.Reset()
return w.w.Write(b)
}

func (c *addCloudCommand) verifyName(name string) error {
if c.Replace {
return nil
}
public, _, err := cloud.PublicCloudMetadata()
if err != nil {
return err
}
personal, err := cloud.PersonalCloudMetadata()
if err != nil {
return err
}
if err := nameExists(name, public, personal); err != nil {
return errors.Errorf(err.Error() + "; use --replace to override this definition")
}
return nil
}

func nameExists(name string, public, personal map[string]cloud.Cloud) error {
if _, ok := public[name]; ok {
return errors.Errorf("%q is the name of a public cloud", name)
}
builtinClouds := common.BuiltInClouds()
if _, ok = builtinClouds[c.Cloud]; ok && !c.Replace {
return errors.Errorf("%q is the name of a built-in cloud; use --replace to use your cloud definition instead", c.Cloud)
if _, ok := builtinClouds[name]; ok {
return errors.Errorf("%q is the name of a built-in cloud", name)
}
if _, ok := personal[name]; ok {
return errors.Errorf("%q already exists", name)
}
return nil
}

func querySchema(schema *jsonschema.Schema, pollster *interact.Pollster) error {
return nil
}

func addCloud(name string, newCloud cloud.Cloud) error {
personalClouds, err := cloud.PersonalCloudMetadata()
if err != nil {
return err
}
if _, ok = personalClouds[c.Cloud]; ok && !c.Replace {
return errors.Errorf("%q already exists; use --replace to replace this existing cloud", c.Cloud)
}
if personalClouds == nil {
personalClouds = make(map[string]cloud.Cloud)
}
personalClouds[c.Cloud] = newCloud
personalClouds[name] = newCloud
return cloud.WritePersonalCloudMetadata(personalClouds)
}
Loading

0 comments on commit fc1e670

Please sign in to comment.