Skip to content

Commit

Permalink
Support _one of_ for required tasks
Browse files Browse the repository at this point in the history
This adds support for the notion of _one of_ required tasks from the
required tasks list.

For example, given required tasks data as:

```yaml
 - A
 - [B, C, D]
 - E
```

Tasks "A", at least one of "B", "C" or "D", and "E" are required.
  • Loading branch information
zregvart committed Nov 2, 2023
1 parent 178cfa0 commit d4e9abf
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 12 deletions.
43 changes: 37 additions & 6 deletions policy/pipeline/required_tasks.rego
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ warn contains result if {
# in the Pipeline definition.
# custom:
# short_name: missing_required_task
# failure_msg: Required task %q is missing
# failure_msg: '%s is missing'
deny contains result if {
count(tkn.tasks(input)) > 0

Expand All @@ -63,7 +63,7 @@ deny contains result if {

# Don't report an error if a task is required now, but not in the future
required_task in latest_required_tasks
result := lib.result_helper_with_term(rego.metadata.chain(), [required_task], required_task)
result := lib.result_helper_with_term(rego.metadata.chain(), [_format_missing(required_task, false)], required_task)
}

# METADATA
Expand All @@ -73,7 +73,7 @@ deny contains result if {
# is not currently included in the Pipeline definition.
# custom:
# short_name: missing_future_required_task
# failure_msg: Task %q is missing and will be required in the future
# failure_msg: '%s is missing and will be required in the future'
warn contains result if {
count(tkn.tasks(input)) > 0

Expand All @@ -83,7 +83,7 @@ warn contains result if {
# If the required_task is also part of the current_required_tasks, do
# not proceed with a warning since that's clearly a violation.
not required_task in current_required_tasks
result := lib.result_helper_with_term(rego.metadata.chain(), [required_task], required_task)
result := lib.result_helper_with_term(rego.metadata.chain(), [_format_missing(required_task, true)], required_task)
}

# METADATA
Expand All @@ -103,10 +103,31 @@ deny contains result if {
# _missing_tasks returns a set of task names that are in the given
# required_tasks, but not in the pipeline definition.
_missing_tasks(required_tasks) := {task |
some task in required_tasks
not task in tkn.tasks_names(input)
some required_task in required_tasks
some task in _any_missing(required_task, tkn.tasks_names(input))
}

_any_missing(required, tasks) := missing if {
# one of required tasks is required
is_array(required)

# convert arrays to sets so we can intersect below
req := lib.to_set(required)
tsk := lib.to_set(tasks)
count(req & tsk) == 0

# no required tasks are in tasks
missing := [required]
} else := missing if {
# above could be false, so we need to doublecheck that we're not dealing
# with an array
not is_array(required)
missing := {required |
# a required task was not found in tasks
not required in tasks
}
} else := {}

# get the future tasks that are pipeline specific. If none exists
# get the default list
latest_required_tasks := task_data if {
Expand All @@ -122,3 +143,13 @@ current_required_tasks := task_data if {
} else := task_data if {
task_data := tkn.current_required_default_tasks
}

# given an array a nice message saying one of the elements of the array,
# otherwise the quoted value
_format_missing(o, opt) := desc if {
is_array(o)
desc := sprintf(`One of "%s" tasks`, [concat(`", "`, o)])
} else := msg if {
opt
msg := sprintf("Task %q", [o])
} else := sprintf("Required task %q", [o])
72 changes: 72 additions & 0 deletions policy/pipeline/required_tasks_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,78 @@ test_missing_required_tasks_data if {
with input as pipeline
}

test_one_of_required_tasks if {
pipeline := _pipeline_with_tasks_and_label(["a", "b", "c1", "d2", "e", "f"], [], [])
data_required_tasks := {"fbc": [{
"tasks": {"a", ["c1", "c2", "c3"], ["d1", "d2", "d3"], ["e"]},
"effective_on": "2009-01-02T00:00:00Z",
}]}
lib.assert_empty(required_tasks.deny) with data["pipeline-required-tasks"] as data_required_tasks
with input as pipeline
}

test_one_of_required_tasks_missing if {
pipeline := _pipeline_with_tasks_and_label(["a", "b", "d2", "e", "f"], [], [])

data_required_tasks := {"fbc": [{
"tasks": {"a", ["c1", "c2", "c3"], ["d1", "d3"]},
"effective_on": "2009-01-02T00:00:00Z",
}]}

expected := {
{
"code": "required_tasks.missing_required_task",
"msg": `One of "c1", "c2", "c3" tasks is missing`,
"term": ["c1", "c2", "c3"],
},
{
"code": "required_tasks.missing_required_task",
"msg": `One of "d1", "d3" tasks is missing`,
"term": ["d1", "d3"],
},
}

lib.assert_equal_results(expected, required_tasks.deny) with data["pipeline-required-tasks"] as data_required_tasks
with input as pipeline
}

test_future_one_of_required_tasks if {
pipeline := _pipeline_with_tasks_and_label(["a", "b", "c1", "d2", "e", "f"], [], [])
data_required_tasks := {"fbc": [{
"tasks": {"a", ["c1", "c2", "c3"], ["d1", "d2", "d3"], ["e"]},
"effective_on": "2099-01-02T00:00:00Z",
}]}
lib.assert_empty(required_tasks.warn) with data["pipeline-required-tasks"] as data_required_tasks
with input as pipeline
}

test_future_one_of_required_tasks_missing if {
pipeline := _pipeline_with_tasks_and_label(["a", "b", "d2", "e", "f"], [], [])

data_required_tasks := {"fbc": [{
"tasks": {"a", ["c1", "c2", "c3"], ["d1", "d3"]},
"effective_on": "2099-01-02T00:00:00Z",
}]}

expected := {
{
"code": "required_tasks.missing_future_required_task",
"msg": `One of "c1", "c2", "c3" tasks is missing and will be required in the future`,
"term": ["c1", "c2", "c3"],
},
{
"code": "required_tasks.missing_future_required_task",
"msg": `One of "d1", "d3" tasks is missing and will be required in the future`,
"term": ["d1", "d3"],
},
}
lib.assert_equal_results(
expected,
required_tasks.warn,
) with data["pipeline-required-tasks"] as data_required_tasks
with input as pipeline
}

_pipeline_with_tasks_and_label(names, finally_names, add_tasks) := pipeline if {
tasks := array.concat([t | some name in names; t := _task(name)], add_tasks)
finally_tasks := [t | some name in finally_names; t := _task(name)]
Expand Down
43 changes: 37 additions & 6 deletions policy/release/tasks.rego
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ deny contains result if {
# in the PipelineRun attestation.
# custom:
# short_name: required_tasks_found
# failure_msg: Required task %q is missing
# failure_msg: '%s is missing'
# solution: >-
# Make sure all required tasks are in the build pipeline. The required task list
# is contained as xref:ec-cli:ROOT:configuration.adoc#_data_sources[data] under the key 'required-tasks'.
Expand All @@ -92,7 +92,7 @@ deny contains result if {

# Don't report an error if a task is required now, but not in the future
required_task in latest_required_tasks
result := lib.result_helper_with_term(rego.metadata.chain(), [required_task], required_task)
result := lib.result_helper_with_term(rego.metadata.chain(), [_format_missing(required_task, false)], required_task)
}

# METADATA
Expand Down Expand Up @@ -122,7 +122,7 @@ warn contains result if {
# was not included in the PipelineRun attestation.
# custom:
# short_name: future_required_tasks_found
# failure_msg: Task %q is missing and will be required in the future
# failure_msg: '%s is missing and will be required in the future'
# solution: >-
# There is a task that will be required at a future date that is missing
# from the build pipeline.
Expand All @@ -137,7 +137,7 @@ warn contains result if {
# If the required_task is also part of the current_required_tasks, do
# not proceed with a warning since that's clearly a violation.
not required_task in current_required_tasks
result := lib.result_helper_with_term(rego.metadata.chain(), [required_task], required_task)
result := lib.result_helper_with_term(rego.metadata.chain(), [_format_missing(required_task, true)], required_task)
}

# METADATA
Expand Down Expand Up @@ -169,10 +169,31 @@ _missing_tasks(required_tasks) := {task |
some att in lib.pipelinerun_attestations
count(tkn.tasks(att)) > 0

some task in required_tasks
not task in tkn.tasks_names(att)
some required_task in required_tasks
some task in _any_missing(required_task, tkn.tasks_names(att))
}

_any_missing(required, tasks) := missing if {
# one of required tasks is required
is_array(required)

# convert arrays to sets so we can intersect below
req := lib.to_set(required)
tsk := lib.to_set(tasks)
count(req & tsk) == 0

# no required tasks are in tasks
missing := [required]
} else := missing if {
# above could be false, so we need to doublecheck that we're not dealing
# with an array
not is_array(required)
missing := {required |
# a required task was not found in tasks
not required in tasks
}
} else := {}

# get the future tasks that are pipeline specific. If none exists
# get the default list
latest_required_tasks := task_data if {
Expand Down Expand Up @@ -229,3 +250,13 @@ _slsav1_status(condition) := status if {
condition.status == "False"
status := "Failed"
}

# given an array a nice message saying one of the elements of the array,
# otherwise the quoted value
_format_missing(o, opt) := desc if {
is_array(o)
desc := sprintf(`One of "%s" tasks`, [concat(`", "`, o)])
} else := msg if {
opt
msg := sprintf("Task %q", [o])
} else := sprintf("Required task %q", [o])
91 changes: 91 additions & 0 deletions policy/release/tasks_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,97 @@ test_invalid_status_conditions if {
lib.assert_equal(["MISSING"], tasks._status(given_task))
}

test_one_of_required_tasks if {
attestation_v02 := _attestations_with_tasks(["a", "b", "c1", "d2", "e", "f"], [])
data_required_tasks := {"generic": [{
"tasks": {"a", ["c1", "c2", "c3"], ["d1", "d2", "d3"], ["e"]},
"effective_on": "2009-01-02T00:00:00Z",
}]}
lib.assert_empty(tasks.deny) with data["pipeline-required-tasks"] as data_required_tasks
with input.attestations as attestation_v02

attestation_v1 := _slsav1_attestations_with_tasks(["a", "b", "c1", "d2", "e", "f"], [])
lib.assert_empty(tasks.deny) with data["pipeline-required-tasks"] as data_required_tasks
with input.attestations as attestation_v1
}

test_one_of_required_tasks_missing if {
attestation_v02 := _attestations_with_tasks(["a", "b", "d2", "e", "f"], [])

data_required_tasks := {"generic": [{
"tasks": {"a", ["c1", "c2", "c3"], ["d1", "d3"]},
"effective_on": "2009-01-02T00:00:00Z",
}]}

expected := {
{
"code": "tasks.required_tasks_found",
"msg": `One of "c1", "c2", "c3" tasks is missing`,
"term": ["c1", "c2", "c3"],
},
{
"code": "tasks.required_tasks_found",
"msg": `One of "d1", "d3" tasks is missing`,
"term": ["d1", "d3"],
},
}

lib.assert_equal_results(expected, tasks.deny) with data["pipeline-required-tasks"] as data_required_tasks
with input.attestations as attestation_v02

attestation_v1 := _slsav1_attestations_with_tasks(["a", "b", "d2", "e", "f"], [])
lib.assert_equal_results(expected, tasks.deny) with data["pipeline-required-tasks"] as data_required_tasks
with input.attestations as attestation_v1
}

test_future_one_of_required_tasks if {
attestation_v02 := _attestations_with_tasks(["a", "b", "c1", "d2", "e", "f"], [])
data_required_tasks := {"generic": [{
"tasks": {"a", ["c1", "c2", "c3"], ["d1", "d2", "d3"], ["e"]},
"effective_on": "2099-01-02T00:00:00Z",
}]}
lib.assert_empty(tasks.warn) with data["pipeline-required-tasks"] as data_required_tasks
with input.attestations as attestation_v02

attestation_v1 := _slsav1_attestations_with_tasks(["a", "b", "c1", "d2", "e", "f"], [])
lib.assert_empty(tasks.warn) with data["pipeline-required-tasks"] as data_required_tasks
with input.attestations as attestation_v1
}

test_future_one_of_required_tasks_missing if {
attestation_v02 := _attestations_with_tasks(["a", "b", "d2", "e", "f"], [])

data_required_tasks := {"generic": [{
"tasks": {"a", ["c1", "c2", "c3"], ["d1", "d3"]},
"effective_on": "2099-01-02T00:00:00Z",
}]}

expected := {
{
"code": "tasks.future_required_tasks_found",
"msg": `One of "c1", "c2", "c3" tasks is missing and will be required in the future`,
"term": ["c1", "c2", "c3"],
},
{
"code": "tasks.future_required_tasks_found",
"msg": `One of "d1", "d3" tasks is missing and will be required in the future`,
"term": ["d1", "d3"],
},
}
lib.assert_equal_results(
expected,
tasks.warn,
) with data["pipeline-required-tasks"] as data_required_tasks
with input.attestations as attestation_v02

attestation_v1 := _slsav1_attestations_with_tasks(["a", "b", "d2", "e", "f"], [])
lib.assert_equal_results(
expected,
tasks.warn,
) with data["pipeline-required-tasks"] as data_required_tasks
with input.attestations as attestation_v1
}

_attestations_with_tasks(names, add_tasks) := attestations if {
tasks := array.concat([t | some name in names; t := _task(name)], add_tasks)

Expand Down

0 comments on commit d4e9abf

Please sign in to comment.