feat(agent): added dynamic secret change based re-trigger

This commit is contained in:
Akhil Mohan
2024-03-22 23:59:08 +05:30
parent a07d055347
commit 88f7e4255e
3 changed files with 168 additions and 50 deletions
+1 -1
View File
@@ -512,7 +512,7 @@ type CreateDynamicSecretLeaseV1Request struct {
type CreateDynamicSecretLeaseV1Response struct { type CreateDynamicSecretLeaseV1Response struct {
Lease struct { Lease struct {
Id string `json:"id"` Id string `json:"id"`
ExpireAt string `json:"expireAt"` ExpireAt time.Time `json:"expireAt"`
} `json:"lease"` } `json:"lease"`
DynamicSecret struct { DynamicSecret struct {
Id string `json:"id"` Id string `json:"id"`
+135 -19
View File
@@ -14,6 +14,7 @@ import (
"os/signal" "os/signal"
"path" "path"
"runtime" "runtime"
"slices"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
@@ -84,6 +85,15 @@ type Template struct {
} `yaml:"config"` } `yaml:"config"`
} }
func newAgentTemplateChannels(templates []Template) map[string]chan bool {
// we keep each destination as an identifier for various channel
templateChannel := make(map[string]chan bool)
for _, template := range templates {
templateChannel[template.DestinationPath] = make(chan bool)
}
return templateChannel
}
type DynamicSecretLease struct { type DynamicSecretLease struct {
LeaseID string LeaseID string
ExpireAt time.Time ExpireAt time.Time
@@ -92,21 +102,100 @@ type DynamicSecretLease struct {
Slug string Slug string
ProjectSlug string ProjectSlug string
Data map[string]interface{} Data map[string]interface{}
Templates []string
} }
type DynamicSecretLeaseManger []DynamicSecretLease type DynamicSecretLeaseManager struct {
leases []DynamicSecretLease
mutex sync.Mutex
}
func (d DynamicSecretLeaseManger) FindLease(projectSlug, environment, secretPath, slug string) *DynamicSecretLease { func (d *DynamicSecretLeaseManager) Prune() {
for _, lease := range d { d.mutex.Lock()
// presentTime := time.Now() defer d.mutex.Unlock()
// isExpired := presentTime.Before(lease.ExpireAt.Add(-15 * time.Second))
d.leases = slices.DeleteFunc(d.leases, func(s DynamicSecretLease) bool {
return time.Now().After(s.ExpireAt.Add(-15 * time.Second))
})
}
func (d *DynamicSecretLeaseManager) Append(lease DynamicSecretLease) {
d.mutex.Lock()
defer d.mutex.Unlock()
index := slices.IndexFunc(d.leases, func(s DynamicSecretLease) bool {
if lease.SecretPath == s.SecretPath && lease.Environment == s.Environment && lease.ProjectSlug == s.ProjectSlug && lease.Slug == s.Slug {
return true
}
return false
})
if index != -1 {
d.leases[index].Templates = append(d.leases[index].Templates, lease.Templates...)
return
}
d.leases = append(d.leases, lease)
}
func (d *DynamicSecretLeaseManager) RegisterTemplate(projectSlug, environment, secretPath, slug, templateName string) {
d.mutex.Lock()
defer d.mutex.Unlock()
index := slices.IndexFunc(d.leases, func(lease DynamicSecretLease) bool {
if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug {
return true
}
return false
})
if index != -1 {
d.leases[index].Templates = append(d.leases[index].Templates, templateName)
}
}
func (d *DynamicSecretLeaseManager) GetLease(projectSlug, environment, secretPath, slug string) *DynamicSecretLease {
d.mutex.Lock()
defer d.mutex.Unlock()
for _, lease := range d.leases {
if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug { if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug {
return &lease return &lease
} }
} }
return nil return nil
} }
// this is like etag for dynamic secret
func (d *DynamicSecretLeaseManager) GetDynamicSecretTemplateTag(templateName string) string {
d.mutex.Lock()
defer d.mutex.Unlock()
tag := ""
for _, el := range d.leases {
if slices.Contains(el.Templates, templateName) {
tag += el.LeaseID
}
}
return tag
}
func NewDynamicSecretLeaseManager(sigChan chan os.Signal) *DynamicSecretLeaseManager {
manager := &DynamicSecretLeaseManager{}
go func() {
for {
select {
case <-sigChan:
return
default:
time.Sleep(5 * time.Second)
manager.Prune()
}
}
}()
return manager
}
func ReadFile(filePath string) ([]byte, error) { func ReadFile(filePath string) ([]byte, error) {
return ioutil.ReadFile(filePath) return ioutil.ReadFile(filePath)
@@ -258,28 +347,34 @@ func secretTemplateFunction(accessToken string, existingEtag string, currentEtag
} }
} }
func dynamicSecretTemplateFunction(accessToken string) func(string, string, string, string) (map[string]interface{}, error) { func dynamicSecretTemplateFunction(accessToken string, dynamicSecretManager *DynamicSecretLeaseManager, templateName string) func(string, string, string, string) (map[string]interface{}, error) {
return func(projectSlug, envSlug, secretPath, slug string) (map[string]interface{}, error) { return func(projectSlug, envSlug, secretPath, slug string) (map[string]interface{}, error) {
dynamicSecretData := dynamicSecretManager.GetLease(projectSlug, envSlug, secretPath, slug)
if dynamicSecretData != nil {
dynamicSecretManager.RegisterTemplate(projectSlug, envSlug, secretPath, slug, templateName)
return dynamicSecretData.Data, nil
}
res, err := util.CreateDynamicSecretLease(accessToken, projectSlug, envSlug, secretPath, slug) res, err := util.CreateDynamicSecretLease(accessToken, projectSlug, envSlug, secretPath, slug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
dynamicSecretManager.Append(DynamicSecretLease{LeaseID: res.Lease.Id, ExpireAt: res.Lease.ExpireAt, Environment: envSlug, SecretPath: secretPath, Slug: slug, ProjectSlug: projectSlug, Data: res.Data, Templates: []string{templateName}})
return res.Data, nil return res.Data, nil
} }
} }
func ProcessTemplate(templatePath string, data interface{}, accessToken string, existingEtag string, currentEtag *string) (*bytes.Buffer, error) { func ProcessTemplate(templatePath string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretManager *DynamicSecretLeaseManager) (*bytes.Buffer, error) {
templateName := path.Base(templatePath)
// custom template function to fetch secrets from Infisical // custom template function to fetch secrets from Infisical
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag)
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken) dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretManager, templateName)
funcs := template.FuncMap{ funcs := template.FuncMap{
"secret": secretFunction, "secret": secretFunction,
"dynamic_secret": dynamicSecretFunction, "dynamic_secret": dynamicSecretFunction,
} }
templateName := path.Base(templatePath)
tmpl, err := template.New(templateName).Funcs(funcs).ParseFiles(templatePath) tmpl, err := template.New(templateName).Funcs(funcs).ParseFiles(templatePath)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -293,7 +388,7 @@ func ProcessTemplate(templatePath string, data interface{}, accessToken string,
return &buf, nil return &buf, nil
} }
func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken string, existingEtag string, currentEtag *string) (*bytes.Buffer, error) { func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretLeaser *DynamicSecretLeaseManager) (*bytes.Buffer, error) {
// custom template function to fetch secrets from Infisical // custom template function to fetch secrets from Infisical
decoded, err := base64.StdEncoding.DecodeString(encodedTemplate) decoded, err := base64.StdEncoding.DecodeString(encodedTemplate)
if err != nil { if err != nil {
@@ -303,7 +398,7 @@ func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken
templateString := string(decoded) templateString := string(decoded)
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken) dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretLeaser, encodedTemplate)
funcs := template.FuncMap{ funcs := template.FuncMap{
"secret": secretFunction, "secret": secretFunction,
"dynamic_secret": dynamicSecretFunction, "dynamic_secret": dynamicSecretFunction,
@@ -333,7 +428,7 @@ type AgentManager struct {
mutex sync.Mutex mutex sync.Mutex
filePaths []Sink // Store file paths if needed filePaths []Sink // Store file paths if needed
templates []Template templates []Template
dynamicSecretLeases []DynamicSecretLeaseManger dynamicSecretLeases *DynamicSecretLeaseManager
clientIdPath string clientIdPath string
clientSecretPath string clientSecretPath string
newAccessTokenNotificationChan chan bool newAccessTokenNotificationChan chan bool
@@ -557,23 +652,38 @@ func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, sigChan ch
var existingEtag string var existingEtag string
var currentEtag string var currentEtag string
var dynamicSecretTag string
var currentDynamicSecretTag string
var templateName string
var firstRun = true var firstRun = true
var pollingLapsed = pollingInterval
execTimeout := secretTemplate.Config.Execute.Timeout execTimeout := secretTemplate.Config.Execute.Timeout
execCommand := secretTemplate.Config.Execute.Command execCommand := secretTemplate.Config.Execute.Command
for { for {
select {
case <-sigChan:
return
default:
{
pollingLapsed = pollingLapsed - 5*time.Second
if !firstRun {
currentDynamicSecretTag = tm.dynamicSecretLeases.GetDynamicSecretTemplateTag(templateName)
}
shouldTrigger := currentDynamicSecretTag != dynamicSecretTag || pollingLapsed < 0
if shouldTrigger {
token := tm.GetToken() token := tm.GetToken()
if token != "" { if token != "" {
var processedTemplate *bytes.Buffer var processedTemplate *bytes.Buffer
var err error var err error
if secretTemplate.SourcePath != "" { if secretTemplate.SourcePath != "" {
processedTemplate, err = ProcessTemplate(secretTemplate.SourcePath, nil, token, existingEtag, &currentEtag) processedTemplate, err = ProcessTemplate(secretTemplate.SourcePath, nil, token, existingEtag, &currentEtag, tm.dynamicSecretLeases)
templateName = path.Base(secretTemplate.SourcePath)
} else { } else {
processedTemplate, err = ProcessBase64Template(secretTemplate.Base64TemplateContent, nil, token, existingEtag, &currentEtag) processedTemplate, err = ProcessBase64Template(secretTemplate.Base64TemplateContent, nil, token, existingEtag, &currentEtag, tm.dynamicSecretLeases)
templateName = path.Base(secretTemplate.Base64TemplateContent)
} }
if err != nil { if err != nil {
@@ -598,12 +708,17 @@ func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, sigChan ch
} }
} }
} }
time.Sleep(pollingInterval) dynamicSecretTag = tm.dynamicSecretLeases.GetDynamicSecretTemplateTag(templateName)
// restart the polling because latest changes have been picked
pollingLapsed = pollingInterval
}
time.Sleep(5 * time.Second)
} else { } else {
// It fails to get the access token. So we will re-try in 3 seconds. We do this because if we don't, the user will have to wait for the next polling interval to get the first secret render. // It fails to get the access token. So we will re-try in 3 seconds. We do this because if we don't, the user will have to wait for the next polling interval to get the first secret render.
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
} }
}
}
} }
} }
@@ -686,6 +801,7 @@ var agentCmd = &cobra.Command{
filePaths := agentConfig.Sinks filePaths := agentConfig.Sinks
tm := NewAgentManager(filePaths, agentConfig.Templates, configUniversalAuthType.ClientIDPath, configUniversalAuthType.ClientSecretPath, tokenRefreshNotifier, configUniversalAuthType.RemoveClientSecretOnRead, agentConfig.Infisical.ExitAfterAuth) tm := NewAgentManager(filePaths, agentConfig.Templates, configUniversalAuthType.ClientIDPath, configUniversalAuthType.ClientSecretPath, tokenRefreshNotifier, configUniversalAuthType.RemoveClientSecretOnRead, agentConfig.Infisical.ExitAfterAuth)
tm.dynamicSecretLeases = NewDynamicSecretLeaseManager(sigChan)
go tm.ManageTokenLifecycle() go tm.ManageTokenLifecycle()
+3 -1
View File
@@ -1,5 +1,7 @@
package models package models
import "time"
type UserCredentials struct { type UserCredentials struct {
Email string `json:"email"` Email string `json:"email"`
PrivateKey string `json:"privateKey"` PrivateKey string `json:"privateKey"`
@@ -50,7 +52,7 @@ type DynamicSecret struct {
type DynamicSecretLease struct { type DynamicSecretLease struct {
Lease struct { Lease struct {
Id string `json:"id"` Id string `json:"id"`
ExpireAt string `json:"expireAt"` ExpireAt time.Time `json:"expireAt"`
} `json:"lease"` } `json:"lease"`
DynamicSecret DynamicSecret `json:"dynamicSecret"` DynamicSecret DynamicSecret `json:"dynamicSecret"`
// this is a varying dict based on provider // this is a varying dict based on provider