mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 18:27:36 +00:00
feat(agent): added dynamic secret change based re-trigger
This commit is contained in:
@@ -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
@@ -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, ¤tEtag)
|
processedTemplate, err = ProcessTemplate(secretTemplate.SourcePath, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases)
|
||||||
|
templateName = path.Base(secretTemplate.SourcePath)
|
||||||
} else {
|
} else {
|
||||||
processedTemplate, err = ProcessBase64Template(secretTemplate.Base64TemplateContent, nil, token, existingEtag, ¤tEtag)
|
processedTemplate, err = ProcessBase64Template(secretTemplate.Base64TemplateContent, nil, token, existingEtag, ¤tEtag, 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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user