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 }