diff --git a/src/tools/genpolicy/src/cronjob.rs b/src/tools/genpolicy/src/cronjob.rs new file mode 100644 index 0000000000..f920095b41 --- /dev/null +++ b/src/tools/genpolicy/src/cronjob.rs @@ -0,0 +1,152 @@ +// Copyright (c) 2024 Microsoft Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Allow K8s YAML field names. +#![allow(non_snake_case)] + +use crate::job; +use crate::obj_meta; +use crate::pod; +use crate::policy; +use crate::settings; +use crate::utils::Config; +use crate::yaml; + +use async_trait::async_trait; +use protocols::agent; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +/// See Reference / Kubernetes API / Workload Resources / CronJob. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CronJob { + apiVersion: String, + kind: String, + metadata: obj_meta::ObjectMeta, + spec: CronJobSpec, + #[serde(skip)] + doc_mapping: serde_yaml::Value, +} + +/// See Reference / Kubernetes API / Workload Resources / CronJob. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CronJobSpec { + jobTemplate: JobTemplateSpec, + + #[serde(skip_serializing_if = "Option::is_none")] + concurrencyPolicy: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + failedJobsHistoryLimit: Option, + + schedule: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + startingDeadlineSeconds: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + successfulJobsHistoryLimit: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + suspend: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + timeZone: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + backoffLimit: Option, + // TODO: additional fields. +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct JobTemplateSpec { + #[serde(skip_serializing_if = "Option::is_none")] + metadata: Option, + spec: job::JobSpec, +} + +#[async_trait] +impl yaml::K8sResource for CronJob { + async fn init( + &mut self, + config: &Config, + doc_mapping: &serde_yaml::Value, + _silent_unsupported_fields: bool, + ) { + yaml::k8s_resource_init(&mut self.spec.jobTemplate.spec.template.spec, config).await; + self.doc_mapping = doc_mapping.clone(); + } + + fn get_sandbox_name(&self) -> Option { + None + } + + fn get_namespace(&self) -> Option { + self.metadata.get_namespace() + } + + fn get_container_mounts_and_storages( + &self, + policy_mounts: &mut Vec, + storages: &mut Vec, + container: &pod::Container, + settings: &settings::Settings, + ) { + if let Some(volumes) = &self.spec.jobTemplate.spec.template.spec.volumes { + yaml::get_container_mounts_and_storages( + policy_mounts, + storages, + container, + settings, + volumes, + ); + } + } + + fn generate_policy(&self, agent_policy: &policy::AgentPolicy) -> String { + agent_policy.generate_policy(self) + } + + fn serialize(&mut self, policy: &str) -> String { + yaml::add_policy_annotation( + &mut self.doc_mapping, + "spec.jobTemplate.spec.template", + policy, + ); + serde_yaml::to_string(&self.doc_mapping).unwrap() + } + + fn get_containers(&self) -> &Vec { + &self.spec.jobTemplate.spec.template.spec.containers + } + + fn get_annotations(&self) -> &Option> { + if let Some(metadata) = &self.spec.jobTemplate.spec.template.metadata { + return &metadata.annotations; + } + &None + } + + fn use_host_network(&self) -> bool { + if let Some(host_network) = self.spec.jobTemplate.spec.template.spec.hostNetwork { + return host_network; + } + false + } + + fn use_sandbox_pidns(&self) -> bool { + if let Some(shared) = self + .spec + .jobTemplate + .spec + .template + .spec + .shareProcessNamespace + { + return shared; + } + false + } +} diff --git a/src/tools/genpolicy/src/job.rs b/src/tools/genpolicy/src/job.rs index ce536e98e1..72d7efd2d4 100644 --- a/src/tools/genpolicy/src/job.rs +++ b/src/tools/genpolicy/src/job.rs @@ -34,7 +34,7 @@ pub struct Job { /// See Reference / Kubernetes API / Workload Resources / Job. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct JobSpec { - template: pod_template::PodTemplateSpec, + pub template: pod_template::PodTemplateSpec, #[serde(skip_serializing_if = "Option::is_none")] backoffLimit: Option, diff --git a/src/tools/genpolicy/src/main.rs b/src/tools/genpolicy/src/main.rs index 29ca79ba84..db17060491 100644 --- a/src/tools/genpolicy/src/main.rs +++ b/src/tools/genpolicy/src/main.rs @@ -7,6 +7,7 @@ use log::{debug, info}; mod config_map; mod containerd; +mod cronjob; mod daemon_set; mod deployment; mod job; diff --git a/src/tools/genpolicy/src/yaml.rs b/src/tools/genpolicy/src/yaml.rs index 45e4388a27..dd51f1117e 100644 --- a/src/tools/genpolicy/src/yaml.rs +++ b/src/tools/genpolicy/src/yaml.rs @@ -7,6 +7,7 @@ #![allow(non_snake_case)] use crate::config_map; +use crate::cronjob; use crate::daemon_set; use crate::deployment; use crate::job; @@ -163,6 +164,14 @@ pub fn new_k8s_resource( debug!("{:#?}", &job); Ok((boxed::Box::new(job), header.kind)) } + "CronJob" => { + let cronJob: cronjob::CronJob = serde_ignored::deserialize(d, |path| { + handle_unused_field(&path.to_string(), silent_unsupported_fields); + }) + .unwrap(); + debug!("{:#?}", &cronJob); + Ok((boxed::Box::new(cronJob), header.kind)) + } "List" => { let list: list::List = serde_ignored::deserialize(d, |path| { handle_unused_field(&path.to_string(), silent_unsupported_fields); diff --git a/tests/integration/kubernetes/k8s-cron-job.bats b/tests/integration/kubernetes/k8s-cron-job.bats new file mode 100644 index 0000000000..a40871eecd --- /dev/null +++ b/tests/integration/kubernetes/k8s-cron-job.bats @@ -0,0 +1,59 @@ +#!/usr/bin/env bats +# +# Copyright (c) 2019 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 +# + +load "${BATS_TEST_DIRNAME}/../../common.bash" +load "${BATS_TEST_DIRNAME}/tests_common.sh" + +setup() { + get_pod_config_dir + job_name="cron-job-pi-test" + yaml_file="${pod_config_dir}/cron-job.yaml" + + policy_settings_dir="$(create_tmp_policy_settings_dir "${pod_config_dir}")" + add_requests_to_policy_settings "${policy_settings_dir}" "ReadStreamRequest" + auto_generate_policy "${policy_settings_dir}" "${yaml_file}" +} + +@test "Run a cron job to completion" { + # Create cron job + kubectl apply -f "${yaml_file}" + + # Verify job + waitForProcess "$wait_time" "$sleep_time" "kubectl describe cronjobs.batch $job_name | grep SuccessfulCreate" + + # List pods that belong to the cron-job + pod_name=$(kubectl get pods --no-headers -o custom-columns=":metadata.name" | grep '^cron-job-pi-test' | head -n 1) + + # Verify that the job is completed + cmd="kubectl get pods -o jsonpath='{.items[*].status.phase}' | grep Succeeded" + waitForProcess "$wait_time" "$sleep_time" "$cmd" + + # Verify the output of the pod + pi_number="3.14" + kubectl logs "$pod_name" | grep "$pi_number" +} + +teardown() { + # Debugging information + kubectl describe pod "$pod_name" + kubectl describe cronjobs.batch/"$job_name" + + # Clean-up + + kubectl delete cronjobs.batch/"$job_name" + # Verify that the job is not running + run kubectl get cronjobs.batch + echo "$output" + [[ "$output" =~ "No resources found" ]] + + # Verify that pod is not running + run kubectl get pods + echo "$output" + [[ "$output" =~ "No resources found" ]] + + delete_tmp_policy_settings_dir "${policy_settings_dir}" +} diff --git a/tests/integration/kubernetes/run_kubernetes_tests.sh b/tests/integration/kubernetes/run_kubernetes_tests.sh index 51f33ac482..2cbd9a0efa 100755 --- a/tests/integration/kubernetes/run_kubernetes_tests.sh +++ b/tests/integration/kubernetes/run_kubernetes_tests.sh @@ -37,6 +37,7 @@ else "k8s-copy-file.bats" \ "k8s-cpu-ns.bats" \ "k8s-credentials-secrets.bats" \ + "k8s-cron-job.bats" \ "k8s-custom-dns.bats" \ "k8s-empty-dirs.bats" \ "k8s-env.bats" \ diff --git a/tests/integration/kubernetes/runtimeclass_workloads/cron-job.yaml b/tests/integration/kubernetes/runtimeclass_workloads/cron-job.yaml new file mode 100644 index 0000000000..56004117ea --- /dev/null +++ b/tests/integration/kubernetes/runtimeclass_workloads/cron-job.yaml @@ -0,0 +1,15 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: cron-job-pi-test +spec: + schedule: "* * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: pi + image: quay.io/prometheus/busybox:latest + command: ["/bin/sh", "-c", "echo 'scale=5; 4*a(1)' | bc -l"] + restartPolicy: OnFailure \ No newline at end of file diff --git a/tests/integration/kubernetes/setup.sh b/tests/integration/kubernetes/setup.sh index 1f829d1ef0..907ad636c1 100644 --- a/tests/integration/kubernetes/setup.sh +++ b/tests/integration/kubernetes/setup.sh @@ -77,6 +77,13 @@ add_annotations_to_yaml() { "${K8S_TEST_YAML}" ;; + CronJob) + info "Adding \"${annotation_name}=${annotation_value}\" to ${resource_kind} from ${yaml_file}" + yq -i \ + ".spec.jobTemplate.spec.template.metadata.annotations.\"${annotation_name}\" = \"${annotation_value}\"" \ + "${K8S_TEST_YAML}" + ;; + List) info "Issue #7765: adding annotations to ${resource_kind} from ${yaml_file} is not implemented yet" ;;