github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/crossmodel/list.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package crossmodel 5 6 import ( 7 "fmt" 8 "regexp" 9 "time" 10 11 "github.com/juju/cmd" 12 "github.com/juju/errors" 13 "github.com/juju/gnuflag" 14 "gopkg.in/juju/charm.v6" 15 16 "github.com/juju/juju/api/applicationoffers" 17 jujucmd "github.com/juju/juju/cmd" 18 "github.com/juju/juju/cmd/juju/common" 19 "github.com/juju/juju/cmd/modelcmd" 20 "github.com/juju/juju/core/crossmodel" 21 "github.com/juju/juju/jujuclient" 22 ) 23 24 const listCommandDoc = ` 25 List information about applications' endpoints that have been shared and who is connected. 26 27 The default tabular output shows each user connected (relating to) the offer, and the 28 relation id of the relation. 29 30 The summary output shows one row per offer, with a count of active/total relations. 31 32 The YAML output shows additional information about the source of connections, including 33 the source model UUID. 34 35 The output can be filtered by: 36 - interface: the interface name of the endpoint 37 - application: the name of the offered application 38 - connected user: the name of a user who has a relation to the offer 39 - allowed consumer: the name of a user allowed to consume the offer 40 - active only: only show offers which are in use (are related to) 41 42 Examples: 43 $ juju offers 44 $ juju offers -m model 45 $ juju offers --interface db2 46 $ juju offers --application mysql 47 $ juju offers --connected-user fred 48 $ juju offers --allowed-consumer mary 49 $ juju offers hosted-mysql 50 $ juju offers hosted-mysql --active-only 51 52 See also: 53 find-offers 54 show-offer 55 ` 56 57 // listCommand returns storage instances. 58 type listCommand struct { 59 modelcmd.ModelCommandBase 60 61 out cmd.Output 62 63 newAPIFunc func() (ListAPI, error) 64 refreshModels func(jujuclient.ClientStore, string) error 65 66 activeOnly bool 67 interfaceName string 68 applicationName string 69 connectedUserName string 70 consumerName string 71 offerName string 72 filters []crossmodel.ApplicationOfferFilter 73 } 74 75 // NewListEndpointsCommand constructs new list endpoint command. 76 func NewListEndpointsCommand() cmd.Command { 77 listCmd := &listCommand{} 78 listCmd.newAPIFunc = func() (ListAPI, error) { 79 return listCmd.NewApplicationOffersAPI() 80 } 81 listCmd.refreshModels = listCmd.ModelCommandBase.RefreshModels 82 return modelcmd.Wrap(listCmd) 83 } 84 85 // NewApplicationOffersAPI returns an application offers api for the root api endpoint 86 // that the command returns. 87 func (c *listCommand) NewApplicationOffersAPI() (*applicationoffers.Client, error) { 88 root, err := c.NewControllerAPIRoot() 89 if err != nil { 90 return nil, err 91 } 92 return applicationoffers.NewClient(root), nil 93 } 94 95 // Init implements Command.Init. 96 func (c *listCommand) Init(args []string) (err error) { 97 offerName, err := cmd.ZeroOrOneArgs(args) 98 if err != nil { 99 return errors.Trace(err) 100 } 101 c.offerName = offerName 102 return nil 103 } 104 105 // Info implements Command.Info. 106 func (c *listCommand) Info() *cmd.Info { 107 return jujucmd.Info(&cmd.Info{ 108 Name: "offers", 109 Args: "[<offer-name>]", 110 Aliases: []string{"list-offers"}, 111 Purpose: "Lists shared endpoints.", 112 Doc: listCommandDoc, 113 }) 114 } 115 116 // SetFlags implements Command.SetFlags. 117 func (c *listCommand) SetFlags(f *gnuflag.FlagSet) { 118 c.ModelCommandBase.SetFlags(f) 119 f.StringVar(&c.applicationName, "application", "", "return results matching the application") 120 f.StringVar(&c.interfaceName, "interface", "", "return results matching the interface name") 121 f.StringVar(&c.consumerName, "allowed-consumer", "", "return results where the user is allowed to consume the offer") 122 f.StringVar(&c.connectedUserName, "connected-user", "", "return results where the user has a connection to the offer") 123 f.BoolVar(&c.activeOnly, "active-only", false, "only return results where the offer is in use") 124 c.out.AddFlags(f, "tabular", map[string]cmd.Formatter{ 125 "yaml": cmd.FormatYaml, 126 "json": cmd.FormatJson, 127 "tabular": formatListTabular, 128 "summary": formatListSummary, 129 }) 130 } 131 132 // Run implements Command.Run. 133 func (c *listCommand) Run(ctx *cmd.Context) (err error) { 134 api, err := c.newAPIFunc() 135 if err != nil { 136 return err 137 } 138 defer api.Close() 139 140 controllerName, err := c.ControllerName() 141 if err != nil { 142 return errors.Trace(err) 143 } 144 145 modelName, _, err := c.ModelDetails() 146 if err != nil { 147 return errors.Trace(err) 148 } 149 if !jujuclient.IsQualifiedModelName(modelName) { 150 store := modelcmd.QualifyingClientStore{c.ClientStore()} 151 var err error 152 modelName, err = store.QualifiedModelName(controllerName, modelName) 153 if err != nil { 154 return errors.Trace(err) 155 } 156 } 157 158 unqualifiedModelName, ownerTag, err := jujuclient.SplitModelName(modelName) 159 if err != nil { 160 return errors.Trace(err) 161 } 162 c.filters = []crossmodel.ApplicationOfferFilter{{ 163 OwnerName: ownerTag.Name(), 164 ModelName: unqualifiedModelName, 165 ApplicationName: c.applicationName, 166 }} 167 if c.offerName != "" { 168 c.filters[0].OfferName = fmt.Sprintf("^%v$", regexp.QuoteMeta(c.offerName)) 169 } 170 if c.interfaceName != "" { 171 c.filters[0].Endpoints = []crossmodel.EndpointFilterTerm{{ 172 Interface: c.interfaceName, 173 }} 174 } 175 if c.connectedUserName != "" { 176 c.filters[0].ConnectedUsers = []string{c.connectedUserName} 177 } 178 if c.consumerName != "" { 179 c.filters[0].AllowedConsumers = []string{c.consumerName} 180 } 181 182 offeredApplications, err := api.ListOffers(c.filters...) 183 if err != nil { 184 return err 185 } 186 187 // For now, all offers come from the one controller. 188 data, err := formatApplicationOfferDetails(controllerName, offeredApplications, c.activeOnly) 189 if err != nil { 190 return errors.Annotate(err, "failed to format found applications") 191 } 192 193 return c.out.Write(ctx, data) 194 } 195 196 // ListAPI defines the API methods that list endpoints command use. 197 type ListAPI interface { 198 Close() error 199 ListOffers(filters ...crossmodel.ApplicationOfferFilter) ([]*crossmodel.ApplicationOfferDetails, error) 200 } 201 202 // ListOfferItem defines the serialization behaviour of an offer item in endpoints list. 203 type ListOfferItem struct { 204 // OfferName is the name of the offer. 205 OfferName string `yaml:"-" json:"-"` 206 207 // ApplicationName is the application backing this offer. 208 ApplicationName string `yaml:"application" json:"application"` 209 210 // Store is the controller hosting this offer. 211 Source string `yaml:"store,omitempty" json:"store,omitempty"` 212 213 // CharmURL is the charm URL of this application. 214 CharmURL string `yaml:"charm,omitempty" json:"charm,omitempty"` 215 216 // OfferURL is part of Juju location where this offer is shared relative to the store. 217 OfferURL string `yaml:"offer-url" json:"offer-url"` 218 219 // Endpoints is a list of application endpoints. 220 Endpoints map[string]RemoteEndpoint `yaml:"endpoints" json:"endpoints"` 221 222 // Connections holds details of connections to the offer. 223 Connections []offerConnectionDetails `yaml:"connections,omitempty" json:"connections,omitempty"` 224 225 // Users are the users who can consume the offer. 226 Users map[string]OfferUser `yaml:"users,omitempty" json:"users,omitempty"` 227 } 228 229 type offeredApplications map[string]ListOfferItem 230 231 type offerConnectionStatus struct { 232 Current string `json:"current" yaml:"current"` 233 Message string `json:"message,omitempty" yaml:"message,omitempty"` 234 Since string `json:"since,omitempty" yaml:"since,omitempty"` 235 } 236 237 type offerConnectionDetails struct { 238 SourceModelUUID string `json:"source-model-uuid" yaml:"source-model-uuid"` 239 Username string `json:"username" yaml:"username"` 240 RelationId int `json:"relation-id" yaml:"relation-id"` 241 Endpoint string `json:"endpoint" yaml:"endpoint"` 242 Status offerConnectionStatus `json:"status" yaml:"status"` 243 IngressSubnets []string `json:"ingress-subnets,omitempty" yaml:"ingress-subnets,omitempty"` 244 } 245 246 func formatApplicationOfferDetails(store string, all []*crossmodel.ApplicationOfferDetails, activeOnly bool) (offeredApplications, error) { 247 result := make(offeredApplications) 248 for _, one := range all { 249 if activeOnly && len(one.Connections) == 0 { 250 continue 251 } 252 url, err := crossmodel.ParseOfferURL(one.OfferURL) 253 if err != nil { 254 return nil, errors.Annotatef(err, "%v", one.OfferURL) 255 } 256 if url.Source == "" { 257 url.Source = store 258 } 259 260 // Store offers by name. 261 result[one.OfferName] = convertOfferToListItem(url, one) 262 } 263 return result, nil 264 } 265 266 func convertOfferToListItem(url *crossmodel.OfferURL, offer *crossmodel.ApplicationOfferDetails) ListOfferItem { 267 item := ListOfferItem{ 268 OfferName: offer.OfferName, 269 ApplicationName: offer.ApplicationName, 270 Source: url.Source, 271 CharmURL: offer.CharmURL, 272 OfferURL: offer.OfferURL, 273 Endpoints: convertCharmEndpoints(offer.Endpoints...), 274 Users: convertUsers(offer.Users...), 275 } 276 for _, conn := range offer.Connections { 277 item.Connections = append(item.Connections, offerConnectionDetails{ 278 SourceModelUUID: conn.SourceModelUUID, 279 Username: conn.Username, 280 RelationId: conn.RelationId, 281 Endpoint: conn.Endpoint, 282 Status: offerConnectionStatus{ 283 Current: conn.Status.String(), 284 Message: conn.Message, 285 Since: friendlyDuration(conn.Since), 286 }, 287 IngressSubnets: conn.IngressSubnets, 288 }) 289 } 290 return item 291 } 292 293 func friendlyDuration(when *time.Time) string { 294 if when == nil { 295 return "" 296 } 297 return common.UserFriendlyDuration(*when, time.Now()) 298 } 299 300 // convertCharmEndpoints takes any number of charm relations and 301 // creates a collection of ui-formatted endpoints. 302 func convertCharmEndpoints(relations ...charm.Relation) map[string]RemoteEndpoint { 303 if len(relations) == 0 { 304 return nil 305 } 306 output := make(map[string]RemoteEndpoint, len(relations)) 307 for _, one := range relations { 308 output[one.Name] = RemoteEndpoint{one.Name, one.Interface, string(one.Role)} 309 } 310 return output 311 }