Skip to content

Commit

Permalink
Merge pull request juju#17442 from ycliuhw/secret-revision-expiry-wat…
Browse files Browse the repository at this point in the history
…cher

juju#17442

This PR implements the service watcher for watching secret expiration changes.
Once this lands, the user can set the expiration time for the secrets, a `secret-expired` will be fired when the secret is expired. There is no business logic change in this PR.

Additionally, this PR fixes a bug in the UpdateCharmSecret method(the secret expiry config was ignored) in the secret domain layer.

## Checklist

<!-- If an item is not applicable, use `~strikethrough~`. -->

- [x] Code style: imports ordered, good names, simple structure, etc
- [x] Comments saying why design decisions were made
- [x] Go unit tests, with comments saying what you're testing
- [ ] ~[Integration tests](https://github.com/juju/juju/tree/main/tests), with comments saying what you're testing~
- [ ] ~[doc.go](https://discourse.charmhub.io/t/readme-in-packages/451) added or updated in changed packages~

## QA steps

```
juju exec --unit dummy-source/0 -- secret-set cpc3skubpn0hb7602f8g --expire=1m

juju exec --unit dummy-source/0 -- secret-set cpc3sbebpn0hb7602f80 --expire=1m

juju show-secret cpc3sbebpn0hb7602f80
cpc3sbebpn0hb7602f80:
 revision: 2
 expires: 2024-05-30T09:07:33.816994754Z
 rotation: never
 owner: dummy-source
 created: 2024-05-30T08:51:57.822932829Z
 updated: 2024-05-30T09:06:33.821168294Z

juju show-secret cpc3skubpn0hb7602f8g
cpc3skubpn0hb7602f8g:
 revision: 3
 expires: 2024-05-30T09:07:23.105946731Z
 rotation: never
 owner: dummy-source/0
 created: 2024-05-30T08:52:35.97915777Z
 updated: 2024-05-30T09:06:23.109453691Z

juju show-status-log dummy-source/0
Time Type Status Message
...
30 May 2024 19:06:33+10:00 juju-unit executing running action juju-exec
30 May 2024 19:06:33+10:00 juju-unit idle
30 May 2024 19:07:23+10:00 juju-unit executing running secret-expired hook for cpc3skubpn0hb7602f8g/3
30 May 2024 19:07:23+10:00 juju-unit executing running secret-expired hook for cpc3sbebpn0hb7602f80/2
```

## Documentation changes

No

## Links

**Jira card:** JUJU-5895
  • Loading branch information
jujubot authored May 31, 2024
2 parents c4ba7a3 + 23410b7 commit b6fe286
Show file tree
Hide file tree
Showing 9 changed files with 726 additions and 135 deletions.
8 changes: 8 additions & 0 deletions domain/secret/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ type State interface {
GetSecretsRotationChanges(
ctx context.Context, appOwners domainsecret.ApplicationOwners, unitOwners domainsecret.UnitOwners, secretIDs ...string,
) ([]domainsecret.RotationInfo, error)

// For watching secret revision expiry changes.
InitialWatchStatementForSecretsRevisionExpiryChanges(
appOwners domainsecret.ApplicationOwners, unitOwners domainsecret.UnitOwners,
) (string, eventsource.NamespaceQuery)
GetSecretsRevisionExpiryChanges(
ctx context.Context, appOwners domainsecret.ApplicationOwners, unitOwners domainsecret.UnitOwners, revisionUUIDs ...string,
) ([]domainsecret.ExpiryInfo, error)
}

// WatcherFactory describes methods for creating watchers.
Expand Down
83 changes: 83 additions & 0 deletions domain/secret/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1692,3 +1692,86 @@ func (s *serviceSuite) TestWatchSecretsRotationChanges(c *gc.C) {
)
wC.AssertNoChange()
}

func (s *serviceSuite) TestWatchSecretRevisionsExpiryChanges(c *gc.C) {
ctrl := gomock.NewController(c)
defer ctrl.Finish()

s.state = NewMockState(ctrl)
mockWatcherFactory := NewMockWatcherFactory(ctrl)

uri1 := coresecrets.NewURI()
uri2 := coresecrets.NewURI()

ch := make(chan []string)
mockStringWatcher := NewMockStringsWatcher(ctrl)
mockStringWatcher.EXPECT().Changes().Return(ch).AnyTimes()
mockStringWatcher.EXPECT().Wait().Return(nil).AnyTimes()
mockStringWatcher.EXPECT().Kill().AnyTimes()

var namespaceQuery eventsource.NamespaceQuery = func(context.Context, database.TxnRunner) ([]string, error) {
return nil, nil
}
s.state.EXPECT().InitialWatchStatementForSecretsRevisionExpiryChanges(
domainsecret.ApplicationOwners{"mediawiki"}, domainsecret.UnitOwners{"mysql/0", "mysql/1"},
).Return("secret_revision_expire", namespaceQuery)
mockWatcherFactory.EXPECT().NewNamespaceWatcher("secret_revision_expire", changestream.All, gomock.Any()).Return(mockStringWatcher, nil)

now := time.Now()
s.state.EXPECT().GetSecretsRevisionExpiryChanges(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, appOwners domainsecret.ApplicationOwners, unitOwners domainsecret.UnitOwners, revisionUUIDs ...string) ([]domainsecret.ExpiryInfo, error) {
c.Assert(appOwners, jc.SameContents, domainsecret.ApplicationOwners{"mediawiki"})
c.Assert(unitOwners, jc.SameContents, domainsecret.UnitOwners{"mysql/0", "mysql/1"})
c.Assert(revisionUUIDs, jc.SameContents, []string{"revision-uuid-1", "revision-uuid-2"})
return []domainsecret.ExpiryInfo{
{
URI: uri1,
Revision: 1,
NextTriggerTime: now,
},
{
URI: uri2,
Revision: 2,
NextTriggerTime: now.Add(2 * time.Hour),
},
}, nil
})
svc := NewWatchableService(s.state, loggertesting.WrapCheckLog(c), mockWatcherFactory, nil)
w, err := svc.WatchSecretRevisionsExpiryChanges(context.Background(),
CharmSecretOwner{
Kind: ApplicationOwner,
ID: "mediawiki",
},
CharmSecretOwner{
Kind: UnitOwner,
ID: "mysql/0",
},
CharmSecretOwner{
Kind: UnitOwner,
ID: "mysql/1",
},
)
c.Assert(err, jc.ErrorIsNil)
c.Assert(w, gc.NotNil)
defer workertest.CleanKill(c, w)
wC := watchertest.NewSecretsTriggerWatcherC(c, w)

select {
case ch <- []string{"revision-uuid-1", "revision-uuid-2"}:
case <-time.After(coretesting.ShortWait):
c.Fatalf("timed out waiting for the initial changes")
}

wC.AssertChange(
watcher.SecretTriggerChange{
URI: uri1,
Revision: 1,
NextTriggerTime: now,
},
watcher.SecretTriggerChange{
URI: uri2,
Revision: 2,
NextTriggerTime: now.Add(2 * time.Hour),
},
)
wC.AssertNoChange()
}
83 changes: 83 additions & 0 deletions domain/secret/service/state_mock_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 28 additions & 40 deletions domain/secret/service/watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/juju/errors"
"github.com/juju/worker/v4"
"github.com/juju/worker/v4/catacomb"
"gopkg.in/tomb.v2"

"github.com/juju/juju/core/changestream"
"github.com/juju/juju/core/logger"
Expand Down Expand Up @@ -125,47 +124,36 @@ func (s *WatchableService) WatchObsolete(ctx context.Context, owners ...CharmSec
return newSecretWatcher(w, s.logger, processChanges)
}

// TODO(secrets) - replace with real watcher
func newMockTriggerWatcher(ch watcher.SecretTriggerChannel) *mockSecretTriggerWatcher {
w := &mockSecretTriggerWatcher{ch: ch}
w.tomb.Go(func() error {
<-w.tomb.Dying()
return tomb.ErrDying
})
return w
}

type mockSecretTriggerWatcher struct {
tomb tomb.Tomb
ch watcher.SecretTriggerChannel
}

func (w *mockSecretTriggerWatcher) Changes() watcher.SecretTriggerChannel {
return w.ch
}

func (w *mockSecretTriggerWatcher) Stop() error {
w.Kill()
return w.Wait()
}

func (w *mockSecretTriggerWatcher) Kill() {
w.tomb.Kill(nil)
}

func (w *mockSecretTriggerWatcher) Err() error {
return w.tomb.Err()
}

func (w *mockSecretTriggerWatcher) Wait() error {
return w.tomb.Wait()
}

// WatchSecretRevisionsExpiryChanges returns a watcher that notifies when the expiry time of a secret revision changes.
func (s *WatchableService) WatchSecretRevisionsExpiryChanges(ctx context.Context, owners ...CharmSecretOwner) (watcher.SecretTriggerWatcher, error) {
ch := make(chan []watcher.SecretTriggerChange, 1)
ch <- []watcher.SecretTriggerChange{}
return newMockTriggerWatcher(ch), nil
if len(owners) == 0 {
return nil, errors.New("at least one owner must be provided")
}

appOwners, unitOwners := splitCharmSecretOwners(owners...)
table, query := s.st.InitialWatchStatementForSecretsRevisionExpiryChanges(appOwners, unitOwners)
w, err := s.watcherFactory.NewNamespaceWatcher(
table, changestream.All, query,
)
if err != nil {
return nil, errors.Trace(err)
}
processChanges := func(ctx context.Context, revisionUUIDs ...string) ([]watcher.SecretTriggerChange, error) {
result, err := s.st.GetSecretsRevisionExpiryChanges(ctx, appOwners, unitOwners, revisionUUIDs...)
if err != nil {
return nil, errors.Trace(err)
}
changes := make([]watcher.SecretTriggerChange, len(result))
for i, r := range result {
changes[i] = watcher.SecretTriggerChange{
URI: r.URI,
Revision: r.Revision,
NextTriggerTime: r.NextTriggerTime,
}
}
return changes, nil
}
return newSecretWatcher(w, s.logger, processChanges)
}

// WatchSecretsRotationChanges returns a watcher that notifies when the rotation time of a secret changes.
Expand Down
Loading

0 comments on commit b6fe286

Please sign in to comment.