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  }