Files
nomad/client/allocrunner/taskrunner/secrets_hook.go
Michael Smithhisler 10ed46cbd4 secrets: pass key/value config data to plugins as env (#26455)
Co-authored-by: Michael Schurter <mschurter@hashicorp.com>
Co-authored-by: Tim Gross <tgross@hashicorp.com>
2025-09-05 16:08:24 -04:00

212 lines
5.9 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package taskrunner
import (
"bytes"
"context"
"fmt"
"sync"
"github.com/hashicorp/consul-template/renderer"
"github.com/hashicorp/go-envparse"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/nomad/client/allocrunner/interfaces"
ti "github.com/hashicorp/nomad/client/allocrunner/taskrunner/interfaces"
"github.com/hashicorp/nomad/client/allocrunner/taskrunner/secrets"
"github.com/hashicorp/nomad/client/allocrunner/taskrunner/template"
"github.com/hashicorp/nomad/client/commonplugins"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/taskenv"
"github.com/hashicorp/nomad/nomad/structs"
)
type TemplateProvider interface {
BuildTemplate() *structs.Template
}
type PluginProvider interface {
Fetch(context.Context) (map[string]string, error)
}
type secretsHookConfig struct {
// logger is used to log
logger log.Logger
// lifecycle is used to interact with the task's lifecycle
lifecycle ti.TaskLifecycle
// events is used to emit events
events ti.EventEmitter
// clientConfig is the Nomad Client configuration
clientConfig *config.Config
// envBuilder is the environment variable builder for the task.
envBuilder *taskenv.Builder
// nomadNamespace is the job's Nomad namespace
nomadNamespace string
}
type secretsHook struct {
// logger is used to log
logger log.Logger
// lifecycle is used to interact with the task's lifecycle
lifecycle ti.TaskLifecycle
// events is used to emit events
events ti.EventEmitter
// clientConfig is the Nomad Client configuration
clientConfig *config.Config
// envBuilder is the environment variable builder for the task
envBuilder *taskenv.Builder
// nomadNamespace is the job's Nomad namespace
nomadNamespace string
// secrets to be fetched and populated for interpolation
secrets []*structs.Secret
}
func newSecretsHook(conf *secretsHookConfig, secrets []*structs.Secret) *secretsHook {
return &secretsHook{
logger: conf.logger,
lifecycle: conf.lifecycle,
events: conf.events,
clientConfig: conf.clientConfig,
envBuilder: conf.envBuilder,
nomadNamespace: conf.nomadNamespace,
secrets: secrets,
}
}
func (h *secretsHook) Name() string {
return "secrets"
}
func (h *secretsHook) Prestart(ctx context.Context, req *interfaces.TaskPrestartRequest, resp *interfaces.TaskPrestartResponse) error {
tmplProvider, pluginProvider, err := h.buildSecretProviders(req.TaskDir.SecretsDir)
if err != nil {
return err
}
templates := []*structs.Template{}
for _, p := range tmplProvider {
templates = append(templates, p.BuildTemplate())
}
vaultCluster := req.Task.GetVaultClusterName()
vaultConfig := h.clientConfig.GetVaultConfigs(h.logger)[vaultCluster]
mu := &sync.Mutex{}
contents := []byte{}
unblock := make(chan struct{})
tm, err := template.NewTaskTemplateManager(&template.TaskTemplateManagerConfig{
UnblockCh: unblock,
Lifecycle: h.lifecycle,
Events: h.events,
Templates: templates,
ClientConfig: h.clientConfig,
VaultToken: req.VaultToken,
VaultConfig: vaultConfig,
VaultNamespace: req.Alloc.Job.VaultNamespace,
TaskDir: req.TaskDir.Dir,
EnvBuilder: h.envBuilder,
MaxTemplateEventRate: template.DefaultMaxTemplateEventRate,
NomadNamespace: h.nomadNamespace,
NomadToken: req.NomadToken,
TaskID: req.Alloc.ID + "-" + req.Task.Name,
Logger: h.logger,
// This RenderFunc is used to keep any secret data from being written to disk.
RenderFunc: func(ri *renderer.RenderInput) (*renderer.RenderResult, error) {
// This RenderFunc is called by a single goroutine synchronously, but we
// lock the append in the event this behavior changes without us knowing.
mu.Lock()
defer mu.Unlock()
contents = append(contents, ri.Contents...)
return &renderer.RenderResult{
DidRender: true,
WouldRender: true,
Contents: ri.Contents,
}, nil
},
})
if err != nil {
return err
}
go tm.Run()
// Safeguard against the template manager continuing to run.
defer tm.Stop()
select {
case <-ctx.Done():
return nil
case <-unblock:
}
// Set secrets from templates
m, err := envparse.Parse(bytes.NewBuffer(contents))
if err != nil {
return err
}
h.envBuilder.SetSecrets(m)
// Set secrets from plugin providers
for _, p := range pluginProvider {
vars, err := p.Fetch(ctx)
if err != nil {
return err
}
h.envBuilder.SetSecrets(vars)
}
resp.Done = true
return nil
}
func (h *secretsHook) buildSecretProviders(secretDir string) ([]TemplateProvider, []PluginProvider, error) {
// Any configuration errors will be found when calling the secret providers constructor,
// so use a multierror to collect all errors and return them to the user at the same time.
tmplProvider, pluginProvider, mErr := []TemplateProvider{}, []PluginProvider{}, new(multierror.Error)
for idx, s := range h.secrets {
if s == nil {
continue
}
tmplFile := fmt.Sprintf("temp-%d", idx)
switch s.Provider {
case secrets.SecretProviderNomad:
if p, err := secrets.NewNomadProvider(s, secretDir, tmplFile, h.nomadNamespace); err != nil {
multierror.Append(mErr, err)
} else {
tmplProvider = append(tmplProvider, p)
}
case secrets.SecretProviderVault:
if p, err := secrets.NewVaultProvider(s, secretDir, tmplFile); err != nil {
multierror.Append(mErr, err)
} else {
tmplProvider = append(tmplProvider, p)
}
default:
plug, err := commonplugins.NewExternalSecretsPlugin(h.clientConfig.CommonPluginDir, s.Provider, s.Env)
if err != nil {
multierror.Append(mErr, err)
continue
}
pluginProvider = append(pluginProvider, secrets.NewExternalPluginProvider(plug, s.Name, s.Path))
}
}
return tmplProvider, pluginProvider, mErr.ErrorOrNil()
}