github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/romulus/listplans/list_plans.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // The listplans package contains implementation of the command that
     5  // can be used to list plans that are available for a charm.
     6  package listplans
     7  
     8  import (
     9  	"bytes"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"strings"
    14  
    15  	"github.com/gosuri/uitable"
    16  	"github.com/juju/cmd"
    17  	"github.com/juju/errors"
    18  	"github.com/juju/gnuflag"
    19  	api "github.com/juju/romulus/api/plan"
    20  	wireformat "github.com/juju/romulus/wireformat/plan"
    21  	"gopkg.in/macaroon-bakery.v2-unstable/httpbakery"
    22  	"gopkg.in/yaml.v2"
    23  
    24  	jujucmd "github.com/juju/juju/cmd"
    25  	rcmd "github.com/juju/juju/cmd/juju/romulus"
    26  	"github.com/juju/juju/cmd/modelcmd"
    27  	"github.com/juju/juju/cmd/output"
    28  )
    29  
    30  // apiClient defines the interface of the plan api client need by this command.
    31  type apiClient interface {
    32  	// GetAssociatedPlans returns the plans associated with the charm.
    33  	GetAssociatedPlans(charmURL string) ([]wireformat.Plan, error)
    34  }
    35  
    36  var newClient = func(apiRoot string, client *httpbakery.Client) (apiClient, error) {
    37  	return api.NewClient(api.APIRoot(apiRoot), api.HTTPClient(client))
    38  }
    39  
    40  const listPlansDoc = `
    41  List plans available for the specified charm.
    42  
    43  Examples:
    44      juju plans cs:webapp
    45  `
    46  
    47  // ListPlansCommand retrieves plans that are available for the specified charm
    48  type ListPlansCommand struct {
    49  	modelcmd.ControllerCommandBase
    50  
    51  	out      cmd.Output
    52  	CharmURL string
    53  }
    54  
    55  // NewListPlansCommand creates a new ListPlansCommand.
    56  func NewListPlansCommand() modelcmd.ControllerCommand {
    57  	return modelcmd.WrapController(&ListPlansCommand{})
    58  }
    59  
    60  // Info implements Command.Info.
    61  func (c *ListPlansCommand) Info() *cmd.Info {
    62  	return jujucmd.Info(&cmd.Info{
    63  		Name:    "plans",
    64  		Args:    "",
    65  		Purpose: "List plans.",
    66  		Doc:     listPlansDoc,
    67  		Aliases: []string{"list-plans"},
    68  	})
    69  }
    70  
    71  // Init reads and verifies the cli arguments for the ListPlansCommand
    72  func (c *ListPlansCommand) Init(args []string) error {
    73  	if len(args) == 0 {
    74  		return errors.New("missing arguments")
    75  	}
    76  	charmURL, args := args[0], args[1:]
    77  	if err := cmd.CheckEmpty(args); err != nil {
    78  		return errors.Errorf("unknown command line arguments: " + strings.Join(args, ","))
    79  	}
    80  	c.CharmURL = charmURL
    81  	return c.CommandBase.Init(args)
    82  }
    83  
    84  // SetFlags implements Command.SetFlags.
    85  func (c *ListPlansCommand) SetFlags(f *gnuflag.FlagSet) {
    86  	c.CommandBase.SetFlags(f)
    87  	defaultFormat := "tabular"
    88  	c.out.AddFlags(f, defaultFormat, map[string]cmd.Formatter{
    89  		"yaml":    cmd.FormatYaml,
    90  		"json":    cmd.FormatJson,
    91  		"smart":   cmd.FormatSmart,
    92  		"summary": formatSummary,
    93  		"tabular": formatTabular,
    94  	})
    95  }
    96  
    97  // Run implements Command.Run.
    98  // Retrieves the plan from the plans service. The set of plans to be
    99  // retrieved can be limited using the plan and isv flags.
   100  func (c *ListPlansCommand) Run(ctx *cmd.Context) (rErr error) {
   101  	client, err := c.BakeryClient()
   102  	if err != nil {
   103  		return errors.Annotate(err, "failed to create an http client")
   104  	}
   105  
   106  	resolver, err := rcmd.NewCharmStoreResolverForControllerCmd(&c.ControllerCommandBase)
   107  	if err != nil {
   108  		return errors.Trace(err)
   109  	}
   110  	resolvedURL, err := resolver.Resolve(client, c.CharmURL)
   111  	if err != nil {
   112  		return errors.Annotatef(err, "failed to resolve charmURL %v", c.CharmURL)
   113  	}
   114  	c.CharmURL = resolvedURL
   115  
   116  	apiRoot, err := rcmd.GetMeteringURLForControllerCmd(&c.ControllerCommandBase)
   117  	if err != nil {
   118  		return errors.Trace(err)
   119  	}
   120  	apiClient, err := newClient(apiRoot, client)
   121  	if err != nil {
   122  		return errors.Annotate(err, "failed to create a plan API client")
   123  	}
   124  
   125  	plans, err := apiClient.GetAssociatedPlans(c.CharmURL)
   126  	if err != nil {
   127  		return errors.Annotate(err, "failed to retrieve plans")
   128  	}
   129  
   130  	output := make([]plan, len(plans))
   131  	for i, p := range plans {
   132  		outputPlan := plan{
   133  			URL: p.URL,
   134  		}
   135  		def, err := readPlan(bytes.NewBufferString(p.Definition))
   136  		if err != nil {
   137  			return errors.Annotate(err, "failed to parse plan definition")
   138  		}
   139  		if def.Description != nil {
   140  			outputPlan.Price = def.Description.Price
   141  			outputPlan.Description = def.Description.Text
   142  		}
   143  		output[i] = outputPlan
   144  	}
   145  
   146  	if len(output) == 0 && c.out.Name() == "tabular" {
   147  		ctx.Infof("No plans to display.")
   148  	}
   149  
   150  	err = c.out.Write(ctx, output)
   151  	if err != nil {
   152  		return errors.Trace(err)
   153  	}
   154  
   155  	return nil
   156  }
   157  
   158  type plan struct {
   159  	URL         string `json:"plan" yaml:"plan"`
   160  	Price       string `json:"price" yaml:"price"`
   161  	Description string `json:"description" yaml:"description"`
   162  }
   163  
   164  // formatSummary returns a summary of available plans.
   165  func formatSummary(writer io.Writer, value interface{}) error {
   166  	plans, ok := value.([]plan)
   167  	if !ok {
   168  		return errors.Errorf("expected value of type %T, got %T", plans, value)
   169  	}
   170  	tw := output.TabWriter(writer)
   171  	p := func(values ...interface{}) {
   172  		for _, v := range values {
   173  			fmt.Fprintf(tw, "%s\t", v)
   174  		}
   175  		fmt.Fprintln(tw)
   176  	}
   177  	p("Plan", "Price")
   178  	for _, plan := range plans {
   179  		p(plan.URL, plan.Price)
   180  	}
   181  	tw.Flush()
   182  	return nil
   183  }
   184  
   185  // formatTabular returns a tabular summary of available plans.
   186  func formatTabular(writer io.Writer, value interface{}) error {
   187  	plans, ok := value.([]plan)
   188  	if !ok {
   189  		return errors.Errorf("expected value of type %T, got %T", plans, value)
   190  	}
   191  
   192  	table := uitable.New()
   193  	table.MaxColWidth = 50
   194  	table.Wrap = true
   195  
   196  	table.AddRow("Plan", "Price", "Description")
   197  	for _, plan := range plans {
   198  		table.AddRow(plan.URL, plan.Price, plan.Description)
   199  	}
   200  	fmt.Fprint(writer, table)
   201  	return nil
   202  }
   203  
   204  type planModel struct {
   205  	Description *descriptionModel `json:"description,omitempty"`
   206  }
   207  
   208  // descriptionModel provides a human readable description of the plan.
   209  type descriptionModel struct {
   210  	Price string `json:"price,omitempty"`
   211  	Text  string `json:"text,omitempty"`
   212  }
   213  
   214  // readPlan reads, parses and returns a planModel struct representation.
   215  func readPlan(r io.Reader) (plan *planModel, err error) {
   216  	data, err := ioutil.ReadAll(r)
   217  	if err != nil {
   218  		return
   219  	}
   220  
   221  	var doc planModel
   222  	err = yaml.Unmarshal(data, &doc)
   223  	if err != nil {
   224  		return
   225  	}
   226  	return &doc, nil
   227  }