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

test: add context-aware integration tests #399

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
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,6 @@ k8s-openapi = { version = "0.20.0", default-features = false, features = [
rstest = "0.18"
test-context = "0.1"
tempfile = "3.8.1"
tower-test = "0.4"
# kube-rs mocking requires tower-test v0.14.x
hyper = { version = "^0.14.28" }
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ test: fmt lint
cargo test --workspace

.PHONY: unit-tests
unit-test: fmt lint
unit-tests: fmt lint
cargo test --workspace --lib

.PHONY: integration-test
.PHONY: integration-tests
integration-tests: fmt lint
cargo test --test '*'

Expand Down
45 changes: 45 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use tempfile::TempDir;

use policy_evaluator::{
evaluation_context::EvaluationContext, policy_evaluator::PolicyEvaluator,
policy_evaluator::PolicyExecutionMode, policy_evaluator_builder::PolicyEvaluatorBuilder,
};
use policy_fetcher::{policy::Policy, PullDestination};

pub(crate) async fn fetch_policy(policy_uri: &str, tempdir: TempDir) -> Policy {
policy_evaluator::policy_fetcher::fetch_policy(
policy_uri,
PullDestination::LocalFile(tempdir.into_path()),
None,
)
.await
.expect("cannot fetch policy")
}

pub(crate) fn build_policy_evaluator(
execution_mode: PolicyExecutionMode,
policy: &Policy,
eval_ctx: &EvaluationContext,
) -> PolicyEvaluator {
let policy_evaluator_builder = PolicyEvaluatorBuilder::new()
.execution_mode(execution_mode)
.policy_file(&policy.local_path)
.expect("cannot read policy file")
.enable_wasmtime_cache()
.enable_epoch_interruptions(1, 2);

let policy_evaluator_pre = policy_evaluator_builder
.build_pre()
.expect("cannot build policy evaluator pre");

policy_evaluator_pre
.rehydrate(eval_ctx)
.expect("cannot rehydrate policy evaluator")
}

pub(crate) fn load_request_data(request_file_name: &str) -> Vec<u8> {
let request_file_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/data")
.join(request_file_name);
std::fs::read(request_file_path).expect("cannot read request file")
flavio marked this conversation as resolved.
Show resolved Hide resolved
}
130 changes: 130 additions & 0 deletions tests/data/app_deployment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
{
"kind": {
"group": "apps",
"kind": "Deployment",
"version": "v1"
},
"name": "api",
"namespace": "customer-1",
"object": {
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"annotations": {
"deployment.kubernetes.io/revision": "1",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"api\",\"app.kubernetes.io/component\":\"api\"},\"name\":\"api\",\"namespace\":\"customer-1\"},\"spec\":{\"replicas\":3,\"selector\":{\"matchLabels\":{\"app\":\"api\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"api\",\"app.kubernetes.io/component\":\"api\"}},\"spec\":{\"containers\":[{\"image\":\"api:1.0.0\",\"name\":\"api\",\"ports\":[{\"containerPort\":8080}]}]}}}}\n"
},
"creationTimestamp": "2023-12-17T08:52:31Z",
"generation": 1,
"labels": {
"app": "api",
"app.kubernetes.io/component": "api",
"customer-id": "1"
},
"name": "api",
"namespace": "customer-1",
"resourceVersion": "1167496",
"uid": "8a1e598b-cc4b-49b7-a465-bee6204c18db"
},
"spec": {
"progressDeadlineSeconds": 600,
"replicas": 3,
"revisionHistoryLimit": 10,
"selector": {
"matchLabels": {
"app": "api"
}
},
"strategy": {
"rollingUpdate": {
"maxSurge": "25%",
"maxUnavailable": "25%"
},
"type": "RollingUpdate"
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"app": "api",
"app.kubernetes.io/component": "api"
}
},
"spec": {
"containers": [
{
"image": "api:1.0.0",
"imagePullPolicy": "IfNotPresent",
"name": "api",
"ports": [
{
"containerPort": 8080,
"protocol": "TCP"
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
}
],
"dnsPolicy": "ClusterFirst",
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"terminationGracePeriodSeconds": 30
}
}
},
"status": {
"conditions": [
{
"lastTransitionTime": "2023-12-17T08:52:32Z",
"lastUpdateTime": "2023-12-17T08:52:32Z",
"message": "Deployment does not have minimum availability.",
"reason": "MinimumReplicasUnavailable",
"status": "False",
"type": "Available"
},
{
"lastTransitionTime": "2023-12-17T08:52:31Z",
"lastUpdateTime": "2023-12-17T08:52:32Z",
"message": "ReplicaSet \"api-58f88975b6\" is progressing.",
"reason": "ReplicaSetUpdated",
"status": "True",
"type": "Progressing"
}
],
"observedGeneration": 1,
"replicas": 3,
"unavailableReplicas": 3,
"updatedReplicas": 3
}
},

"operation": "CREATE",
"options": {
"apiVersion": "meta.k8s.io/v1",
"fieldManager": "kubectl-client-side-apply",
"kind": "CreateOptions"
},
"requestKind": {
"group": "apps",
"kind": "Deployment",
"version": "v1"
},
"requestResource": {
"group": "apps",
"resource": "deployments",
"version": "v1"
},
"resource": {
"group": "apps",
"resource": "deployments",
"version": "v1"
},
"uid": "8560a482-887d-49b2-8781-415d04a0dcb0",
"userInfo": {
"groups": ["system:masters", "system:authenticated"],
"username": "system:admin"
}
}
134 changes: 106 additions & 28 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
#![allow(clippy::too_many_arguments)]
mod common;
mod k8s_mock;

use hyper::{Body, Request, Response};
use kube::Client;
use rstest::*;
use serde_json::json;
use std::collections::BTreeSet;
use std::future::Future;
use tokio::sync::oneshot;
use tower_test::mock::Handle;

use policy_evaluator::{
admission_request::AdmissionRequest,
admission_response::AdmissionResponseStatus,
evaluation_context::EvaluationContext,
policy_evaluator::PolicySettings,
policy_evaluator::{PolicyExecutionMode, ValidateRequest},
policy_evaluator_builder::PolicyEvaluatorBuilder,
policy_metadata::ContextAwareResource,
};
use policy_fetcher::PullDestination;

use crate::common::{build_policy_evaluator, fetch_policy, load_request_data};
use crate::k8s_mock::{rego_scenario, wapc_scenario};

#[rstest]
#[case::wapc(
Expand Down Expand Up @@ -147,38 +158,17 @@ async fn test_policy_evaluator(
#[case] mutating: bool,
) {
let tempdir = tempfile::TempDir::new().expect("cannot create tempdir");
let policy = policy_evaluator::policy_fetcher::fetch_policy(
policy_uri,
PullDestination::LocalFile(tempdir.into_path()),
None,
)
.await
.expect("cannot fetch policy");
let policy = fetch_policy(policy_uri, tempdir).await;

let eval_ctx = EvaluationContext {
policy_id: "test".to_string(),
policy_id: "test".to_owned(),
callback_channel: None,
ctx_aware_resources_allow_list: Default::default(),
};

let policy_evaluator_builder = PolicyEvaluatorBuilder::new()
.execution_mode(execution_mode)
.policy_file(&policy.local_path)
.expect("cannot read policy file")
.enable_wasmtime_cache()
.enable_epoch_interruptions(1, 2);

let policy_evaluator_pre = policy_evaluator_builder
.build_pre()
.expect("cannot build policy evaluator pre");
let mut policy_evaluator = policy_evaluator_pre
.rehydrate(&eval_ctx)
.expect("cannot rehydrate policy evaluator");

let request_file_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/data")
.join(request_file_path);
let request_data = std::fs::read(request_file_path).expect("cannot read request file");
let mut policy_evaluator = build_policy_evaluator(execution_mode, &policy, &eval_ctx);

let request_data = load_request_data(request_file_path);
let request_json = serde_json::from_slice(&request_data).expect("cannot deserialize request");

let validation_request = if raw {
Expand Down Expand Up @@ -214,3 +204,91 @@ async fn test_policy_evaluator(
assert!(admission_response.patch.is_none());
}
}

#[rstest]
#[case::wapc(
PolicyExecutionMode::KubewardenWapc,
"ghcr.io/kubewarden/tests/context-aware-test-policy:v0.1.0",
"app_deployment.json",
wapc_scenario
)]
#[case::opa(
PolicyExecutionMode::Opa,
"ghcr.io/kubewarden/tests/context-aware-test-opa-policy:v0.1.0",
"app_deployment.json",
rego_scenario
)]
#[case::gatekeeper(
PolicyExecutionMode::OpaGatekeeper,
"ghcr.io/kubewarden/tests/context-aware-test-gatekeeper-policy:v0.1.0",
"app_deployment.json",
rego_scenario
)]
fabriziosestito marked this conversation as resolved.
Show resolved Hide resolved
#[tokio::test(flavor = "multi_thread")]
async fn test_wapc_runtime_context_aware<F, Fut>(
#[case] execution_mode: PolicyExecutionMode,
#[case] policy_uri: &str,
#[case] request_file_path: &str,
#[case] scenario: F,
) where
F: FnOnce(Handle<Request<Body>, Response<Body>>) -> Fut,
Fut: Future<Output = ()>,
{
let tempdir = tempfile::TempDir::new().expect("cannot create tempdir");
let policy = fetch_policy(policy_uri, tempdir).await;

let (mocksvc, handle) = tower_test::mock::pair::<Request<Body>, Response<Body>>();
let client = Client::new(mocksvc, "default");
scenario(handle).await;

let (callback_handler_shutdown_channel_tx, callback_handler_shutdown_channel_rx) =
oneshot::channel();
let callback_builder = policy_evaluator::callback_handler::CallbackHandlerBuilder::new(
callback_handler_shutdown_channel_rx,
);
let mut callback_handler = callback_builder
.kube_client(client)
.build()
.expect("cannot build callback handler");
let callback_handler_channel = callback_handler.sender_channel();

tokio::spawn(async move {
callback_handler.loop_eval().await;
});

let eval_ctx = EvaluationContext {
policy_id: "test".to_owned(),
callback_channel: Some(callback_handler_channel),
ctx_aware_resources_allow_list: BTreeSet::from([
ContextAwareResource {
api_version: "v1".to_owned(),
kind: "Namespace".to_owned(),
},
ContextAwareResource {
api_version: "apps/v1".to_owned(),
kind: "Deployment".to_owned(),
},
ContextAwareResource {
api_version: "v1".to_owned(),
kind: "Service".to_owned(),
},
]),
};

let mut policy_evaluator = build_policy_evaluator(execution_mode, &policy, &eval_ctx);

let request_data = load_request_data(request_file_path);
let request: AdmissionRequest =
serde_json::from_slice(&request_data).expect("cannot deserialize request");

let admission_response = policy_evaluator.validate(
ValidateRequest::AdmissionRequest(request),
&PolicySettings::default(),
);

assert!(admission_response.allowed);

callback_handler_shutdown_channel_tx
.send(())
.expect("cannot send shutdown signal");
}
Loading
Loading