adds implied secrets constraint to job hook (#26328)

This commit is contained in:
Michael Smithhisler
2025-07-24 14:07:28 -04:00
parent ac32b0864d
commit c7a6b8b253
3 changed files with 164 additions and 1 deletions

View File

@@ -279,6 +279,8 @@ func (jobImpliedConstraints) Mutate(j *structs.Job) (*structs.Job, []error, erro
taskScheduleTaskGroups := j.RequiredScheduleTask()
secretBlocks := j.Secrets()
// Hot path where none of our things require constraints.
//
// [UPDATE THIS] if you are adding a new constraint thing!
@@ -286,7 +288,8 @@ func (jobImpliedConstraints) Mutate(j *structs.Job) (*structs.Job, []error, erro
nativeServiceDisco.Empty() && len(consulServiceDisco) == 0 &&
numaTaskGroups.Empty() && bridgeNetworkingTaskGroups.Empty() &&
transparentProxyTaskGroups.Empty() &&
taskScheduleTaskGroups.Empty() {
taskScheduleTaskGroups.Empty() &&
len(secretBlocks) == 0 {
return j, nil, nil
}
@@ -302,6 +305,12 @@ func (jobImpliedConstraints) Mutate(j *structs.Job) (*structs.Job, []error, erro
mutateConstraint(constraintMatcherLeft, tg, vaultConstraintFn(vaultBlock))
}
for _, secretProviders := range secretBlocks {
for _, provider := range secretProviders {
mutateConstraint(constraintMatcherLeft, tg, secretsConstraintFn(provider))
}
}
// If the task group utilizes NUMA resources, run the mutator.
if numaTaskGroups.Contains(tg.Name) {
mutateConstraint(constraintMatcherFull, tg, numaVersionConstraint)
@@ -382,6 +391,17 @@ func vaultConstraintFn(vault *structs.Vault) *structs.Constraint {
return vaultConstraint
}
// secretsConstraintFn returns a constraint that checks for the existence of a
// fingerprinted secrets plugin. This is to support upgrades to 1.11 where a nomad
// server follower may not be upgraded yet and attempt to place a job on a client
// that has not been upgraded. This should be removed in Nomad 1.14.
func secretsConstraintFn(provider string) *structs.Constraint {
return &structs.Constraint{
LTarget: fmt.Sprintf("${attr.plugins.secrets.%s.version}", provider),
Operand: structs.ConstraintAttributeIsSet,
}
}
// consulConstraintFn returns a service discovery constraint that matches the
// fingerprint of the requested Consul cluster. This is to support Nomad
// Enterprise but neither the fingerprint or non-default cluster are allowed

View File

@@ -1280,6 +1280,122 @@ func Test_jobImpliedConstraints_Mutate(t *testing.T) {
expectedOutputWarnings: nil,
expectedOutputError: nil,
},
{
name: "task with secret",
inputJob: &structs.Job{
Name: "example",
TaskGroups: []*structs.TaskGroup{
{
Name: "group-with-secret",
Tasks: []*structs.Task{
{
Name: "task-with-secret",
Secrets: []*structs.Secret{
{
Provider: "test",
},
},
},
},
},
},
},
expectedOutputJob: &structs.Job{
Name: "example",
TaskGroups: []*structs.TaskGroup{
{
Name: "group-with-secret",
Tasks: []*structs.Task{
{
Name: "task-with-secret",
Secrets: []*structs.Secret{
{
Provider: "test",
},
},
},
},
Constraints: []*structs.Constraint{
{
LTarget: "${attr.plugins.secrets.test.version}",
Operand: structs.ConstraintAttributeIsSet,
},
},
},
},
},
},
{
name: "tasks with overlapping secrets",
inputJob: &structs.Job{
Name: "example",
TaskGroups: []*structs.TaskGroup{
{
Name: "group-with-secret",
Tasks: []*structs.Task{
{
Name: "task-with-secret",
Secrets: []*structs.Secret{
{
Provider: "foo",
},
},
},
{
Name: "task-with-secret",
Secrets: []*structs.Secret{
{
Provider: "foo",
},
{
Provider: "bar",
},
},
},
},
},
},
},
expectedOutputJob: &structs.Job{
Name: "example",
TaskGroups: []*structs.TaskGroup{
{
Name: "group-with-secret",
Tasks: []*structs.Task{
{
Name: "task-with-secret",
Secrets: []*structs.Secret{
{
Provider: "foo",
},
},
},
{
Name: "task-with-secret",
Secrets: []*structs.Secret{
{
Provider: "foo",
},
{
Provider: "bar",
},
},
},
},
Constraints: []*structs.Constraint{
{
LTarget: "${attr.plugins.secrets.foo.version}",
Operand: structs.ConstraintAttributeIsSet,
},
{
LTarget: "${attr.plugins.secrets.bar.version}",
Operand: structs.ConstraintAttributeIsSet,
},
},
},
},
},
},
}
for _, tc := range testCases {

View File

@@ -5096,6 +5096,33 @@ func (j *Job) Vault() map[string]map[string]*Vault {
return blocks
}
// Secrets returns the set of secrets per task group, per task
func (j *Job) Secrets() map[string][]string {
blocks := make(map[string][]string, len(j.TaskGroups))
for _, tg := range j.TaskGroups {
secrets := []string{}
for _, task := range tg.Tasks {
if len(task.Secrets) == 0 {
continue
}
for _, s := range task.Secrets {
if !slices.Contains(secrets, s.Provider) {
secrets = append(secrets, s.Provider)
}
}
}
if len(secrets) != 0 {
blocks[tg.Name] = secrets
}
}
return blocks
}
// ConnectTasks returns the set of Consul Connect enabled tasks defined on the
// job that will require a Service Identity token in the case that Consul ACLs
// are enabled. The TaskKind.Value is the name of the Consul service.