diff --git a/api/agent/uniter/application.go b/api/agent/uniter/application.go index c49b5e02a26..8110b23429e 100644 --- a/api/agent/uniter/application.go +++ b/api/agent/uniter/application.go @@ -10,7 +10,8 @@ import ( "github.com/juju/errors" "github.com/juju/names/v5" - "github.com/juju/juju/api/common" + apiwatcher "github.com/juju/juju/api/watcher" + apiservererrors "github.com/juju/juju/apiserver/errors" "github.com/juju/juju/core/life" "github.com/juju/juju/core/status" "github.com/juju/juju/core/watcher" @@ -44,7 +45,18 @@ func (s *Application) String() string { // Watch returns a watcher for observing changes to an application. func (s *Application) Watch(ctx context.Context) (watcher.NotifyWatcher, error) { - return common.Watch(ctx, s.client.facade, "Watch", s.tag) + arg := params.Entity{Tag: s.tag.String()} + var result params.NotifyWatchResult + + err := s.client.facade.FacadeCall(ctx, "WatchApplication", arg, &result) + if err != nil { + return nil, errors.Trace(apiservererrors.RestoreError(err)) + } + + if result.Error != nil { + return nil, result.Error + } + return apiwatcher.NewNotifyWatcher(s.client.facade.RawAPICaller(), result), nil } // Life returns the application's current life state. diff --git a/api/agent/uniter/application_test.go b/api/agent/uniter/application_test.go index 3e24ebd55ad..1582e2410a2 100644 --- a/api/agent/uniter/application_test.go +++ b/api/agent/uniter/application_test.go @@ -53,13 +53,11 @@ func (s *applicationSuite) apiCallerFunc(c *gc.C) basetesting.APICallerFunc { Life: s.life, }}, } - case "Watch": - c.Assert(arg, jc.DeepEquals, params.Entities{Entities: []params.Entity{{Tag: "application-mysql"}}}) - c.Assert(result, gc.FitsTypeOf, ¶ms.NotifyWatchResults{}) - *(result.(*params.NotifyWatchResults)) = params.NotifyWatchResults{ - Results: []params.NotifyWatchResult{{ - NotifyWatcherId: "1", - }}, + case "WatchApplication": + c.Assert(arg, jc.DeepEquals, params.Entity{Tag: "application-mysql"}) + c.Assert(result, gc.FitsTypeOf, ¶ms.NotifyWatchResult{}) + *(result.(*params.NotifyWatchResult)) = params.NotifyWatchResult{ + NotifyWatcherId: "1", } case "CharmURL": c.Assert(arg, jc.DeepEquals, params.Entities{Entities: []params.Entity{{Tag: "application-mysql"}}}) diff --git a/api/agent/uniter/unit.go b/api/agent/uniter/unit.go index b081d6188c1..dd451a4b848 100644 --- a/api/agent/uniter/unit.go +++ b/api/agent/uniter/unit.go @@ -158,9 +158,20 @@ func (u *Unit) EnsureDead(ctx context.Context) error { return result.OneError() } -// Watch returns a watcher for observing changes to the unit. -func (u *Unit) Watch(ctx context.Context) (watcher.NotifyWatcher, error) { - return common.Watch(ctx, u.client.facade, "Watch", u.tag) +// Watch returns a watcher for observing changes to an application. +func (s *Unit) Watch(ctx context.Context) (watcher.NotifyWatcher, error) { + arg := params.Entity{Tag: s.tag.String()} + var result params.NotifyWatchResult + + err := s.client.facade.FacadeCall(ctx, "WatchUnit", arg, &result) + if err != nil { + return nil, errors.Trace(apiservererrors.RestoreError(err)) + } + + if result.Error != nil { + return nil, result.Error + } + return apiwatcher.NewNotifyWatcher(s.client.facade.RawAPICaller(), result), nil } // WatchRelations returns a StringsWatcher that notifies of changes to diff --git a/api/agent/uniter/unit_test.go b/api/agent/uniter/unit_test.go index ce794307003..5dc067948fa 100644 --- a/api/agent/uniter/unit_test.go +++ b/api/agent/uniter/unit_test.go @@ -344,13 +344,11 @@ func (s *unitSuite) TestWatch(c *gc.C) { return nil } c.Assert(objType, gc.Equals, "Uniter") - c.Assert(request, gc.Equals, "Watch") - c.Assert(arg, gc.DeepEquals, params.Entities{Entities: []params.Entity{{Tag: "unit-mysql-0"}}}) - c.Assert(result, gc.FitsTypeOf, ¶ms.NotifyWatchResults{}) - *(result.(*params.NotifyWatchResults)) = params.NotifyWatchResults{ - Results: []params.NotifyWatchResult{{ - NotifyWatcherId: "1", - }}, + c.Assert(request, gc.Equals, "WatchUnit") + c.Assert(arg, gc.DeepEquals, params.Entity{Tag: "unit-mysql-0"}) + c.Assert(result, gc.FitsTypeOf, ¶ms.NotifyWatchResult{}) + *(result.(*params.NotifyWatchResult)) = params.NotifyWatchResult{ + NotifyWatcherId: "1", } return nil }) diff --git a/api/facadeversions.go b/api/facadeversions.go index 2e54e47aeb4..56d4e209274 100644 --- a/api/facadeversions.go +++ b/api/facadeversions.go @@ -118,7 +118,7 @@ var facadeVersions = facades.FacadeVersions{ "Subnets": {5}, "Undertaker": {1}, "UnitAssigner": {1}, - "Uniter": {19}, + "Uniter": {19, 20}, "Upgrader": {1}, "UpgradeSteps": {3}, "UserManager": {3}, diff --git a/apiserver/facades/agent-schema.json b/apiserver/facades/agent-schema.json index 9ea903c1318..8ea7123d7d8 100644 --- a/apiserver/facades/agent-schema.json +++ b/apiserver/facades/agent-schema.json @@ -10747,7 +10747,7 @@ { "Name": "Uniter", "Description": "", - "Version": 19, + "Version": 20, "AvailableTo": [ "controller-machine-agent", "machine-agent", @@ -11448,33 +11448,33 @@ } } }, - "Watch": { + "WatchAPIHostPorts": { "type": "object", "properties": { - "Params": { - "$ref": "#/definitions/Entities" - }, "Result": { - "$ref": "#/definitions/NotifyWatchResults" + "$ref": "#/definitions/NotifyWatchResult" } } }, - "WatchAPIHostPorts": { + "WatchActionNotifications": { "type": "object", "properties": { + "Params": { + "$ref": "#/definitions/Entities" + }, "Result": { - "$ref": "#/definitions/NotifyWatchResult" + "$ref": "#/definitions/StringsWatchResults" } } }, - "WatchActionNotifications": { + "WatchApplication": { "type": "object", "properties": { "Params": { - "$ref": "#/definitions/Entities" + "$ref": "#/definitions/Entity" }, "Result": { - "$ref": "#/definitions/StringsWatchResults" + "$ref": "#/definitions/NotifyWatchResult" } } }, @@ -11552,6 +11552,17 @@ } } }, + "WatchUnit": { + "type": "object", + "properties": { + "Params": { + "$ref": "#/definitions/Entity" + }, + "Result": { + "$ref": "#/definitions/NotifyWatchResult" + } + } + }, "WatchUnitAddressesHash": { "type": "object", "properties": { diff --git a/apiserver/facades/agent/uniter/export_test.go b/apiserver/facades/agent/uniter/export_test.go index 69a3d0b1bbf..6bf20a559ef 100644 --- a/apiserver/facades/agent/uniter/export_test.go +++ b/apiserver/facades/agent/uniter/export_test.go @@ -21,6 +21,7 @@ var ( WatchStorageAttachment = watchStorageAttachment NewUniterAPI = newUniterAPI + NewUniterAPIv19 = newUniterAPIv19 NewUniterAPIWithServices = newUniterAPIWithServices ) diff --git a/apiserver/facades/agent/uniter/package_test.go b/apiserver/facades/agent/uniter/package_test.go index 05d0fefce1d..92d98147464 100644 --- a/apiserver/facades/agent/uniter/package_test.go +++ b/apiserver/facades/agent/uniter/package_test.go @@ -174,6 +174,16 @@ func (s *uniterSuiteBase) newUniterAPI(c *gc.C, st *state.State, auth facade.Aut return uniterAPI } +func (s *uniterSuiteBase) newUniterAPIv19(c *gc.C, st *state.State, auth facade.Authorizer) *uniter.UniterAPIv19 { + facadeContext := s.facadeContext(c) + facadeContext.State_ = st + facadeContext.Auth_ = auth + facadeContext.LeadershipRevoker_ = s.leadershipRevoker + uniterAPI, err := uniter.NewUniterAPIv19(context.Background(), facadeContext) + c.Assert(err, jc.ErrorIsNil) + return uniterAPI +} + func (s *uniterSuiteBase) addRelation(c *gc.C, first, second string) *state.Relation { st := s.ControllerModel(c).State() eps, err := st.InferEndpoints(first, second) diff --git a/apiserver/facades/agent/uniter/register.go b/apiserver/facades/agent/uniter/register.go index ee671b555c4..5c582d03437 100644 --- a/apiserver/facades/agent/uniter/register.go +++ b/apiserver/facades/agent/uniter/register.go @@ -22,10 +22,21 @@ import ( // Register is called to expose a package of facades onto a given registry. func Register(registry facade.FacadeRegistry) { registry.MustRegister("Uniter", 19, func(stdCtx context.Context, ctx facade.ModelContext) (facade.Facade, error) { + return newUniterAPIv19(stdCtx, ctx) + }, reflect.TypeOf((*UniterAPIv19)(nil))) + registry.MustRegister("Uniter", 20, func(stdCtx context.Context, ctx facade.ModelContext) (facade.Facade, error) { return newUniterAPI(stdCtx, ctx) }, reflect.TypeOf((*UniterAPI)(nil))) } +func newUniterAPIv19(stdCtx context.Context, ctx facade.ModelContext) (*UniterAPIv19, error) { + api, err := newUniterAPI(stdCtx, ctx) + if err != nil { + return nil, err + } + return &UniterAPIv19{UniterAPI: api}, nil +} + // newUniterAPI creates a new instance of the core Uniter API. func newUniterAPI(stdCtx context.Context, ctx facade.ModelContext) (*UniterAPI, error) { domainServices := ctx.DomainServices() @@ -131,7 +142,6 @@ func newUniterAPIWithServices( } logger := context.Logger().Child("uniter") return &UniterAPI{ - AgentEntityWatcher: common.NewAgentEntityWatcher(st, watcherRegistry, accessUnitOrApplication), APIAddresser: common.NewAPIAddresser(systemState, resources), ModelConfigWatcher: common.NewModelConfigWatcher(modelConfigService, context.WatcherRegistry()), RebootRequester: common.NewRebootRequester(machineService, accessMachine), @@ -169,5 +179,6 @@ func newUniterAPIWithServices( StorageAPI: storageAPI, logger: logger, store: context.ObjectStore(), + watcherRegistry: watcherRegistry, }, nil } diff --git a/apiserver/facades/agent/uniter/service.go b/apiserver/facades/agent/uniter/service.go index c06f4e94209..03857e0f4df 100644 --- a/apiserver/facades/agent/uniter/service.go +++ b/apiserver/facades/agent/uniter/service.go @@ -8,6 +8,7 @@ import ( "github.com/juju/juju/cloud" "github.com/juju/juju/controller" + coreapplication "github.com/juju/juju/core/application" "github.com/juju/juju/core/credential" "github.com/juju/juju/core/leadership" "github.com/juju/juju/core/life" @@ -73,6 +74,25 @@ type ApplicationService interface { // DestroyUnit prepares a unit for removal from the model. DestroyUnit(ctx context.Context, unitName coreunit.Name) error + + // WatchApplication returns a NotifyWatcher for changes to the application. + WatchApplication(ctx context.Context, name string) (watcher.NotifyWatcher, error) + + // GetApplicationIDByUnitName returns the application ID for the named unit. + // + // Returns [github.com/juju/juju/domain/application.UnitNotFound] if the + // unit is not found. + GetApplicationIDByUnitName(ctx context.Context, unitName coreunit.Name) (coreapplication.ID, error) + + // GetApplicationIDByName returns an application ID by application name. + // + // Returns [github.com/juju/juju/domain/application.ApplicationNotFound] if + // the application is not found. + GetApplicationIDByName(ctx context.Context, name string) (coreapplication.ID, error) + + // GetCharmModifiedVersion looks up the charm modified version of the given + // application. + GetCharmModifiedVersion(ctx context.Context, id coreapplication.ID) (int, error) } // UnitStateService describes the ability to retrieve and persist diff --git a/apiserver/facades/agent/uniter/uniter.go b/apiserver/facades/agent/uniter/uniter.go index 854e63e1976..39aef201b7a 100644 --- a/apiserver/facades/agent/uniter/uniter.go +++ b/apiserver/facades/agent/uniter/uniter.go @@ -21,7 +21,9 @@ import ( apiservererrors "github.com/juju/juju/apiserver/errors" "github.com/juju/juju/apiserver/facade" leadershipapiserver "github.com/juju/juju/apiserver/facades/agent/leadership" + "github.com/juju/juju/apiserver/internal" "github.com/juju/juju/caas" + "github.com/juju/juju/core/application" "github.com/juju/juju/core/leadership" "github.com/juju/juju/core/life" corelogger "github.com/juju/juju/core/logger" @@ -30,6 +32,7 @@ import ( "github.com/juju/juju/core/objectstore" "github.com/juju/juju/core/status" coreunit "github.com/juju/juju/core/unit" + corewatcher "github.com/juju/juju/core/watcher" applicationerrors "github.com/juju/juju/domain/application/errors" machineerrors "github.com/juju/juju/domain/machine/errors" "github.com/juju/juju/domain/unitstate" @@ -46,7 +49,6 @@ type UniterAPI struct { *StatusAPI *StorageAPI - *common.AgentEntityWatcher *common.APIAddresser *common.ModelConfigWatcher *common.RebootRequester @@ -66,6 +68,7 @@ type UniterAPI struct { accessUnitOrApplication common.GetAuthFunc accessMachine common.GetAuthFunc containerBrokerFunc caas.NewContainerBrokerFunc + watcherRegistry facade.WatcherRegistry cloudService CloudService credentialService CredentialService @@ -89,6 +92,10 @@ type UniterAPI struct { logger corelogger.Logger } +type UniterAPIv19 struct { + *UniterAPI +} + // EnsureDead calls EnsureDead on each given unit from state. // If it's Alive, nothing will happen. func (u *UniterAPI) EnsureDead(ctx context.Context, args params.Entities) (params.ErrorResults, error) { @@ -617,7 +624,7 @@ func (u *UniterAPI) CharmModifiedVersion(ctx context.Context, args params.Entiti return results, err } for i, entity := range args.Entities { - ver, err := u.charmModifiedVersion(entity.Tag, canAccess) + ver, err := u.charmModifiedVersion(ctx, entity.Tag, canAccess) if err != nil { results.Results[i].Error = apiservererrors.ServerError(err) continue @@ -627,7 +634,11 @@ func (u *UniterAPI) CharmModifiedVersion(ctx context.Context, args params.Entiti return results, nil } -func (u *UniterAPI) charmModifiedVersion(tagStr string, canAccess func(names.Tag) bool) (int, error) { +func (u *UniterAPI) charmModifiedVersion( + ctx context.Context, + tagStr string, + canAccess func(names.Tag) bool, +) (int, error) { tag, err := names.ParseTag(tagStr) if err != nil { return -1, apiservererrors.ErrPerm @@ -635,23 +646,37 @@ func (u *UniterAPI) charmModifiedVersion(tagStr string, canAccess func(names.Tag if !canAccess(tag) { return -1, apiservererrors.ErrPerm } - unitOrApplication, err := u.st.FindEntity(tag) - if err != nil { - return -1, err - } - var application *state.Application - switch entity := unitOrApplication.(type) { - case *state.Application: - application = entity - case *state.Unit: - application, err = entity.Application() + + var id application.ID + switch tag.(type) { + case names.ApplicationTag: + id, err = u.applicationService.GetApplicationIDByName(ctx, tag.Id()) + if errors.Is(err, applicationerrors.ApplicationNotFound) { + // Return an error that also matches a generic not found error. + return -1, internalerrors.Join(err, errors.Hide(errors.NotFound)) + } else if err != nil { + return -1, err + } + case names.UnitTag: + name, err := coreunit.NewName(tag.Id()) if err != nil { return -1, err } + id, err = u.applicationService.GetApplicationIDByUnitName(ctx, name) + if errors.Is(err, applicationerrors.UnitNotFound) { + // Return an error that also matches a generic not found error. + return -1, internalerrors.Join(err, errors.Hide(errors.NotFound)) + } else if err != nil { + return -1, err + } default: - return -1, errors.BadRequestf("type %T does not have a CharmModifiedVersion", entity) + return -1, errors.BadRequestf("type %s does not have a CharmModifiedVersion", tag.Kind()) + } + charmModifiedVersion, err := u.applicationService.GetCharmModifiedVersion(ctx, id) + if err != nil { + return -1, err } - return application.CharmModifiedVersion(), nil + return charmModifiedVersion, nil } // CharmURL returns the charm URL for all given units or applications. @@ -2835,3 +2860,120 @@ func (u *UniterAPI) APIAddresses(ctx context.Context) (result params.StringsResu return u.APIAddresser.APIAddresses(ctx, controllerConfig) } + +// WatchApplication starts an NotifyWatcher for an application. +// WatchApplication is not implemented in the UniterAPIv19 facade. +func (u *UniterAPI) WatchApplication(ctx context.Context, entity params.Entity) (params.NotifyWatchResult, error) { + canWatch, err := u.accessApplication() + if err != nil { + return params.NotifyWatchResult{}, errors.Trace(err) + } + + tag, err := names.ParseApplicationTag(entity.Tag) + if err != nil { + return params.NotifyWatchResult{Error: apiservererrors.ServerError(apiservererrors.ErrPerm)}, nil + } + + if !canWatch(tag) { + return params.NotifyWatchResult{Error: apiservererrors.ServerError(apiservererrors.ErrPerm)}, nil + } + + watcher, err := u.applicationService.WatchApplication(ctx, tag.Id()) + if err != nil { + return params.NotifyWatchResult{Error: apiservererrors.ServerError(err)}, nil + } + + id, _, err := internal.EnsureRegisterWatcher[struct{}](ctx, u.watcherRegistry, watcher) + return params.NotifyWatchResult{ + NotifyWatcherId: id, + Error: apiservererrors.ServerError(err), + }, nil +} + +// WatchUnit starts an NotifyWatcher for a unit. +// WatchUnit is not implemented in the UniterAPIv19 facade. +func (u *UniterAPI) WatchUnit(ctx context.Context, entity params.Entity) (params.NotifyWatchResult, error) { + canWatch, err := u.accessUnit() + if err != nil { + return params.NotifyWatchResult{}, errors.Trace(err) + } + + tag, err := names.ParseUnitTag(entity.Tag) + if err != nil { + return params.NotifyWatchResult{Error: apiservererrors.ServerError(apiservererrors.ErrPerm)}, nil + } + + if !canWatch(tag) { + return params.NotifyWatchResult{Error: apiservererrors.ServerError(apiservererrors.ErrPerm)}, nil + } + + watcher, err := u.watchUnit(tag) + if err != nil { + return params.NotifyWatchResult{Error: apiservererrors.ServerError(err)}, nil + } + + id, _, err := internal.EnsureRegisterWatcher[struct{}](ctx, u.watcherRegistry, watcher) + return params.NotifyWatchResult{ + NotifyWatcherId: id, + Error: apiservererrors.ServerError(err), + }, nil +} + +// Watch starts an NotifyWatcher for a unit or application. +// This is being deprecated in favour of separate WatchUnit and WatchApplication +// methods. +func (u *UniterAPIv19) Watch(ctx context.Context, args params.Entities) (params.NotifyWatchResults, error) { + result := params.NotifyWatchResults{ + Results: make([]params.NotifyWatchResult, len(args.Entities)), + } + if len(args.Entities) == 0 { + return result, nil + } + canWatch, err := u.accessUnitOrApplication() + if err != nil { + return params.NotifyWatchResults{}, errors.Trace(err) + } + for i, entity := range args.Entities { + tag, err := names.ParseTag(entity.Tag) + if err != nil { + result.Results[i].Error = apiservererrors.ServerError(apiservererrors.ErrPerm) + continue + } + + if !canWatch(tag) { + result.Results[i].Error = apiservererrors.ServerError(apiservererrors.ErrPerm) + continue + } + + var watcher corewatcher.NotifyWatcher + switch tag.(type) { + case names.ApplicationTag: + watcher, err = u.applicationService.WatchApplication(ctx, tag.Id()) + default: + watcher, err = u.watchUnit(tag) + } + if err != nil { + result.Results[i].Error = apiservererrors.ServerError(err) + continue + } + + id, _, err := internal.EnsureRegisterWatcher[struct{}](ctx, u.watcherRegistry, watcher) + result.Results[i].NotifyWatcherId = id + result.Results[i].Error = apiservererrors.ServerError(err) + } + return result, nil +} + +// watchUnit returns a state notify watcher for the given unit. +func (u *UniterAPI) watchUnit(tag names.Tag) (corewatcher.NotifyWatcher, error) { + entity0, err := u.st.FindEntity(tag) + if err != nil { + return nil, err + } + entity, ok := entity0.(state.NotifyWatcherFactory) + if !ok { + return nil, apiservererrors.NotSupportedError(tag, "watching") + } + watcher := entity.Watch() + return watcher, err +} diff --git a/apiserver/facades/agent/uniter/uniter_test.go b/apiserver/facades/agent/uniter/uniter_test.go index 237505b470f..71335eb990d 100644 --- a/apiserver/facades/agent/uniter/uniter_test.go +++ b/apiserver/facades/agent/uniter/uniter_test.go @@ -352,7 +352,7 @@ func (s *uniterSuite) TestEnsureDead(c *gc.C) { func (s *uniterSuite) TestWatch(c *gc.C) { defer s.setUpMocks(c).Finish() // Recreate the uniter API with the mocks initialized. - s.uniter = s.newUniterAPI(c, s.ControllerModel(c).State(), s.authorizer) + uniterAPI := s.newUniterAPIv19(c, s.ControllerModel(c).State(), s.authorizer) args := params.Entities{Entities: []params.Entity{ {Tag: "unit-mysql-0"}, {Tag: "unit-wordpress-0"}, @@ -360,14 +360,10 @@ func (s *uniterSuite) TestWatch(c *gc.C) { {Tag: "application-mysql"}, {Tag: "application-wordpress"}, {Tag: "application-foo"}, - // TODO(dfc) these aren't valid tags any more - // but I hope to restore this test when params.Entity takes - // tags, not strings, which is coming soon. - // {Tag: "just-foo"}, }} s.watcherRegistry.EXPECT().Register(gomock.Any()).Return("1", nil) s.watcherRegistry.EXPECT().Register(gomock.Any()).Return("2", nil) - result, err := s.uniter.Watch(context.Background(), args) + result, err := uniterAPI.Watch(context.Background(), args) c.Assert(err, jc.ErrorIsNil) c.Assert(result, gc.DeepEquals, params.NotifyWatchResults{ Results: []params.NotifyWatchResult{ @@ -377,12 +373,87 @@ func (s *uniterSuite) TestWatch(c *gc.C) { {Error: apiservertesting.ErrUnauthorized}, {NotifyWatcherId: "2"}, {Error: apiservertesting.ErrUnauthorized}, - // see above - // {Error: apiservertesting.ErrUnauthorized}, }, }) } +func (s *uniterSuite) TestWatchNoArgsNoError(c *gc.C) { + uniterAPI := s.newUniterAPIv19(c, s.ControllerModel(c).State(), s.authorizer) + result, err := uniterAPI.Watch(context.Background(), params.Entities{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result.Results, gc.HasLen, 0) +} + +func (s *uniterSuite) TestApplicationWatch(c *gc.C) { + defer s.setUpMocks(c).Finish() + // Recreate the uniter API with the mocks initialized. + uniterAPI := s.newUniterAPI(c, s.ControllerModel(c).State(), s.authorizer) + args := params.Entity{Tag: "application-wordpress"} + s.watcherRegistry.EXPECT().Register(gomock.Any()).Return("1", nil) + result, err := uniterAPI.WatchApplication(context.Background(), args) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, gc.DeepEquals, params.NotifyWatchResult{ + NotifyWatcherId: "1", + }) +} + +func (s *uniterSuite) TestWatchApplicationBadTag(c *gc.C) { + uniterAPI := s.newUniterAPI(c, s.ControllerModel(c).State(), s.authorizer) + result, err := uniterAPI.WatchApplication(context.Background(), params.Entity{Tag: "bad-tag"}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.NotifyWatchResult{Error: ¶ms.Error{ + Code: params.CodeUnauthorized, + Message: "permission denied", + }}) +} + +func (s *uniterSuite) TestWatchApplicationNoPermission(c *gc.C) { + uniterAPI := s.newUniterAPI(c, s.ControllerModel(c).State(), s.authorizer) + // Permissions for mysql will be denied by the accessApplication function + // defined in test set up. + result, err := uniterAPI.WatchApplication(context.Background(), params.Entity{Tag: "application-mysql"}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.NotifyWatchResult{Error: ¶ms.Error{ + Code: params.CodeUnauthorized, + Message: "permission denied", + }}) +} + +func (s *uniterSuite) TestUnitWatch(c *gc.C) { + defer s.setUpMocks(c).Finish() + // Recreate the uniter API with the mocks initialized. + uniterAPI := s.newUniterAPI(c, s.ControllerModel(c).State(), s.authorizer) + args := params.Entity{Tag: "unit-wordpress-0"} + s.watcherRegistry.EXPECT().Register(gomock.Any()).Return("1", nil) + result, err := uniterAPI.WatchUnit(context.Background(), args) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, gc.DeepEquals, params.NotifyWatchResult{ + NotifyWatcherId: "1", + }) +} + +func (s *uniterSuite) TestWatchUnitBadTag(c *gc.C) { + uniterAPI := s.newUniterAPI(c, s.ControllerModel(c).State(), s.authorizer) + result, err := uniterAPI.WatchUnit(context.Background(), params.Entity{Tag: "bad-tag"}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.NotifyWatchResult{Error: ¶ms.Error{ + Code: params.CodeUnauthorized, + Message: "permission denied", + }}) +} + +func (s *uniterSuite) TestWatchUnitNoPermission(c *gc.C) { + uniterAPI := s.newUniterAPI(c, s.ControllerModel(c).State(), s.authorizer) + // Permissions for mysql will be denied by the accessUnit function + // defined in test set up. + result, err := uniterAPI.WatchUnit(context.Background(), params.Entity{Tag: "unit-mysql-0"}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, params.NotifyWatchResult{Error: ¶ms.Error{ + Code: params.CodeUnauthorized, + Message: "permission denied", + }}) +} + func (s *uniterSuite) TestPublicAddress(c *gc.C) { // Try first without setting an address. args := params.Entities{Entities: []params.Entity{ diff --git a/domain/application/service/application.go b/domain/application/service/application.go index 637f723c8a5..ac6f0632bc9 100644 --- a/domain/application/service/application.go +++ b/domain/application/service/application.go @@ -943,7 +943,7 @@ func (s *Service) UpdateApplicationCharm(ctx context.Context, name string, param return nil } -// GetApplicationIDByName returns a application ID by application name. It +// GetApplicationIDByName returns an application ID by application name. It // returns an error if the application can not be found by the name. // // Returns [applicationerrors.ApplicationNameNotValid] if the name is not valid, diff --git a/internal/worker/uniter/relation/resolver_test.go b/internal/worker/uniter/relation/resolver_test.go index 1a37e8de0cd..bccd8aae5fd 100644 --- a/internal/worker/uniter/relation/resolver_test.go +++ b/internal/worker/uniter/relation/resolver_test.go @@ -168,7 +168,8 @@ func (s *relationResolverSuite) assertNewRelationsWithExistingRelations(c *gc.C, s.leadershipContext = &stubLeadershipContext{isLeader: isLeader} var numCalls int32 - unitEntity := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}} + unitEntitySingleton := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}} + unitEntity := params.Entity{Tag: "unit-wordpress-0"} relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{ {Relation: "relation-wordpress.db#mysql.db", Unit: "unit-wordpress-0"}, }} @@ -198,14 +199,14 @@ func (s *relationResolverSuite) assertNewRelationsWithExistingRelations(c *gc.C, unitStateResults := params.UnitStateResults{Results: []params.UnitStateResult{{}}} apiCalls := []apiCall{ - uniterAPICall("Refresh", unitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: life.Alive, Resolved: params.ResolvedNone}}}, nil), - uniterAPICall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil), - uniterAPICall("State", unitEntity, unitStateResults, nil), - uniterAPICall("RelationsStatus", unitEntity, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{ + uniterAPICall("Refresh", unitEntitySingleton, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: life.Alive, Resolved: params.ResolvedNone}}}, nil), + uniterAPICall("GetPrincipal", unitEntitySingleton, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil), + uniterAPICall("State", unitEntitySingleton, unitStateResults, nil), + uniterAPICall("RelationsStatus", unitEntitySingleton, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{ {RelationResults: []params.RelationUnitStatus{{RelationTag: "relation-wordpress:db mysql:db", InScope: true}}}}}, nil), uniterAPICall("Relation", relationUnits, relationResults, nil), uniterAPICall("Relation", relationUnits, relationResults, nil), - uniterAPICall("Watch", unitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil), + uniterAPICall("WatchUnit", unitEntity, params.NotifyWatchResult{NotifyWatcherId: "1"}, nil), uniterAPICall("SetState", unitSetStateArgs, noErrorResult, nil), uniterAPICall("EnterScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil), } @@ -278,7 +279,8 @@ func relationJoinedAPICalls() []apiCall { } func relationJoinedAPICalls2SetState() []apiCall { - unitEntity := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}} + unitEntitySingleton := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}} + unitEntity := params.Entity{Tag: "unit-wordpress-0"} relationResults := params.RelationResults{ Results: []params.RelationResult{ { @@ -314,15 +316,15 @@ func relationJoinedAPICalls2SetState() []apiCall { unitStateResults := params.UnitStateResults{Results: []params.UnitStateResult{{}}} apiCalls := []apiCall{ - uniterAPICall("Refresh", unitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: life.Alive, Resolved: params.ResolvedNone}}}, nil), - uniterAPICall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil), - uniterAPICall("State", unitEntity, unitStateResults, nil), - uniterAPICall("RelationsStatus", unitEntity, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{{RelationResults: []params.RelationUnitStatus{}}}}, nil), + uniterAPICall("Refresh", unitEntitySingleton, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: life.Alive, Resolved: params.ResolvedNone}}}, nil), + uniterAPICall("GetPrincipal", unitEntitySingleton, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil), + uniterAPICall("State", unitEntitySingleton, unitStateResults, nil), + uniterAPICall("RelationsStatus", unitEntitySingleton, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{{RelationResults: []params.RelationUnitStatus{}}}}, nil), uniterAPICall("RelationById", params.RelationIds{RelationIds: []int{1}}, relationResults, nil), uniterAPICall("Relation", relationUnits, relationResults, nil), - //uniterAPICall("State", unitEntity, unitStateResults, nil), + //uniterAPICall("State", unitEntitySingleton, unitStateResults, nil), uniterAPICall("Relation", relationUnits, relationResults, nil), - uniterAPICall("Watch", unitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil), + uniterAPICall("WatchUnit", unitEntity, params.NotifyWatchResult{NotifyWatcherId: "1"}, nil), uniterAPICall("SetState", unitSetStateArgs, noErrorResult, nil), uniterAPICall("EnterScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil), uniterAPICall("SetRelationStatus", relationStatus, noErrorResult, nil), @@ -777,7 +779,9 @@ func (s *relationResolverSuite) TestCommitHook(c *gc.C) { func (s *relationResolverSuite) TestImplicitRelationNoHooks(c *gc.C) { unitTag := names.NewUnitTag("wordpress/0") - unitEntity := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}} + unitEntitySingleton := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}} + unitEntity := params.Entity{Tag: "unit-wordpress-0"} + relationResults := params.RelationResults{ Results: []params.RelationResult{ { @@ -808,14 +812,14 @@ func (s *relationResolverSuite) TestImplicitRelationNoHooks(c *gc.C) { unitStateResults := params.UnitStateResults{Results: []params.UnitStateResult{{}}} apiCalls := []apiCall{ - uniterAPICall("Refresh", unitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: life.Alive, Resolved: params.ResolvedNone}}}, nil), - uniterAPICall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil), - uniterAPICall("State", unitEntity, unitStateResults, nil), - uniterAPICall("RelationsStatus", unitEntity, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{{RelationResults: []params.RelationUnitStatus{}}}}, nil), + uniterAPICall("Refresh", unitEntitySingleton, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: life.Alive, Resolved: params.ResolvedNone}}}, nil), + uniterAPICall("GetPrincipal", unitEntitySingleton, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil), + uniterAPICall("State", unitEntitySingleton, unitStateResults, nil), + uniterAPICall("RelationsStatus", unitEntitySingleton, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{{RelationResults: []params.RelationUnitStatus{}}}}, nil), uniterAPICall("RelationById", params.RelationIds{RelationIds: []int{1}}, relationResults, nil), uniterAPICall("Relation", relationUnits, relationResults, nil), uniterAPICall("Relation", relationUnits, relationResults, nil), - uniterAPICall("Watch", unitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil), + uniterAPICall("WatchUnit", unitEntity, params.NotifyWatchResult{NotifyWatcherId: "1"}, nil), uniterAPICall("SetState", unitSetStateArgs, noErrorResult, nil), uniterAPICall("EnterScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil), uniterAPICall("SetRelationStatus", relationStatus, noErrorResult, nil), @@ -846,9 +850,10 @@ func (s *relationResolverSuite) TestImplicitRelationNoHooks(c *gc.C) { } var ( - noErrorResult = params.ErrorResults{Results: []params.ErrorResult{{}}} - nrpeUnitTag = names.NewUnitTag("nrpe/0") - nrpeUnitEntity = params.Entities{Entities: []params.Entity{{Tag: nrpeUnitTag.String()}}} + noErrorResult = params.ErrorResults{Results: []params.ErrorResult{{}}} + nrpeUnitTag = names.NewUnitTag("nrpe/0") + nrpeUnitEntitySingleton = params.Entities{Entities: []params.Entity{{Tag: nrpeUnitTag.String()}}} + nrpeUnitEntity = params.Entity{Tag: nrpeUnitTag.String()} ) func subSubRelationAPICalls() []apiCall { @@ -927,19 +932,19 @@ func subSubRelationAPICalls() []apiCall { unitStateResults := params.UnitStateResults{Results: []params.UnitStateResult{{}}} return []apiCall{ - uniterAPICall("Refresh", nrpeUnitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: life.Alive, Resolved: params.ResolvedNone}}}, nil), - uniterAPICall("GetPrincipal", nrpeUnitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "unit-wordpress-0", Ok: true}}}, nil), - uniterAPICall("State", nrpeUnitEntity, unitStateResults, nil), - uniterAPICall("RelationsStatus", nrpeUnitEntity, relationStatusResults, nil), + uniterAPICall("Refresh", nrpeUnitEntitySingleton, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: life.Alive, Resolved: params.ResolvedNone}}}, nil), + uniterAPICall("GetPrincipal", nrpeUnitEntitySingleton, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "unit-wordpress-0", Ok: true}}}, nil), + uniterAPICall("State", nrpeUnitEntitySingleton, unitStateResults, nil), + uniterAPICall("RelationsStatus", nrpeUnitEntitySingleton, relationStatusResults, nil), uniterAPICall("Relation", relationUnits1, relationResults1, nil), uniterAPICall("Relation", relationUnits2, relationResults2, nil), uniterAPICall("Relation", relationUnits1, relationResults1, nil), - uniterAPICall("Watch", nrpeUnitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil), + uniterAPICall("WatchUnit", nrpeUnitEntity, params.NotifyWatchResult{NotifyWatcherId: "1"}, nil), uniterAPICall("SetState", unitSetStateArgs1, noErrorResult, nil), uniterAPICall("EnterScope", relationUnits1, noErrorResult, nil), uniterAPICall("SetRelationStatus", relationStatus1, noErrorResult, nil), uniterAPICall("Relation", relationUnits2, relationResults2, nil), - uniterAPICall("Watch", nrpeUnitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "2"}}}, nil), + uniterAPICall("WatchUnit", nrpeUnitEntity, params.NotifyWatchResult{NotifyWatcherId: "2"}, nil), uniterAPICall("SetState", unitSetStateArgs2, noErrorResult, nil), uniterAPICall("EnterScope", relationUnits2, noErrorResult, nil), uniterAPICall("SetRelationStatus", relationStatus2, noErrorResult, nil), @@ -956,11 +961,11 @@ func (s *relationResolverSuite) TestSubSubPrincipalRelationDyingDestroysUnit(c * // This should only be called once the relation to the // principal app is destroyed. - apiCalls = append(apiCalls, uniterAPICall("Destroy", nrpeUnitEntity, noErrorResult, nil)) + apiCalls = append(apiCalls, uniterAPICall("Destroy", nrpeUnitEntitySingleton, noErrorResult, nil)) //unitStateResults := params.UnitStateResults{Results: []params.UnitStateResult{{ // RelationState: map[int]string{2: "id: 2\n"}, //}}} - //apiCalls = append(apiCalls, uniterAPICall("State", nrpeUnitEntity, unitStateResults, nil)) + //apiCalls = append(apiCalls, uniterAPICall("State", nrpeUnitEntitySingleton, unitStateResults, nil)) apiCaller := mockAPICaller(c, &numCalls, apiCalls...) r := s.newRelationStateTracker(c, apiCaller, nrpeUnitTag) @@ -1015,7 +1020,7 @@ func (s *relationResolverSuite) TestSubSubOtherRelationDyingNotDestroyed(c *gc.C //unitStateResults := params.UnitStateResults{Results: []params.UnitStateResult{{ // RelationState: map[int]string{2: "id: 2\n"}, //}}} - //apiCalls = append(apiCalls, uniterAPICall("State", nrpeUnitEntity, unitStateResults, nil)) + //apiCalls = append(apiCalls, uniterAPICall("State", nrpeUnitEntitySingleton, unitStateResults, nil)) apiCaller := mockAPICaller(c, &numCalls, apiCalls...) @@ -1109,13 +1114,13 @@ func principalWithSubordinateAPICalls() []apiCall { unitStateResults := params.UnitStateResults{Results: []params.UnitStateResult{{}}} return []apiCall{ - uniterAPICall("Refresh", nrpeUnitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: life.Alive, Resolved: params.ResolvedNone}}}, nil), - uniterAPICall("GetPrincipal", nrpeUnitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "unit-wordpress-0", Ok: true}}}, nil), - uniterAPICall("State", nrpeUnitEntity, unitStateResults, nil), - uniterAPICall("RelationsStatus", nrpeUnitEntity, relationStatusResults, nil), + uniterAPICall("Refresh", nrpeUnitEntitySingleton, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: life.Alive, Resolved: params.ResolvedNone}}}, nil), + uniterAPICall("GetPrincipal", nrpeUnitEntitySingleton, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "unit-wordpress-0", Ok: true}}}, nil), + uniterAPICall("State", nrpeUnitEntitySingleton, unitStateResults, nil), + uniterAPICall("RelationsStatus", nrpeUnitEntitySingleton, relationStatusResults, nil), uniterAPICall("Relation", relationUnits1, relationResults1, nil), uniterAPICall("Relation", relationUnits1, relationResults1, nil), - uniterAPICall("Watch", nrpeUnitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil), + uniterAPICall("WatchUnit", nrpeUnitEntity, params.NotifyWatchResult{NotifyWatcherId: "1"}, nil), uniterAPICall("SetState", unitSetStateArgs, noErrorResult, nil), uniterAPICall("EnterScope", relationUnits1, noErrorResult, nil), uniterAPICall("SetRelationStatus", relationStatus1, noErrorResult, nil), @@ -1131,11 +1136,11 @@ func (s *relationResolverSuite) TestPrincipalDyingDestroysSubordinates(c *gc.C) callsBeforeDestroy := int32(len(apiCalls)) callsAfterDestroy := callsBeforeDestroy + 1 // This should only be called after we queue the subordinate for destruction - apiCalls = append(apiCalls, uniterAPICall("Destroy", nrpeUnitEntity, noErrorResult, nil)) + apiCalls = append(apiCalls, uniterAPICall("Destroy", nrpeUnitEntitySingleton, noErrorResult, nil)) //unitStateResults := params.UnitStateResults{Results: []params.UnitStateResult{{ // RelationState: map[int]string{1: "id: 1\n", 73: ""}, //}}} - //apiCalls = append(apiCalls, uniterAPICall("State", nrpeUnitEntity, unitStateResults, nil)) + //apiCalls = append(apiCalls, uniterAPICall("State", nrpeUnitEntitySingleton, unitStateResults, nil)) apiCaller := mockAPICaller(c, &numCalls, apiCalls...) r := s.newRelationStateTracker(c, apiCaller, nrpeUnitTag)