Skip to content

Commit 685b050

Browse files
committed
add DeployDirtyAccounts function and corresponding tests for account deployment and dirty flag management
1 parent 9731146 commit 685b050

3 files changed

Lines changed: 121 additions & 0 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) 2025 ToeiRei
2+
// Keymaster - SSH key management system
3+
// This source code is licensed under the MIT license found in the LICENSE file.
4+
5+
package core
6+
7+
import (
8+
"context"
9+
"fmt"
10+
)
11+
12+
// DeployDirtyAccounts fetches all active accounts from the store, selects
13+
// accounts marked `IsDirty`, deploys to each using the provided DeployerManager,
14+
// and clears the `is_dirty` flag for accounts that deployed successfully.
15+
// It returns the per-account DeployResult slice and an error if fetching
16+
// accounts failed.
17+
func DeployDirtyAccounts(ctx context.Context, st Store, dm DeployerManager, rep Reporter) ([]DeployResult, error) {
18+
accounts, err := st.GetAllActiveAccounts()
19+
if err != nil {
20+
return nil, fmt.Errorf("get accounts: %w", err)
21+
}
22+
23+
dirty := DirtyAccounts(accounts)
24+
results := make([]DeployResult, 0, len(dirty))
25+
for _, acc := range dirty {
26+
err := dm.DeployForAccount(acc, false)
27+
results = append(results, DeployResult{Account: acc, Error: err})
28+
if err == nil {
29+
// Best-effort: clear is_dirty; log/store error ignored for now
30+
_ = st.UpdateAccountIsDirty(acc.ID, false)
31+
}
32+
}
33+
return results, nil
34+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) 2025 ToeiRei
2+
// Keymaster - SSH key management system
3+
// This source code is licensed under the MIT license found in the LICENSE file.
4+
5+
package core
6+
7+
import (
8+
"context"
9+
"testing"
10+
11+
"github.com/toeirei/keymaster/internal/model"
12+
)
13+
14+
type fakeStoreForDirty struct {
15+
accounts []model.Account
16+
cleared []int
17+
}
18+
19+
func (f *fakeStoreForDirty) GetAllActiveAccounts() ([]model.Account, error) { return f.accounts, nil }
20+
func (f *fakeStoreForDirty) UpdateAccountIsDirty(id int, dirty bool) error {
21+
if !dirty {
22+
f.cleared = append(f.cleared, id)
23+
}
24+
return nil
25+
}
26+
27+
// implement remaining Store methods as no-ops to satisfy interface
28+
func (f *fakeStoreForDirty) GetAccounts() ([]model.Account, error) { return nil, nil }
29+
func (f *fakeStoreForDirty) GetAllAccounts() ([]model.Account, error) { return nil, nil }
30+
func (f *fakeStoreForDirty) GetAccount(id int) (*model.Account, error) { return nil, nil }
31+
func (f *fakeStoreForDirty) AddAccount(username, hostname, label, tags string) (int, error) {
32+
return 0, nil
33+
}
34+
func (f *fakeStoreForDirty) DeleteAccount(accountID int) error { return nil }
35+
func (f *fakeStoreForDirty) AssignKeyToAccount(keyID, accountID int) error { return nil }
36+
func (f *fakeStoreForDirty) CreateSystemKey(publicKey, privateKey string) (int, error) { return 0, nil }
37+
func (f *fakeStoreForDirty) RotateSystemKey(publicKey, privateKey string) (int, error) { return 0, nil }
38+
func (f *fakeStoreForDirty) GetActiveSystemKey() (*model.SystemKey, error) { return nil, nil }
39+
func (f *fakeStoreForDirty) AddKnownHostKey(hostname, key string) error { return nil }
40+
func (f *fakeStoreForDirty) ExportDataForBackup() (*model.BackupData, error) { return nil, nil }
41+
func (f *fakeStoreForDirty) ImportDataFromBackup(*model.BackupData) error { return nil }
42+
func (f *fakeStoreForDirty) IntegrateDataFromBackup(*model.BackupData) error { return nil }
43+
44+
type fakeDMForDirty struct{ called []int }
45+
46+
func (f *fakeDMForDirty) DeployForAccount(account model.Account, keepFile bool) error {
47+
f.called = append(f.called, account.ID)
48+
return nil
49+
}
50+
func (f *fakeDMForDirty) AuditSerial(account model.Account) error { return nil }
51+
func (f *fakeDMForDirty) AuditStrict(account model.Account) error { return nil }
52+
func (f *fakeDMForDirty) DecommissionAccount(account model.Account, systemPrivateKey string, options interface{}) (DecommissionResult, error) {
53+
return DecommissionResult{}, nil
54+
}
55+
func (f *fakeDMForDirty) BulkDecommissionAccounts(accounts []model.Account, systemPrivateKey string, options interface{}) ([]DecommissionResult, error) {
56+
return nil, nil
57+
}
58+
func (f *fakeDMForDirty) CanonicalizeHostPort(host string) string { return host }
59+
func (f *fakeDMForDirty) ParseHostPort(host string) (string, string, error) { return host, "22", nil }
60+
func (f *fakeDMForDirty) GetRemoteHostKey(host string) (string, error) { return "", nil }
61+
func (f *fakeDMForDirty) FetchAuthorizedKeys(account model.Account) ([]byte, error) { return nil, nil }
62+
func (f *fakeDMForDirty) ImportRemoteKeys(account model.Account) ([]model.PublicKey, int, string, error) {
63+
return nil, 0, "", nil
64+
}
65+
func (f *fakeDMForDirty) IsPassphraseRequired(err error) bool { return false }
66+
67+
func TestDeployDirtyAccounts_ClearsOnSuccess(t *testing.T) {
68+
st := &fakeStoreForDirty{accounts: []model.Account{{ID: 1, IsDirty: false}, {ID: 2, IsDirty: true}, {ID: 3, IsDirty: true}}}
69+
dm := &fakeDMForDirty{}
70+
71+
res, err := DeployDirtyAccounts(context.Background(), st, dm, nil)
72+
if err != nil {
73+
t.Fatalf("unexpected error: %v", err)
74+
}
75+
if len(res) != 2 {
76+
t.Fatalf("expected 2 results, got %d", len(res))
77+
}
78+
if len(dm.called) != 2 || dm.called[0] != 2 || dm.called[1] != 3 {
79+
t.Fatalf("unexpected deploy calls: %v", dm.called)
80+
}
81+
if len(st.cleared) != 2 {
82+
t.Fatalf("expected 2 cleared flags, got %d", len(st.cleared))
83+
}
84+
}

internal/core/interfaces.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ type Store interface {
2626
DeleteAccount(accountID int) error
2727
AssignKeyToAccount(keyID, accountID int) error
2828

29+
// UpdateAccountIsDirty sets or clears the is_dirty flag for an account.
30+
UpdateAccountIsDirty(id int, dirty bool) error
31+
2932
// System key helpers
3033
CreateSystemKey(publicKey, privateKey string) (int, error)
3134
RotateSystemKey(publicKey, privateKey string) (int, error)

0 commit comments

Comments
 (0)