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