Permalink
Browse files

Merge pull request #4374 from natefinch/list-resources-debug

Implement juju resources --details flag

This adds a --details flag that does a diff of the expected resources on each unit, and the actual resources.

(Review request: http://reviews.vapour.ws/r/3818/)
  • Loading branch information...
2 parents dad26b1 + 922cf5e commit f0e1848aca595b35d6483f75a909c38b204eee5a @jujubot jujubot committed Feb 12, 2016
View
@@ -43,6 +43,17 @@ type FormattedSvcResource struct {
}
// FormattedUnitResource holds the formatted representation of a resource's info.
-type FormattedUnitResource struct {
- FormattedSvcResource
+type FormattedUnitResource FormattedSvcResource
+
+// FormattedDetailResource is the data for a single line of tabular output for
+// juju resources <service> --details.
+type FormattedDetailResource struct {
+ UnitID string `json:"unitID" yaml:"unitID"`
+ Unit FormattedSvcResource `json:"unit" yaml:"unit"`
+ Expected FormattedSvcResource `json:"expected" yaml:"expected"`
+ unitNumber int
}
+
+// FormattedDetailResource is the data for the tabular output for juju resources
+// <unit> --details.
+type FormattedUnitDetails []FormattedDetailResource
View
@@ -5,10 +5,14 @@ package cmd
import (
"fmt"
+ "strconv"
+ "strings"
charmresource "gopkg.in/juju/charm.v6-unstable/resource"
+ "github.com/juju/errors"
"github.com/juju/juju/resource"
+ "github.com/juju/names"
)
type charmResourcesFormatter struct {
@@ -73,6 +77,23 @@ func FormatSvcResource(res resource.Resource) FormattedSvcResource {
}
}
+// FormatDetailResource converts the arguments into a FormattedServiceResource.
+func FormatDetailResource(tag names.UnitTag, svc, unit resource.Resource) (FormattedDetailResource, error) {
+ // note that the unit resource can be a zero value here, to indicate that
+ // the unit has not downloaded that resource yet.
+
+ unitNum, err := unitNum(tag)
+ if err != nil {
+ return FormattedDetailResource{}, errors.Trace(err)
+ }
+ return FormattedDetailResource{
+ UnitID: tag.Id(),
+ unitNumber: unitNum,
+ Unit: FormatSvcResource(unit),
+ Expected: FormatSvcResource(svc),
+ }, nil
+}
+
func combinedRevision(r resource.Resource) string {
switch r.Origin {
case charmresource.OriginStore:
@@ -101,3 +122,15 @@ func usedYesNo(used bool) string {
}
return "no"
}
+
+func unitNum(unit names.UnitTag) (int, error) {
+ vals := strings.SplitN(unit.Id(), "/", 2)
+ if len(vals) != 2 {
+ return 0, errors.Errorf("%q is not a valid unit ID", unit.Id())
+ }
+ num, err := strconv.Atoi(vals[1])
+ if err != nil {
+ return 0, errors.Annotatef(err, "%q is not a valid unit ID", unit.Id())
+ }
+ return num, nil
+}
@@ -7,6 +7,7 @@ import (
"strings"
"time"
+ "github.com/juju/names"
"github.com/juju/testing"
jc "github.com/juju/testing/checkers"
gc "gopkg.in/check.v1"
@@ -127,3 +128,105 @@ func (s *SvcFormatterSuite) TestInitialOriginUpload(c *gc.C) {
f := FormatSvcResource(r)
c.Assert(f.combinedOrigin, gc.Equals, "upload")
}
+
+var _ = gc.Suite(&DetailFormatterSuite{})
+
+type DetailFormatterSuite struct {
+ testing.IsolationSuite
+}
+
+func (s *DetailFormatterSuite) TestFormatDetail(c *gc.C) {
+ fp, err := charmresource.GenerateFingerprint(strings.NewReader("something"))
+ c.Assert(err, jc.ErrorIsNil)
+
+ svc := resource.Resource{
+ Resource: charmresource.Resource{
+ Meta: charmresource.Meta{
+ Name: "website",
+ Description: "your website data",
+ Type: charmresource.TypeFile,
+ Path: "foobar",
+ },
+ Revision: 5,
+ Origin: charmresource.OriginStore,
+ Fingerprint: fp,
+ Size: 10,
+ },
+ Username: "Bill User",
+ Timestamp: time.Now().Add(-1 * time.Hour * 24 * 365),
+ ID: "a-service/website",
+ ServiceID: "a-service",
+ }
+
+ fp2, err := charmresource.GenerateFingerprint(strings.NewReader("other"))
+ c.Assert(err, jc.ErrorIsNil)
+
+ unit := resource.Resource{
+ Resource: charmresource.Resource{
+ Meta: charmresource.Meta{
+ Name: "website",
+ Description: "your website data",
+ Type: charmresource.TypeFile,
+ Path: "foobar",
+ },
+ Revision: 7,
+ Origin: charmresource.OriginStore,
+ Fingerprint: fp2,
+ Size: 15,
+ },
+ Username: "Bill User",
+ Timestamp: time.Now(),
+ ID: "a-service/website",
+ ServiceID: "a-service",
+ }
+ tag := names.NewUnitTag("a-service/55")
+
+ d, err := FormatDetailResource(tag, svc, unit)
+ c.Assert(err, jc.ErrorIsNil)
+ c.Assert(d, gc.Equals,
+ FormattedDetailResource{
+ unitNumber: 55,
+ UnitID: "a-service/55",
+ Expected: FormatSvcResource(svc),
+ Unit: FormatSvcResource(unit),
+ },
+ )
+}
+
+func (s *DetailFormatterSuite) TestFormatDetailEmpty(c *gc.C) {
+ fp, err := charmresource.GenerateFingerprint(strings.NewReader("something"))
+ c.Assert(err, jc.ErrorIsNil)
+
+ svc := resource.Resource{
+ Resource: charmresource.Resource{
+ Meta: charmresource.Meta{
+ Name: "website",
+ Description: "your website data",
+ Type: charmresource.TypeFile,
+ Path: "foobar",
+ },
+ Revision: 5,
+ Origin: charmresource.OriginStore,
+ Fingerprint: fp,
+ Size: 10,
+ },
+ Username: "Bill User",
+ Timestamp: time.Now().Add(-1 * time.Hour * 24 * 365),
+ ID: "a-service/website",
+ ServiceID: "a-service",
+ }
+
+ unit := resource.Resource{}
+ tag := names.NewUnitTag("a-service/55")
+
+ d, err := FormatDetailResource(tag, svc, unit)
+ c.Assert(err, jc.ErrorIsNil)
+ c.Assert(d, gc.Equals,
+ FormattedDetailResource{
+ unitNumber: 55,
+ UnitID: "a-service/55",
+ Expected: FormatSvcResource(svc),
+ Unit: FormatSvcResource(unit),
+ },
+ )
+}
@@ -29,7 +29,7 @@ type CharmResourceLister interface {
Close() error
}
-// ShowCommand implements the show-resources command.
+// ListCharmResourcesCommand implements the "juju charm list-resources" command.
type ListCharmResourcesCommand struct {
modelcmd.ModelCommandBase
CharmCommandBase
@@ -6,6 +6,7 @@ package cmd
import (
"bytes"
"fmt"
+ "sort"
"text/tabwriter"
"github.com/juju/errors"
@@ -21,6 +22,7 @@ func FormatCharmTabular(value interface{}) ([]byte, error) {
// TODO(ericsnow) sort the rows first?
var out bytes.Buffer
+
// To format things into columns.
tw := tabwriter.NewWriter(&out, 0, 1, 1, ' ', 0)
@@ -48,15 +50,22 @@ func FormatSvcTabular(value interface{}) ([]byte, error) {
return formatServiceTabular(resources), nil
case []FormattedUnitResource:
return formatUnitTabular(resources), nil
+ case []FormattedDetailResource:
+ return formatDetailTabular(resources), nil
+ case FormattedUnitDetails:
+ return formatUnitDetailTabular(resources), nil
default:
- return nil, errors.Errorf("expected value of type []FormattedSvcResource or []FormattedUnitResource, got %T", resources)
+ return nil, errors.Errorf("unexpected type for data: %T", resources)
}
}
func formatServiceTabular(resources []FormattedSvcResource) []byte {
// TODO(ericsnow) sort the rows first?
var out bytes.Buffer
+
+ fmt.Fprintln(&out, "[Service]")
+
// To format things into columns.
tw := tabwriter.NewWriter(&out, 0, 1, 1, ' ', 0)
@@ -82,6 +91,9 @@ func formatUnitTabular(resources []FormattedUnitResource) []byte {
// TODO(ericsnow) sort the rows first?
var out bytes.Buffer
+
+ fmt.Fprintln(&out, "[Unit]")
+
// To format things into columns.
tw := tabwriter.NewWriter(&out, 0, 1, 1, ' ', 0)
@@ -101,3 +113,66 @@ func formatUnitTabular(resources []FormattedUnitResource) []byte {
return out.Bytes()
}
+
+func formatDetailTabular(resources []FormattedDetailResource) []byte {
+ // note that the unit resource can be a zero value here, to indicate that
+ // the unit has not downloaded that resource yet.
+
+ var out bytes.Buffer
+ fmt.Fprintln(&out, "[Units]")
+
+ sort.Sort(byUnitID(resources))
+ // To format things into columns.
+ tw := tabwriter.NewWriter(&out, 0, 1, 1, ' ', 0)
+
+ // Write the header.
+ fmt.Fprintln(tw, "UNIT\tRESOURCE\tREVISION\tEXPECTED")
+
+ for _, r := range resources {
+ fmt.Fprintf(tw, "%v\t%v\t%v\t%v\n",
+ r.unitNumber,
+ r.Expected.Name,
+ r.Unit.combinedRevision,
+ r.Expected.combinedRevision,
+ )
+ }
+ tw.Flush()
+ return out.Bytes()
+}
+
+func formatUnitDetailTabular(resources FormattedUnitDetails) []byte {
+ // note that the unit resource can be a zero value here, to indicate that
+ // the unit has not downloaded that resource yet.
+
+ var out bytes.Buffer
+ fmt.Fprintln(&out, "[Unit]")
+
+ sort.Sort(byUnitID(resources))
+ // To format things into columns.
+ tw := tabwriter.NewWriter(&out, 0, 1, 1, ' ', 0)
+
+ // Write the header.
+ fmt.Fprintln(tw, "RESOURCE\tREVISION\tEXPECTED")
+
+ for _, r := range resources {
+ fmt.Fprintf(tw, "%v\t%v\t%v\n",
+ r.Expected.Name,
+ r.Unit.combinedRevision,
+ r.Expected.combinedRevision,
+ )
+ }
+ tw.Flush()
+ return out.Bytes()
+}
+
+type byUnitID []FormattedDetailResource
+
+func (b byUnitID) Len() int { return len(b) }
+func (b byUnitID) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
+
+func (b byUnitID) Less(i, j int) bool {
+ if b[i].unitNumber != b[j].unitNumber {
+ return b[i].unitNumber < b[j].unitNumber
+ }
+ return b[i].Expected.Name < b[j].Expected.Name
+}
Oops, something went wrong.

0 comments on commit f0e1848

Please sign in to comment.