Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support _one of_ for required tasks #789

Merged
merged 1 commit into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use a helper method to avoid having to implement the logic for both cases, e.g.

_any_missing(required, tasks) := missing if {
  req := setfy(required)
  tsk := setfy(tasks)
  ...
}

setfy(o) := s if {
  s := {v | some v in o}
} else := {o}

This could maybe reside in policy/lib/set_helpers.rego.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out we already have that helper there :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would've been nice to unify the two use cases (array vs non-array) which is what I was trying to drive at with this comment. But anyways, the current approach works just as well 😉


# 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