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 Oct 31, 2023
1 parent 7200fd2 commit 0f030b0
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 6 deletions.
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 := {r | some r in required}
tsk := {t | some t in 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])
72 changes: 72 additions & 0 deletions policy/release/tasks_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,78 @@ test_invalid_status_conditions if {
lib.assert_equal(["MISSING"], tasks._status(given_task))
}

test_one_of_required_tasks if {
attestations := _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 attestations
}

test_one_of_required_tasks_missing if {
attestations := _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 attestations
}

test_future_one_of_required_tasks if {
attestations := _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 attestations
}

test_future_one_of_required_tasks_missing if {
attestations := _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 attestations
}

_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 0f030b0

Please sign in to comment.