diff --git a/apiserver/facades/client/charmhub/convert.go b/apiserver/facades/client/charmhub/convert.go index d420101fe60..3ea79f73040 100644 --- a/apiserver/facades/client/charmhub/convert.go +++ b/apiserver/facades/client/charmhub/convert.go @@ -103,6 +103,10 @@ func transformChannelMap(channelMap []transport.ChannelMap) ([]string, map[strin channels := make(map[string]params.Channel, len(channelMap)) for _, cm := range channelMap { ch := cm.Channel + // Per the charmhub/snap channel spec. + if ch.Track == "" { + ch.Track = "latest" + } chName := ch.Track + "/" + ch.Risk channels[chName] = params.Channel{ Revision: cm.Revision.Revision, diff --git a/cmd/juju/charmhub/info.go b/cmd/juju/charmhub/info.go index db1d4268ff0..89d703cd607 100644 --- a/cmd/juju/charmhub/info.go +++ b/cmd/juju/charmhub/info.go @@ -43,7 +43,7 @@ type infoCommand struct { api InfoCommandAPI - verbose bool + config bool charmOrBundle string } @@ -63,6 +63,7 @@ func (c *infoCommand) Info() *cmd.Info { // It implements part of the cmd.Command interface. func (c *infoCommand) SetFlags(f *gnuflag.FlagSet) { c.ModelCommandBase.SetFlags(f) + f.BoolVar(&c.config, "config", false, "display config for this charm") c.out.AddFlags(f, "tabular", map[string]cmd.Formatter{ "yaml": cmd.FormatYaml, "json": cmd.FormatJson, @@ -107,7 +108,6 @@ func (c *infoCommand) Run(ctx *cmd.Context) error { if err != nil { return errors.Trace(err) } - return c.out.Write(ctx, &view) } @@ -138,7 +138,7 @@ func (c *infoCommand) formatter(writer io.Writer, value interface{}) error { return errors.Errorf("unexpected results") } - if err := makeInfoWriter(writer, c.warningLog, results).Print(); err != nil { + if err := makeInfoWriter(writer, c.warningLog, c.config, results).Print(); err != nil { return errors.Trace(err) } diff --git a/cmd/juju/charmhub/infowriter.go b/cmd/juju/charmhub/infowriter.go index e0f24ab23d0..51b6b115731 100644 --- a/cmd/juju/charmhub/infowriter.go +++ b/cmd/juju/charmhub/infowriter.go @@ -23,11 +23,12 @@ import ( // There are exceptions, slices of strings and tables. These // are transformed into strings. -func makeInfoWriter(w io.Writer, warningLog Log, in *InfoResponse) Printer { +func makeInfoWriter(w io.Writer, warningLog Log, config bool, in *InfoResponse) Printer { iw := infoWriter{ - w: w, - warningf: warningLog, - in: in, + w: w, + warningf: warningLog, + in: in, + displayConfig: config, } if iw.in.Type == "charm" { return charmInfoWriter{infoWriter: iw} @@ -36,9 +37,10 @@ func makeInfoWriter(w io.Writer, warningLog Log, in *InfoResponse) Printer { } type infoWriter struct { - warningf Log - w io.Writer - in *InfoResponse + warningf Log + w io.Writer + in *InfoResponse + displayConfig bool } func (iw infoWriter) print(info interface{}) error { @@ -136,18 +138,19 @@ func (b bundleInfoWriter) Print() error { } type charmInfoOutput struct { - Name string `yaml:"name,omitempty"` - ID string `yaml:"charm-id,omitempty"` - Summary string `yaml:"summary,omitempty"` - Publisher string `yaml:"publisher,omitempty"` - Supports string `yaml:"supports,omitempty"` - Tags string `yaml:"tags,omitempty"` - Subordinate bool `yaml:"subordinate"` - StoreURL string `yaml:"store-url,omitempty"` - Description string `yaml:"description,omitempty"` - Relations relationOutput `yaml:"relations,omitempty"` - Channels string `yaml:"channels,omitempty"` - Installed string `yaml:"installed,omitempty"` + Name string `yaml:"name,omitempty"` + ID string `yaml:"charm-id,omitempty"` + Summary string `yaml:"summary,omitempty"` + Publisher string `yaml:"publisher,omitempty"` + Supports string `yaml:"supports,omitempty"` + Tags string `yaml:"tags,omitempty"` + Subordinate bool `yaml:"subordinate"` + StoreURL string `yaml:"store-url,omitempty"` + Description string `yaml:"description,omitempty"` + Relations relationOutput `yaml:"relations,omitempty"` + Channels string `yaml:"channels,omitempty"` + Installed string `yaml:"installed,omitempty"` + Config map[string]interface{} `yaml:"config,omitempty"` } type relationOutput struct { @@ -166,12 +169,17 @@ func (c charmInfoWriter) Print() error { Summary: c.in.Summary, Publisher: c.in.Publisher, Supports: strings.Join(c.in.Series, ", "), + StoreURL: c.in.StoreURL, Description: c.in.Description, Channels: c.channels(), Tags: strings.Join(c.in.Tags, ", "), } if c.in.Charm != nil { out.Subordinate = c.in.Charm.Subordinate + if c.displayConfig && c.in.Charm.Config != nil { + out.Config = make(map[string]interface{}, 1) + out.Config["settings"] = c.in.Charm.Config.Options + } } if rels, err := c.relations(); err == nil { out.Relations = rels diff --git a/cmd/juju/charmhub/infowriter_test.go b/cmd/juju/charmhub/infowriter_test.go index f1290e55f9a..0b2cbb64d91 100644 --- a/cmd/juju/charmhub/infowriter_test.go +++ b/cmd/juju/charmhub/infowriter_test.go @@ -6,6 +6,7 @@ package charmhub import ( "bytes" + "github.com/juju/charm/v8" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" ) @@ -17,7 +18,7 @@ var _ = gc.Suite(&printInfoSuite{}) func (s *printInfoSuite) TestCharmPrintInfo(c *gc.C) { ir := getCharmInfoResponse() ctx := commandContextForTest(c) - iw := makeInfoWriter(ctx.Stdout, ctx.Warningf, &ir) + iw := makeInfoWriter(ctx.Stdout, ctx.Warningf, false, &ir) err := iw.Print() c.Assert(err, jc.ErrorIsNil) @@ -48,10 +49,54 @@ channels: | c.Assert(obtained, gc.Equals, expected) } +func (s *printInfoSuite) TestCharmPrintInfoWithConfig(c *gc.C) { + ir := getCharmInfoResponse() + ctx := commandContextForTest(c) + iw := makeInfoWriter(ctx.Stdout, ctx.Warningf, true, &ir) + err := iw.Print() + c.Assert(err, jc.ErrorIsNil) + + obtained := ctx.Stdout.(*bytes.Buffer).String() + expected := `name: wordpress +charm-id: charmCHARMcharmCHARMcharmCHARM01 +summary: WordPress is a full featured web blogging tool, this charm deploys it. +publisher: Wordress Charmers +supports: bionic, xenial +tags: app, seven +subordinate: true +description: |- + This will install and setup WordPress optimized to run in the cloud. + By default it will place Ngnix and php-fpm configured to scale horizontally with + Nginx's reverse proxy. +relations: + provides: + one: two + three: four + requires: + five: six +channels: | + latest/stable: 1.0.3 2019-12-16 (16) 12MB + latest/candidate: 1.0.3 2019-12-16 (17) 12MB + latest/beta: 1.0.3 2019-12-16 (17) 12MB + latest/edge: 1.0.3 2019-12-16 (18) 12MB +config: + settings: + status: + type: string + description: temporary string for unit status + default: hello + thing: + type: string + description: A thing used by the charm. + default: "\U0001F381" +` + c.Assert(obtained, gc.Equals, expected) +} + func (s *printInfoSuite) TestBundleChannelClosed(c *gc.C) { ir := getBundleInfoClosedTrack() ctx := commandContextForTest(c) - iw := makeInfoWriter(ctx.Stdout, ctx.Warningf, &ir) + iw := makeInfoWriter(ctx.Stdout, ctx.Warningf, false, &ir) err := iw.Print() c.Assert(err, jc.ErrorIsNil) @@ -73,7 +118,7 @@ channels: | func (s *printInfoSuite) TestBundlePrintInfo(c *gc.C) { ir := getBundleInfoResponse() ctx := commandContextForTest(c) - iw := makeInfoWriter(ctx.Stdout, ctx.Warningf, &ir) + iw := makeInfoWriter(ctx.Stdout, ctx.Warningf, false, &ir) err := iw.Print() c.Assert(err, jc.ErrorIsNil) @@ -151,6 +196,20 @@ func getCharmInfoResponse() InfoResponse { Series: []string{"bionic", "xenial"}, Tags: []string{"app", "seven"}, Charm: &Charm{ + Config: &charm.Config{ + Options: map[string]charm.Option{ + "status": { + Type: "string", + Description: "temporary string for unit status", + Default: "hello", + }, + "thing": { + Type: "string", + Description: "A thing used by the charm.", + Default: "🎁", + }, + }, + }, Subordinate: true, Relations: map[string]map[string]string{ "provides": {