Skip to content

Commit

Permalink
Merge pull request #16135 from hpidcock/fullstatus-with-storage
Browse files Browse the repository at this point in the history
#16135

Instead of `juju status --storage` making 3 seperate facade calls in addition to the FullStatus call, instead return the information in the FullStatus facade call.

This also implements server side filtering on unit/application for storage entities.

## QA steps

- Deploy multiple apps with storage.
- Check `juju status` without storage
- Check `juju status --storage`
- Check `juju status --storage` works against old controller (Client facade <= 6)
- Check `juju status --format yaml` (should have implicit --storage)
- Check `juju status --storage <filter>`

Example no filter:
```
juju status --storage
Model Controller Cloud/Region Version SLA Timestamp
a minikube minikube 3.2.3 unsupported 16:13:46+10:00

App Version Status Scale Charm Channel Rev Address Exposed Message
b waiting 1 postgresql-k8s 14/stable 73 10.110.113.233 no installing agent
postgresql-k8s waiting 1 postgresql-k8s 14/stable 73 10.104.229.135 no installing agent

Unit Workload Agent Address Ports Message
b/0* blocked idle 10.244.0.6 failed to create k8s resources
postgresql-k8s/0* blocked idle 10.244.0.7 failed to create k8s resources

Storage Unit Storage ID Type Pool Mountpoint Size Status Message
b/0 pgdata/1 filesystem kubernetes /var/lib/postgresql/data 1.0 GiB attached Successfully provisioned volume pvc-f08518a7-20be-4184-bc50-e668baa7fa37
postgresql-k8s/0 pgdata/0 filesystem kubernetes /var/lib/postgresql/data 1.0 GiB attached Successfully provisioned volume pvc-48b7f0ea-b103-4d66-b21e-b69d107a072d
```

Example with filter:
```
juju status --storage b
Model Controller Cloud/Region Version SLA Timestamp
a minikube minikube 3.2.3 unsupported 16:16:27+10:00

App Version Status Scale Charm Channel Rev Address Exposed Message
b 14.7 waiting 1 postgresql-k8s 14/stable 73 10.110.113.233 no installing agent

Unit Workload Agent Address Ports Message
b/0* blocked idle 10.244.0.6 failed to create k8s resources

Storage Unit Storage ID Type Pool Mountpoint Size Status Message
b/0 pgdata/1 filesystem kubernetes /var/lib/postgresql/data 1.0 GiB attached Successfully provisioned volume pvc-f08518a7-20be-4184-bc50-e668baa7fa37
```

## Documentation changes

N/A

## Bug reference

Partially mitigates https://bugs.launchpad.net/juju/+bug/2015371

JUJU-4538
  • Loading branch information
jujubot committed Oct 4, 2023
2 parents 2f516ee + 1e22612 commit e8525c0
Show file tree
Hide file tree
Showing 35 changed files with 1,110 additions and 480 deletions.
59 changes: 58 additions & 1 deletion api/client/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/juju/juju/api"
"github.com/juju/juju/api/base"
"github.com/juju/juju/api/client/storage"
"github.com/juju/juju/api/common"
"github.com/juju/juju/core/model"
"github.com/juju/juju/core/status"
Expand Down Expand Up @@ -48,8 +49,32 @@ func NewClient(c api.Connection, logger Logger) *Client {
}
}

// StatusArgs holds the options for a call to Status.
type StatusArgs struct {
// Patterns is used to filter the status response.
Patterns []string

// IncludeStorage can be set to true to return storage in the response.
IncludeStorage bool
}

// Status returns the status of the juju model.
func (c *Client) Status(patterns []string) (*params.FullStatus, error) {
func (c *Client) Status(args *StatusArgs) (*params.FullStatus, error) {
if args == nil {
args = &StatusArgs{}
}
if c.BestAPIVersion() <= 6 {
return c.statusV6(args.Patterns, args.IncludeStorage)
}
var result params.FullStatus
p := params.StatusParams{Patterns: args.Patterns, IncludeStorage: args.IncludeStorage}
if err := c.facade.FacadeCall("FullStatus", p, &result); err != nil {
return nil, err
}
return &result, nil
}

func (c *Client) statusV6(patterns []string, includeStorage bool) (*params.FullStatus, error) {
var result params.FullStatus
p := params.StatusParams{Patterns: patterns}
if err := c.facade.FacadeCall("FullStatus", p, &result); err != nil {
Expand All @@ -60,6 +85,38 @@ func (c *Client) Status(patterns []string) (*params.FullStatus, error) {
if result.Model.Type == "" {
result.Model.Type = model.IAAS.String()
}
if includeStorage {
storageClient := storage.NewClient(c.conn)
storageDetails, err := storageClient.ListStorageDetails()
if err != nil {
return nil, errors.Annotatef(err, "cannot list storage details")
}
result.Storage = storageDetails

filesystemResult, err := storageClient.ListFilesystems(nil)
if err != nil {
return nil, errors.Annotatef(err, "cannot list filesystem details")
}
if len(filesystemResult) != 1 {
return nil, errors.Errorf("cannot list filesystem details: expected one result got %d", len(filesystemResult))
}
if err := filesystemResult[0].Error; err != nil {
return nil, errors.Annotatef(err, "cannot list filesystem details")
}
result.Filesystems = filesystemResult[0].Result

volumeResult, err := storageClient.ListVolumes(nil)
if err != nil {
return nil, errors.Annotatef(err, "cannot list volume details")
}
if len(volumeResult) != 1 {
return nil, errors.Errorf("cannot list volume details: expected one result got %d", len(volumeResult))
}
if err := volumeResult[0].Error; err != nil {
return nil, errors.Annotatef(err, "cannot list volume details")
}
result.Volumes = volumeResult[0].Result
}
return &result, nil
}

Expand Down
2 changes: 1 addition & 1 deletion api/facadeversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ var facadeVersions = map[string][]int{
"CharmRevisionUpdater": {2},
"Charms": {5, 6, 7},
"Cleaner": {2},
"Client": {6},
"Client": {6, 7},
"Cloud": {7},
"Controller": {11},
"CredentialManager": {1},
Expand Down
2 changes: 1 addition & 1 deletion api/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func (s *stateSuite) TestLoginMacaroonInvalidId(c *gc.C) {
}

func (s *stateSuite) TestBestFacadeVersion(c *gc.C) {
c.Check(s.APIState.BestFacadeVersion("Client"), gc.Equals, 6)
c.Check(s.APIState.BestFacadeVersion("Client"), gc.Equals, 7)
}

func (s *stateSuite) TestAPIHostPortsMovesConnectedValueFirst(c *gc.C) {
Expand Down
8 changes: 4 additions & 4 deletions apiserver/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func (s *loginSuite) TestLoginAsDeactivatedUser(c *gc.C) {
password := "password"
u := s.Factory.MakeUser(c, &factory.UserParams{Password: password, Disabled: true})

_, err := apiclient.NewClient(st, coretesting.NoopLogger{}).Status([]string{})
_, err := apiclient.NewClient(st, coretesting.NoopLogger{}).Status(nil)
c.Assert(errors.Cause(err), gc.DeepEquals, &rpc.RequestError{
Message: `unknown object type "Client"`,
Code: "not implemented",
Expand All @@ -183,7 +183,7 @@ func (s *loginSuite) TestLoginAsDeactivatedUser(c *gc.C) {
Code: "unauthorized access",
})

_, err = apiclient.NewClient(st, coretesting.NoopLogger{}).Status([]string{})
_, err = apiclient.NewClient(st, coretesting.NoopLogger{}).Status(nil)
c.Assert(errors.Cause(err), gc.DeepEquals, &rpc.RequestError{
Message: `unknown object type "Client"`,
Code: "not implemented",
Expand All @@ -197,7 +197,7 @@ func (s *loginSuite) TestLoginAsDeletedUser(c *gc.C) {
password := "password"
u := s.Factory.MakeUser(c, &factory.UserParams{Password: password})

_, err := apiclient.NewClient(st, coretesting.NoopLogger{}).Status([]string{})
_, err := apiclient.NewClient(st, coretesting.NoopLogger{}).Status(nil)
c.Assert(errors.Cause(err), gc.DeepEquals, &rpc.RequestError{
Message: `unknown object type "Client"`,
Code: "not implemented",
Expand All @@ -213,7 +213,7 @@ func (s *loginSuite) TestLoginAsDeletedUser(c *gc.C) {
Code: "unauthorized access",
})

_, err = apiclient.NewClient(st, coretesting.NoopLogger{}).Status([]string{})
_, err = apiclient.NewClient(st, coretesting.NoopLogger{}).Status(nil)
c.Assert(errors.Cause(err), gc.DeepEquals, &rpc.RequestError{
Message: `unknown object type "Client"`,
Code: "not implemented",
Expand Down
75 changes: 75 additions & 0 deletions apiserver/common/storagecommon/filesystemdetails.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2023 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package storagecommon

import (
"github.com/juju/errors"
"github.com/juju/names/v4"

"github.com/juju/juju/apiserver/common"
"github.com/juju/juju/core/life"
"github.com/juju/juju/rpc/params"
"github.com/juju/juju/state"
)

// FilesystemDetails returns the filesystem and its attachments as a params FilesystemDetails.
func FilesystemDetails(
sb DetailsBackend,
unitToMachine UnitAssignedMachineFunc,
f state.Filesystem,
attachments []state.FilesystemAttachment,
) (*params.FilesystemDetails, error) {
details := &params.FilesystemDetails{
FilesystemTag: f.FilesystemTag().String(),
Life: life.Value(f.Life().String()),
}

if volumeTag, err := f.Volume(); err == nil {
details.VolumeTag = volumeTag.String()
}

if info, err := f.Info(); err == nil {
details.Info = FilesystemInfoFromState(info)
}

if len(attachments) > 0 {
details.MachineAttachments = make(map[string]params.FilesystemAttachmentDetails, len(attachments))
details.UnitAttachments = make(map[string]params.FilesystemAttachmentDetails, len(attachments))
for _, attachment := range attachments {
attDetails := params.FilesystemAttachmentDetails{
Life: life.Value(attachment.Life().String()),
}
if stateInfo, err := attachment.Info(); err == nil {
attDetails.FilesystemAttachmentInfo = FilesystemAttachmentInfoFromState(
stateInfo,
)
}
if attachment.Host().Kind() == names.MachineTagKind {
details.MachineAttachments[attachment.Host().String()] = attDetails
} else {
details.UnitAttachments[attachment.Host().String()] = attDetails
}
}
}

aStatus, err := f.Status()
if err != nil {
return nil, errors.Trace(err)
}
details.Status = common.EntityStatusFromState(aStatus)

if storageTag, err := f.Storage(); err == nil {
storageInstance, err := sb.StorageInstance(storageTag)
if err != nil {
return nil, errors.Trace(err)
}
storageDetails, err := StorageDetails(sb, unitToMachine, storageInstance)
if err != nil {
return nil, errors.Trace(err)
}
details.Storage = storageDetails
}

return details, nil
}
122 changes: 122 additions & 0 deletions apiserver/common/storagecommon/storagedetails.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2023 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package storagecommon

import (
"github.com/juju/errors"
"github.com/juju/names/v4"

"github.com/juju/juju/apiserver/common"
"github.com/juju/juju/core/life"
"github.com/juju/juju/core/status"
"github.com/juju/juju/rpc/params"
"github.com/juju/juju/state"
)

// DetailsBacked is used by StorageDetails, VolumeDetails and FilesystemDetails to access
// state for collecting all the required information to send back over the wire.
type DetailsBackend interface {
StorageAccess
VolumeAccess
FilesystemAccess
StorageAttachments(names.StorageTag) ([]state.StorageAttachment, error)
}

type UnitAssignedMachineFunc func(names.UnitTag) (names.MachineTag, error)

// StorageDetails returns the storage instance as a params StorageDetails.
func StorageDetails(
sb DetailsBackend,
unitToMachine UnitAssignedMachineFunc,
si state.StorageInstance,
) (*params.StorageDetails, error) {
// Get information from underlying volume or filesystem.
var persistent bool
var statusEntity status.StatusGetter
if si.Kind() == state.StorageKindFilesystem {
// TODO(axw) when we support persistent filesystems,
// e.g. CephFS, we'll need to do set "persistent"
// here too.
filesystem, err := sb.StorageInstanceFilesystem(si.StorageTag())
if err != nil {
return nil, errors.Trace(err)
}
statusEntity = filesystem
} else {
volume, err := sb.StorageInstanceVolume(si.StorageTag())
if err != nil {
return nil, errors.Trace(err)
}
if info, err := volume.Info(); err == nil {
persistent = info.Persistent
}
statusEntity = volume
}
aStatus, err := statusEntity.Status()
if err != nil {
return nil, errors.Trace(err)
}

// Get unit storage attachments.
var storageAttachmentDetails map[string]params.StorageAttachmentDetails
storageAttachments, err := sb.StorageAttachments(si.StorageTag())
if err != nil {
return nil, errors.Trace(err)
}
if len(storageAttachments) > 0 {
storageAttachmentDetails = make(map[string]params.StorageAttachmentDetails)
for _, a := range storageAttachments {
// TODO(caas) - handle attachments to units
machineTag, location, err := storageAttachmentInfo(sb, a, unitToMachine)
if err != nil {
return nil, errors.Trace(err)
}
details := params.StorageAttachmentDetails{
StorageTag: a.StorageInstance().String(),
UnitTag: a.Unit().String(),
Location: location,
Life: life.Value(a.Life().String()),
}
if machineTag.Id() != "" {
details.MachineTag = machineTag.String()
}
storageAttachmentDetails[a.Unit().String()] = details
}
}

var ownerTag string
if owner, ok := si.Owner(); ok {
ownerTag = owner.String()
}

return &params.StorageDetails{
StorageTag: si.Tag().String(),
OwnerTag: ownerTag,
Kind: params.StorageKind(si.Kind()),
Life: life.Value(si.Life().String()),
Status: common.EntityStatusFromState(aStatus),
Persistent: persistent,
Attachments: storageAttachmentDetails,
}, nil
}

func storageAttachmentInfo(
sb DetailsBackend,
a state.StorageAttachment,
unitToMachine UnitAssignedMachineFunc,
) (_ names.MachineTag, location string, _ error) {
machineTag, err := unitToMachine(a.Unit())
if errors.Is(err, errors.NotAssigned) {
return names.MachineTag{}, "", nil
} else if err != nil {
return names.MachineTag{}, "", errors.Trace(err)
}
info, err := StorageAttachmentInfo(sb, sb, sb, a, machineTag)
if errors.Is(err, errors.NotProvisioned) {
return machineTag, "", nil
} else if err != nil {
return names.MachineTag{}, "", errors.Trace(err)
}
return machineTag, info.Location, nil
}

0 comments on commit e8525c0

Please sign in to comment.