Skip to content

Commit bd8348b

Browse files
authored
feat(cache): add cache (#28)
This is completely optional. Setting this to either `true` (default caching of `12h`), `false` (disable completely, which should be the default), a `number` (in seconds) or a `string` like `1d`, `2w`, `72h`, `2y` will then store the values in a sqlite db in `.cache/kuba/db.sqlite`. The reason behind this is, that you might want to have your GCP secrets of your dayjob not stored/cached, but you have a personal/hobby project, where you want to cache your bitwarden secrets.
1 parent f199aec commit bd8348b

File tree

17 files changed

+1302
-2
lines changed

17 files changed

+1302
-2
lines changed

.github/workflows/web.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ jobs:
4747
linux-bun-${{ hashFiles('**/bun.lock') }}
4848
- name: Add kuba.schema.json to website
4949
run: cp kuba.schema.json web/static/kuba.schema.json
50+
- name: Add kuba-global.schema.json to website
51+
run: cp kuba-global.schema.json web/static/kuba-global.schema.json
5052
- name: Install dependencies
5153
run: cd web && bun install --frozen-lockfile
5254
- name: Create Website

cmd/kuba/cache.go

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
package kuba
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
"strings"
7+
8+
"github.com/mistweaverco/kuba/internal/config"
9+
"github.com/mistweaverco/kuba/internal/lib/cache"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var (
14+
cachePath string
15+
cacheEnv string
16+
cacheVerbose bool
17+
)
18+
19+
var cacheCmd = &cobra.Command{
20+
Use: "cache",
21+
Short: "Manage kuba cache",
22+
Long: `Manage the kuba secrets cache.
23+
24+
This command allows you to:
25+
- List cached secrets
26+
- Clear cache entries
27+
- Show cache statistics
28+
- Configure cache settings
29+
30+
The cache is stored in ~/.cache/kuba/db.sqlite and helps reduce API calls
31+
to cloud providers by storing secrets temporarily.`,
32+
Args: cobra.NoArgs,
33+
RunE: func(cmd *cobra.Command, args []string) error {
34+
return runCacheCommand()
35+
},
36+
}
37+
38+
var cacheListCmd = &cobra.Command{
39+
Use: "list",
40+
Short: "List cached secrets",
41+
Long: "List all cached secrets with their metadata.",
42+
Args: cobra.NoArgs,
43+
RunE: func(cmd *cobra.Command, args []string) error {
44+
return runCacheList()
45+
},
46+
}
47+
48+
var cacheClearCmd = &cobra.Command{
49+
Use: "clear",
50+
Short: "Clear cached secrets",
51+
Long: `Clear cached secrets.
52+
53+
By default, clears all cached secrets. Use --path to clear secrets for a specific
54+
kuba.yaml file, or --env to clear secrets for a specific environment.`,
55+
Args: cobra.NoArgs,
56+
RunE: func(cmd *cobra.Command, args []string) error {
57+
return runCacheClear()
58+
},
59+
}
60+
61+
var cacheStatsCmd = &cobra.Command{
62+
Use: "stats",
63+
Short: "Show cache statistics",
64+
Long: "Show cache statistics including entry counts and configuration.",
65+
Args: cobra.NoArgs,
66+
RunE: func(cmd *cobra.Command, args []string) error {
67+
return runCacheStats()
68+
},
69+
}
70+
71+
var configCacheCmd = &cobra.Command{
72+
Use: "cache",
73+
Short: "Configure cache settings",
74+
Long: `Configure global cache settings.
75+
76+
Examples:
77+
kuba config cache --enable --ttl 1d
78+
kuba config cache --disable
79+
kuba config cache --ttl 2w`,
80+
Args: cobra.NoArgs,
81+
RunE: func(cmd *cobra.Command, args []string) error {
82+
return runCacheConfigWithCmd(cmd)
83+
},
84+
}
85+
86+
func init() {
87+
// Add cache command to root
88+
rootCmd.AddCommand(cacheCmd)
89+
90+
// Add subcommands to cache
91+
cacheCmd.AddCommand(cacheListCmd)
92+
cacheCmd.AddCommand(cacheClearCmd)
93+
cacheCmd.AddCommand(cacheStatsCmd)
94+
95+
// Global flags for cache commands
96+
cacheCmd.PersistentFlags().StringVarP(&cachePath, "path", "p", "", "Path to kuba.yaml file")
97+
cacheCmd.PersistentFlags().StringVarP(&cacheEnv, "env", "e", "", "Environment name")
98+
cacheCmd.PersistentFlags().BoolVarP(&cacheVerbose, "verbose", "v", false, "Verbose output")
99+
100+
// Cache clear flags
101+
cacheClearCmd.Flags().Bool("all", false, "Clear all cached secrets")
102+
cacheClearCmd.Flags().Bool("expired", false, "Clear only expired secrets")
103+
104+
// Cache config flags (moved to config command)
105+
configCacheCmd.Flags().Bool("enable", false, "Enable caching")
106+
configCacheCmd.Flags().Bool("disable", false, "Disable caching")
107+
configCacheCmd.Flags().String("ttl", "", "Set cache TTL (e.g., 1d, 2w, 72h, 2y)")
108+
configCacheCmd.Flags().Bool("show", false, "Show current configuration")
109+
}
110+
111+
func runCacheCommand() error {
112+
fmt.Println("Kuba Cache Management")
113+
fmt.Println("Use 'kuba cache --help' to see available commands.")
114+
return nil
115+
}
116+
117+
func runCacheList() error {
118+
// Load global config
119+
globalConfig, err := config.LoadGlobalConfig()
120+
if err != nil {
121+
return fmt.Errorf("failed to load global config: %w", err)
122+
}
123+
124+
// Convert to cache types
125+
cacheGlobalConfig := &cache.GlobalConfig{
126+
Cache: cache.CacheConfig{
127+
Enabled: globalConfig.Cache.Enabled,
128+
TTL: globalConfig.Cache.TTL,
129+
},
130+
}
131+
132+
// Initialize cache manager
133+
manager, err := cache.NewManager(cacheGlobalConfig)
134+
if err != nil {
135+
return fmt.Errorf("failed to initialize cache manager: %w", err)
136+
}
137+
defer manager.Close()
138+
139+
if !manager.IsEnabled() {
140+
fmt.Println("Caching is disabled.")
141+
return nil
142+
}
143+
144+
// Get cached entries
145+
entries, err := manager.List()
146+
if err != nil {
147+
return fmt.Errorf("failed to list cache entries: %w", err)
148+
}
149+
150+
if len(entries) == 0 {
151+
fmt.Println("No cached secrets found.")
152+
return nil
153+
}
154+
155+
// Filter entries if path or env specified
156+
filteredEntries := entries
157+
if cachePath != "" {
158+
absPath, err := filepath.Abs(cachePath)
159+
if err != nil {
160+
return fmt.Errorf("failed to get absolute path: %w", err)
161+
}
162+
var filtered []cache.CacheEntry
163+
for _, entry := range entries {
164+
if entry.Path == absPath {
165+
filtered = append(filtered, entry)
166+
}
167+
}
168+
filteredEntries = filtered
169+
}
170+
171+
if cacheEnv != "" {
172+
var filtered []cache.CacheEntry
173+
for _, entry := range filteredEntries {
174+
if entry.KubaEnv == cacheEnv {
175+
filtered = append(filtered, entry)
176+
}
177+
}
178+
filteredEntries = filtered
179+
}
180+
181+
// Display entries
182+
fmt.Printf("Found %d cached secret(s):\n\n", len(filteredEntries))
183+
184+
for _, entry := range filteredEntries {
185+
fmt.Printf("Path: %s\n", entry.Path)
186+
fmt.Printf("Environment: %s\n", entry.KubaEnv)
187+
fmt.Printf("Variable: %s\n", entry.Env)
188+
if cacheVerbose {
189+
fmt.Printf("Value: %s\n", entry.Value)
190+
} else {
191+
// Mask the value for security
192+
masked := maskSecret(entry.Value)
193+
fmt.Printf("Value: %s\n", masked)
194+
}
195+
fmt.Printf("Created: %s\n", entry.CreatedAt.Format("2006-01-02 15:04:05"))
196+
fmt.Printf("Expires: %s\n", entry.ExpiresAt.Format("2006-01-02 15:04:05"))
197+
fmt.Println(strings.Repeat("-", 50))
198+
}
199+
200+
return nil
201+
}
202+
203+
func runCacheClear() error {
204+
// Load global config
205+
globalConfig, err := config.LoadGlobalConfig()
206+
if err != nil {
207+
return fmt.Errorf("failed to load global config: %w", err)
208+
}
209+
210+
// Convert to cache types
211+
cacheGlobalConfig := &cache.GlobalConfig{
212+
Cache: cache.CacheConfig{
213+
Enabled: globalConfig.Cache.Enabled,
214+
TTL: globalConfig.Cache.TTL,
215+
},
216+
}
217+
218+
// Initialize cache manager
219+
manager, err := cache.NewManager(cacheGlobalConfig)
220+
if err != nil {
221+
return fmt.Errorf("failed to initialize cache manager: %w", err)
222+
}
223+
defer manager.Close()
224+
225+
if !manager.IsEnabled() {
226+
fmt.Println("Caching is disabled.")
227+
return nil
228+
}
229+
230+
// Determine what to clear
231+
if cachePath != "" && cacheEnv != "" {
232+
// Clear specific environment
233+
if err := manager.ClearByEnvironment(cachePath, cacheEnv); err != nil {
234+
return fmt.Errorf("failed to clear cache for environment: %w", err)
235+
}
236+
fmt.Printf("Cleared cache for environment '%s' in %s\n", cacheEnv, cachePath)
237+
} else if cachePath != "" {
238+
// Clear specific path
239+
if err := manager.ClearByPath(cachePath); err != nil {
240+
return fmt.Errorf("failed to clear cache for path: %w", err)
241+
}
242+
fmt.Printf("Cleared cache for %s\n", cachePath)
243+
} else {
244+
// Clear all
245+
if err := manager.Clear(); err != nil {
246+
return fmt.Errorf("failed to clear cache: %w", err)
247+
}
248+
fmt.Println("Cleared all cached secrets.")
249+
}
250+
251+
return nil
252+
}
253+
254+
func runCacheStats() error {
255+
// Load global config
256+
globalConfig, err := config.LoadGlobalConfig()
257+
if err != nil {
258+
return fmt.Errorf("failed to load global config: %w", err)
259+
}
260+
261+
// Convert to cache types
262+
cacheGlobalConfig := &cache.GlobalConfig{
263+
Cache: cache.CacheConfig{
264+
Enabled: globalConfig.Cache.Enabled,
265+
TTL: globalConfig.Cache.TTL,
266+
},
267+
}
268+
269+
// Initialize cache manager
270+
manager, err := cache.NewManager(cacheGlobalConfig)
271+
if err != nil {
272+
return fmt.Errorf("failed to initialize cache manager: %w", err)
273+
}
274+
defer manager.Close()
275+
276+
// Get stats
277+
stats, err := manager.GetStats()
278+
if err != nil {
279+
return fmt.Errorf("failed to get cache stats: %w", err)
280+
}
281+
282+
fmt.Println("Cache Statistics:")
283+
fmt.Printf("Enabled: %v\n", stats["enabled"])
284+
285+
if enabled, ok := stats["enabled"].(bool); ok && enabled {
286+
fmt.Printf("Total Entries: %v\n", stats["total_entries"])
287+
fmt.Printf("TTL: %v\n", stats["ttl"])
288+
289+
if envCounts, ok := stats["environment_counts"].(map[string]int); ok {
290+
fmt.Println("Entries by Environment:")
291+
for env, count := range envCounts {
292+
fmt.Printf(" %s: %d\n", env, count)
293+
}
294+
}
295+
}
296+
297+
return nil
298+
}
299+
300+
func runCacheConfigWithCmd(cmd *cobra.Command) error {
301+
// Load current config
302+
globalConfig, err := config.LoadGlobalConfig()
303+
if err != nil {
304+
return fmt.Errorf("failed to load global config: %w", err)
305+
}
306+
307+
// Check if we should show current config
308+
show, _ := cmd.Flags().GetBool("show")
309+
if show {
310+
fmt.Println("Current Cache Configuration:")
311+
fmt.Printf("Enabled: %v\n", globalConfig.Cache.Enabled)
312+
fmt.Printf("TTL: %s\n", globalConfig.Cache.TTL)
313+
return nil
314+
}
315+
316+
// Check for conflicting flags
317+
enable, _ := cmd.Flags().GetBool("enable")
318+
disable, _ := cmd.Flags().GetBool("disable")
319+
320+
if enable && disable {
321+
return fmt.Errorf("cannot both enable and disable caching")
322+
}
323+
324+
// Apply changes
325+
modified := false
326+
327+
if enable {
328+
globalConfig.Cache.Enabled = true
329+
modified = true
330+
}
331+
332+
if disable {
333+
globalConfig.Cache.Enabled = false
334+
modified = true
335+
}
336+
337+
ttlStr, _ := cmd.Flags().GetString("ttl")
338+
if ttlStr != "" {
339+
duration, enabled, err := cache.ParseDuration(ttlStr)
340+
if err != nil {
341+
return fmt.Errorf("invalid TTL format: %w", err)
342+
}
343+
globalConfig.Cache.TTL = duration
344+
globalConfig.Cache.Enabled = enabled
345+
modified = true
346+
}
347+
348+
if !modified {
349+
fmt.Println("No changes specified. Use --help to see available options.")
350+
return nil
351+
}
352+
353+
// Save config
354+
if err := config.SaveGlobalConfig(globalConfig); err != nil {
355+
return fmt.Errorf("failed to save global config: %w", err)
356+
}
357+
358+
fmt.Println("Cache configuration updated successfully.")
359+
fmt.Printf("Enabled: %v\n", globalConfig.Cache.Enabled)
360+
fmt.Printf("TTL: %s\n", globalConfig.Cache.TTL)
361+
362+
return nil
363+
}
364+
365+
// maskSecret masks a secret value for display
366+
func maskSecret(value string) string {
367+
if len(value) == 0 {
368+
return ""
369+
}
370+
if len(value) <= 4 {
371+
return strings.Repeat("*", len(value))
372+
}
373+
return value[:2] + strings.Repeat("*", len(value)-4) + value[len(value)-2:]
374+
}

0 commit comments

Comments
 (0)