Skip to content

Commit

Permalink
test: add context-aware integration tests
Browse files Browse the repository at this point in the history
Signed-off-by: Fabrizio Sestito <fabrizio.sestito@suse.com>
  • Loading branch information
fabriziosestito committed Dec 19, 2023
1 parent 29cb050 commit 443bd1d
Show file tree
Hide file tree
Showing 6 changed files with 468 additions and 27 deletions.
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" }
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")
}
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"
}
}
132 changes: 105 additions & 27 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(),
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/fabriziosestito/policies/context-aware-test-policy:latest",
"app_deployment.json",
wapc_scenario
)]
#[case::opa(
PolicyExecutionMode::Opa,
"ghcr.io/fabriziosestito/policies/context-aware-test-opa-policy:latest",
"app_deployment.json",
rego_scenario
)]
#[case::gatekeeper(
PolicyExecutionMode::OpaGatekeeper,
"ghcr.io/fabriziosestito/policies/context-aware-test-gatekeeper-policy:latest",
"app_deployment.json",
rego_scenario
)]
#[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_string(),
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

0 comments on commit 443bd1d

Please sign in to comment.