Skip to content

Commit 9d53e09

Browse files
committed
Cache the generateCipherKey function and add profiling support to lint
The lint/apply/dry-run had become slow, and profiling showed up that we were calling generateCipherKey many times as the yaml base grows. This patch adds caching to the generateCipherKey function avoiding the regeneration for the same password/salt. This reduces our lint run time from 17s to 0.4s
1 parent d6b4bd4 commit 9d53e09

File tree

3 files changed

+83
-7
lines changed

3 files changed

+83
-7
lines changed

cmd/lint.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ package main
1818

1919
import (
2020
"fmt"
21+
"os"
22+
"runtime/pprof"
23+
"time"
2124

2225
"github.com/spf13/cobra"
2326

@@ -33,6 +36,7 @@ var lintCmd = &cobra.Command{
3336
SilenceUsage: true,
3437
RunE: func(cmd *cobra.Command, args []string) error {
3538
vaultPassFile, _ := cmd.Flags().GetString("vault-password-file")
39+
cpuProfile, _ := cmd.Flags().GetString("cpu-profile")
3640
// Determine config file path
3741
configFilePath := defaultConfigFile
3842
if len(args) > 0 {
@@ -47,7 +51,37 @@ var lintCmd = &cobra.Command{
4751

4852
fmt.Println("🔍 Validating configuration...")
4953

50-
config_lint.Validate(cfg)
54+
// Start CPU profiling if requested
55+
if cpuProfile != "" {
56+
f, err := os.Create(cpuProfile)
57+
if err != nil {
58+
return fmt.Errorf("could not create CPU profile: %w", err)
59+
}
60+
defer func() {
61+
if closeErr := f.Close(); closeErr != nil {
62+
fmt.Printf("Warning: failed to close profile file: %v\n", closeErr)
63+
}
64+
}()
65+
66+
if err := pprof.StartCPUProfile(f); err != nil {
67+
return fmt.Errorf("could not start CPU profile: %w", err)
68+
}
69+
defer pprof.StopCPUProfile()
70+
71+
fmt.Printf("📊 CPU profiling enabled, output will be saved to: %s\n", cpuProfile)
72+
}
73+
74+
// Time the validation
75+
start := time.Now()
76+
err = config_lint.ValidateWithError(cfg)
77+
duration := time.Since(start)
78+
79+
if err != nil {
80+
fmt.Printf("❌ Configuration validation failed in %v\n", duration)
81+
return err
82+
}
83+
84+
fmt.Printf("✅ Configuration validation completed in %v\n", duration)
5185

5286
return nil
5387
},
@@ -56,6 +90,8 @@ var lintCmd = &cobra.Command{
5690
func init() {
5791
// Add the vault password file flag
5892
lintCmd.Flags().String("vault-password-file", "", "Path to the vault password file for decrypting variables")
93+
// Add the CPU profiling flag
94+
lintCmd.Flags().String("cpu-profile", "", "Enable CPU profiling and save output to the specified file")
5995
// Add the lint command to the root command
6096
rootCmd.AddCommand(lintCmd)
6197
}

internal/config_lint/lint.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,21 @@ func Validate(cfg *config.Config) {
2424
fmt.Println("✅ All configurations are valid")
2525
}
2626

27+
// ValidateWithError checks the loaded configuration for errors and returns an error if any are found.
28+
// This version does not call os.Exit() and is suitable for use with profiling.
29+
func ValidateWithError(cfg *config.Config) error {
30+
errorsByFile := Lint(cfg)
31+
if len(errorsByFile) > 0 {
32+
reportAllErrors(errorsByFile)
33+
return fmt.Errorf("validation failed with %d error(s)", len(errorsByFile))
34+
}
35+
keys := cfg.Loaded.GetVariables().GetAllKeys()
36+
fmt.Printf("📚 Total Variables: %d\n", len(keys))
37+
fmt.Println("")
38+
fmt.Println("✅ All configurations are valid")
39+
return nil
40+
}
41+
2742
func Lint(cfg *config.Config) map[string][]error {
2843
// This function is a placeholder for the linting logic.
2944
// Currently, it only validates cross-references between objects.

internal/vars/vault.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/hex"
99
"errors"
1010
"strings"
11+
"sync"
1112

1213
"golang.org/x/crypto/pbkdf2"
1314
)
@@ -38,6 +39,8 @@ type CipherKey struct {
3839
// VaultDecryptor handles Ansible Vault decryption
3940
type VaultDecryptor struct {
4041
password string
42+
cache map[string]*CipherKey
43+
mutex sync.RWMutex
4144
}
4245

4346
// NewVaultDecryptor creates a new vault decryptor with the given password
@@ -47,7 +50,10 @@ func NewVaultDecryptor(password string) *VaultDecryptor {
4750
password = strings.ReplaceAll(password, "\n", "")
4851
password = strings.ReplaceAll(password, "\r", "")
4952
password = strings.ReplaceAll(password, "\t", "")
50-
return &VaultDecryptor{password: password}
53+
return &VaultDecryptor{
54+
password: password,
55+
cache: make(map[string]*CipherKey),
56+
}
5157
}
5258

5359
// IsVaultEncrypted checks if a variable value is Ansible Vault encrypted
@@ -76,7 +82,7 @@ func (vd *VaultDecryptor) Decrypt(vaultData string) (string, error) {
7682
return "", err
7783
}
7884
// Generate keys
79-
key := generateCipherKey(vd.password, salt)
85+
key := vd.generateCipherKey(salt)
8086

8187
// Verify checksum
8288
if !isChecksumValid(checkSum, encryptedData, key.HMACKey) {
@@ -134,21 +140,40 @@ func parseVaultData(vaultData string) (salt, checkSum, data []byte, err error) {
134140
return salt, checkSum, data, nil
135141
}
136142

137-
// generateCipherKey generates cipher keys from password and salt
138-
func generateCipherKey(password string, salt []byte) *CipherKey {
143+
// generateCipherKey generates cipher keys from password and salt with caching
144+
func (vd *VaultDecryptor) generateCipherKey(salt []byte) *CipherKey {
145+
// Create cache key from salt (password is already stored in the struct)
146+
cacheKey := hex.EncodeToString(salt)
147+
148+
// Try to get from cache first (read lock)
149+
vd.mutex.RLock()
150+
if cached, exists := vd.cache[cacheKey]; exists {
151+
vd.mutex.RUnlock()
152+
return cached
153+
}
154+
vd.mutex.RUnlock()
155+
156+
// Generate new cipher key
139157
k := pbkdf2.Key(
140-
[]byte(password),
158+
[]byte(vd.password),
141159
salt,
142160
iteration,
143161
(cipherKeyLength + hmacKeyLength + ivLength),
144162
sha256.New,
145163
)
146164

147-
return &CipherKey{
165+
cipherKey := &CipherKey{
148166
Key: k[:cipherKeyLength],
149167
HMACKey: k[cipherKeyLength:(cipherKeyLength + hmacKeyLength)],
150168
IV: k[(cipherKeyLength + hmacKeyLength):(cipherKeyLength + hmacKeyLength + ivLength)],
151169
}
170+
171+
// Store in cache (write lock)
172+
vd.mutex.Lock()
173+
vd.cache[cacheKey] = cipherKey
174+
vd.mutex.Unlock()
175+
176+
return cipherKey
152177
}
153178

154179
// isChecksumValid validates HMAC checksum

0 commit comments

Comments
 (0)