Merge pull request #10986 from 3u13r/euler/feat/genpolicy/env-from-secret

genpolicy: support secrets to be referenced for pod envs
This commit is contained in:
Markus Rudy 2025-05-09 13:29:35 +02:00 committed by GitHub
commit 835f59df2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 572 additions and 45 deletions

View File

@ -41,6 +41,12 @@ dependencies = [
"libc",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anyhow"
version = "1.0.69"
@ -53,6 +59,22 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "assert_cmd"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66"
dependencies = [
"anstyle",
"bstr",
"doc-comment",
"libc",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "async-trait"
version = "0.1.68"
@ -169,6 +191,17 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bstr"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.12.0"
@ -451,6 +484,12 @@ dependencies = [
"syn 2.0.58",
]
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.10.6"
@ -462,6 +501,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "docker_credential"
version = "1.3.1"
@ -699,6 +744,7 @@ name = "genpolicy"
version = "0.1.0"
dependencies = [
"anyhow",
"assert_cmd",
"async-trait",
"base64 0.21.7",
"clap",
@ -1580,6 +1626,33 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "predicates"
version = "3.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
dependencies = [
"anstyle",
"difflib",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
[[package]]
name = "predicates-tree"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "prettyplease"
version = "0.1.25"
@ -2351,6 +2424,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "termtree"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
[[package]]
name = "thiserror"
version = "1.0.40"
@ -2695,6 +2774,15 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wait-timeout"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
dependencies = [
"libc",
]
[[package]]
name = "want"
version = "0.3.0"

View File

@ -40,7 +40,9 @@ serde_yaml = "0.8"
anyhow = "1.0.32"
async-trait = "0.1.68"
docker_credential = "1.3.1"
flate2 = { version = "1.0.26", features = ["zlib-ng"], default-features = false }
flate2 = { version = "1.0.26", features = [
"zlib-ng",
], default-features = false }
libz-ng-sys = "1.1.15" # force newer version that compiles on ppc64le
oci-client = { version = "0.12.0" }
openssl = { version = "0.10.72", features = ["vendored"] }
@ -76,3 +78,4 @@ tar = "0.4.41"
[dev-dependencies]
kata-agent-policy = { path = "../../agent/policy" }
slog = "2.5.2"
assert_cmd = "2.0.14"

View File

@ -13,10 +13,8 @@ use crate::utils::Config;
use crate::yaml;
use async_trait::async_trait;
use log::debug;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs::File;
/// See Reference / Kubernetes API / Config and Storage Resources / ConfigMap.
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -41,14 +39,6 @@ pub struct ConfigMap {
}
impl ConfigMap {
pub fn new(file: &str) -> anyhow::Result<Self> {
debug!("Reading ConfigMap...");
let config_map: ConfigMap = serde_yaml::from_reader(File::open(file)?)?;
debug!("\nRead ConfigMap => {:#?}", config_map);
Ok(config_map)
}
pub fn get_value(&self, value_from: &pod::EnvVarSource) -> Option<String> {
if let Some(key_ref) = &value_from.configMapKeyRef {
if let Some(name) = &key_ref.name {

View File

@ -438,6 +438,11 @@ pub struct SandboxData {
pub storages: Vec<agent::Storage>,
}
enum K8sEnvFromSource {
ConfigMap(config_map::ConfigMap),
Secret(secret::Secret),
}
impl AgentPolicy {
pub async fn from_files(config: &utils::Config) -> Result<AgentPolicy> {
let mut config_maps = Vec::new();
@ -488,9 +493,18 @@ impl AgentPolicy {
}
}
if let Some(config_map_files) = &config.config_map_files {
for file in config_map_files {
config_maps.push(config_map::ConfigMap::new(file)?);
if let Some(config_files) = &config.config_files {
for resource_file in config_files {
for config_resource in parse_config_file(resource_file.to_string(), config).await? {
match config_resource {
K8sEnvFromSource::ConfigMap(config_map) => {
config_maps.push(config_map);
}
K8sEnvFromSource::Secret(secret) => {
secrets.push(secret);
}
}
}
}
}
@ -822,6 +836,37 @@ fn get_image_layer_storages(
storages.push(overlay_storage);
}
async fn parse_config_file(
yaml_file: String,
config: &utils::Config,
) -> Result<Vec<K8sEnvFromSource>> {
let mut k8sRes = Vec::new();
let yaml_contents = yaml::get_input_yaml(&Some(yaml_file))?;
for document in serde_yaml::Deserializer::from_str(&yaml_contents) {
let doc_mapping = Value::deserialize(document)?;
if doc_mapping != Value::Null {
let yaml_string = serde_yaml::to_string(&doc_mapping)?;
let silent = config.silent_unsupported_fields;
let (mut resource, kind) = yaml::new_k8s_resource(&yaml_string, silent)?;
resource.init(config, &doc_mapping, silent).await;
// ConfigMap and Secret documents contain additional input for policy generation.
if kind.eq("ConfigMap") {
let config_map: config_map::ConfigMap = serde_yaml::from_str(&yaml_string)?;
debug!("{:#?}", &config_map);
k8sRes.push(K8sEnvFromSource::ConfigMap(config_map));
} else if kind.eq("Secret") {
let secret: secret::Secret = serde_yaml::from_str(&yaml_string)?;
debug!("{:#?}", &secret);
k8sRes.push(K8sEnvFromSource::Secret(secret));
}
}
}
Ok(k8sRes)
}
/// Converts the given name to a string representation of its sha256 hash.
fn name_to_hash(name: &str) -> String {
let mut hasher = Sha256::new();

View File

@ -18,10 +18,16 @@ struct CommandLineOptions {
#[clap(
short,
long,
help = "Optional Kubernetes config map YAML input file path"
help = "Optional Kubernetes config map YAML input file path. DEPRECATED: use --config-file instead"
)]
config_map_file: Option<String>,
#[clap(
long,
help = "Optional Kubernetes YAML input file path containing config resources such as ConfigMaps and Secrets"
)]
config_file: Option<Vec<String>>,
#[clap(
short = 'p',
long,
@ -111,7 +117,7 @@ pub struct Config {
pub yaml_file: Option<String>,
pub rego_rules_path: String,
pub settings: settings::Settings,
pub config_map_files: Option<Vec<String>>,
pub config_files: Option<Vec<String>>,
pub silent_unsupported_fields: bool,
pub raw_out: bool,
@ -125,16 +131,15 @@ impl Config {
pub fn new() -> Self {
let args = CommandLineOptions::parse();
let mut config_map_files = Vec::new();
if let Some(config_map_file) = &args.config_map_file {
config_map_files.push(config_map_file.clone());
}
// Migrate all files from the old `config_map_file` to the new `config_files` field
let config_files = args
.config_file
.unwrap_or_default()
.into_iter()
.chain(args.config_map_file.iter().cloned())
.collect::<Vec<_>>();
let cm_files = if !config_map_files.is_empty() {
Some(config_map_files.clone())
} else {
None
};
let config_files = (!config_files.is_empty()).then_some(config_files);
let mut layers_cache_file_path = args.layers_cache_file_path;
// preserve backwards compatibility for only using the `use_cached_files` flag
@ -151,7 +156,7 @@ impl Config {
yaml_file: args.yaml_file,
rego_rules_path: args.rego_rules_path,
settings,
config_map_files: cm_files,
config_files,
silent_unsupported_fields: args.silent_unsupported_fields,
raw_out: args.raw_out,
base64_out: args.base64_out,

View File

@ -0,0 +1,99 @@
// Copyright (c) 2025 Edgeless Systems GmbH
//
// SPDX-License-Identifier: Apache-2.0
//
use assert_cmd::prelude::*;
use std::fs::{self};
use std::path;
use std::process::Command;
#[test]
fn config_map_in_separate_file_config_map_flag() -> Result<(), Box<dyn std::error::Error>> {
// Prepare temp dir for running genpolicy.
let test_case_dir = "config_map_separate_file_config_map_flag";
let pod_yaml_name = "pod_with_config_map_ref.yaml";
let config_file = "config_map.yaml";
let workdir = prepare_workdir(test_case_dir, &[pod_yaml_name, config_file]);
let mut cmd = Command::cargo_bin("genpolicy")?;
cmd.arg("--yaml-file").arg(workdir.join(pod_yaml_name));
cmd.assert().failure();
let mut cmd = Command::cargo_bin("genpolicy")?;
cmd.arg("--yaml-file").arg(workdir.join(pod_yaml_name));
cmd.arg("--config-map-file").arg(workdir.join(config_file));
cmd.assert().success();
Ok(())
}
#[test]
fn config_map_in_separate_file_workdir_flag() -> Result<(), Box<dyn std::error::Error>> {
// Prepare temp dir for running genpolicy.
let test_case_dir = "config_map_separate_file_workdir_flag";
let pod_yaml_name = "pod_with_config_map_ref.yaml";
let config_file = "config_map.yaml";
let workdir = prepare_workdir(test_case_dir, &[pod_yaml_name, config_file]);
let mut cmd = Command::cargo_bin("genpolicy")?;
cmd.arg("--yaml-file").arg(workdir.join(pod_yaml_name));
cmd.assert().failure();
let mut cmd = Command::cargo_bin("genpolicy")?;
cmd.arg("--yaml-file").arg(workdir.join(pod_yaml_name));
cmd.arg("--config-file").arg(workdir.join(config_file));
cmd.assert().success();
Ok(())
}
#[test]
fn secret_in_separate_file() -> Result<(), Box<dyn std::error::Error>> {
// Prepare temp dir for running genpolicy.
let test_case_dir = "secret_separate_file";
let pod_yaml_name = "pod_with_secret_ref.yaml";
let config_file = "secret.yaml";
let workdir = prepare_workdir(test_case_dir, &[pod_yaml_name, config_file]);
let mut cmd = Command::cargo_bin("genpolicy")?;
cmd.arg("--yaml-file").arg(workdir.join(pod_yaml_name));
cmd.assert().failure();
let mut cmd = Command::cargo_bin("genpolicy")?;
cmd.arg("--yaml-file").arg(workdir.join(pod_yaml_name));
cmd.arg("--config-file").arg(workdir.join(config_file));
cmd.assert().success();
Ok(())
}
fn prepare_workdir(test_case_dir: &str, files_to_copy: &[&str]) -> path::PathBuf {
// Prepare temp dir for running genpolicy.
let workdir = path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(test_case_dir);
fs::create_dir_all(&workdir)
.expect("should be able to create directories under CARGO_TARGET_TMPDIR");
let testdata_dir =
path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/generate/testdata");
// Make sure that workdir is empty.
for entry in fs::read_dir(&workdir).expect("should be able to read directories") {
let entry = entry.expect("should be able to read directory entries");
fs::remove_file(entry.path()).expect("should be able to remove files");
}
for file in files_to_copy {
fs::copy(testdata_dir.join(file), workdir.join(file))
.expect("copying files around should not fail");
}
let genpolicy_dir = path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
for base in ["rules.rego", "genpolicy-settings.json"] {
fs::copy(genpolicy_dir.join(base), workdir.join(base))
.expect("copying files around should not fail");
}
workdir
}

View File

@ -0,0 +1,75 @@
apiVersion: v1
data:
key: YmFyCg==
kind: ConfigMap
metadata:
name: configmap-sample
---
apiVersion: v1
data:
key: YmFyCg==
kind: ConfigMap
metadata:
name: configmap-sample2
---
# Add some other yaml document to make sure that we can parse out
# ConfigMaps and Secrets from yaml files with multiple documents.
apiVersion: v1
kind: Pod
metadata:
name: one-container-config-map
labels:
run: busybox
spec:
restartPolicy: Never
runtimeClassName: kata-cc
containers:
- name: busybox
image: "quay.io/prometheus/busybox:latest"
stdin: true
env:
- name: ENV_FROM_CONFIGMAP
valueFrom:
configMapKeyRef:
key: key
name: configmap-sample
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.podIP
- name: SERVICE_ACCOUNT
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.serviceAccountName
- name: PROXY_CONFIG
value: "{}\n"
- name: ISTIO_META_POD_PORTS
value: "[\n]"
- name: ISTIO_META_APP_CONTAINERS
value: serviceaclient
- name: ISTIO_META_CLUSTER_ID
value: Kubernetes
- name: ISTIO_META_NODE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.nodeName
securityContext:
privileged: true
command:
- /bin/sh
args:
- "-c"
- while true; do echo $(POD_NAME); sleep 10; done

View File

@ -0,0 +1,60 @@
---
apiVersion: v1
kind: Pod
metadata:
name: one-container
labels:
run: busybox
spec:
restartPolicy: Never
runtimeClassName: kata-cc
containers:
- name: busybox
image: "quay.io/prometheus/busybox:latest"
stdin: true
env:
- name: ENV_FROM_CONFIGMAP
valueFrom:
configMapKeyRef:
key: key
name: configmap-sample
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.podIP
- name: SERVICE_ACCOUNT
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.serviceAccountName
- name: PROXY_CONFIG
value: "{}\n"
- name: ISTIO_META_POD_PORTS
value: "[\n]"
- name: ISTIO_META_APP_CONTAINERS
value: serviceaclient
- name: ISTIO_META_CLUSTER_ID
value: Kubernetes
- name: ISTIO_META_NODE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.nodeName
securityContext:
privileged: true
command:
- /bin/sh
args:
- "-c"
- while true; do echo $(POD_NAME); sleep 10; done

View File

@ -0,0 +1,60 @@
---
apiVersion: v1
kind: Pod
metadata:
name: one-container
labels:
run: busybox
spec:
restartPolicy: Never
runtimeClassName: kata-cc
containers:
- name: busybox
image: "quay.io/prometheus/busybox:latest"
stdin: true
env:
- name: ENV_FROM_SECRET
valueFrom:
secretKeyRef:
key: key
name: secret-sample
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.podIP
- name: SERVICE_ACCOUNT
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.serviceAccountName
- name: PROXY_CONFIG
value: "{}\n"
- name: ISTIO_META_POD_PORTS
value: "[\n]"
- name: ISTIO_META_APP_CONTAINERS
value: serviceaclient
- name: ISTIO_META_CLUSTER_ID
value: Kubernetes
- name: ISTIO_META_NODE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.nodeName
securityContext:
privileged: true
command:
- /bin/sh
args:
- "-c"
- while true; do echo $(POD_NAME); sleep 10; done

View File

@ -0,0 +1,73 @@
apiVersion: v1
data:
key: YmFyCg==
kind: Secret
metadata:
name: secret-sample
---
apiVersion: v1
data:
key: YmFyCg==
kind: ConfigMap
metadata:
name: configmap-sample3
---
apiVersion: v1
kind: Pod
metadata:
name: one-container-secret
labels:
run: busybox
spec:
restartPolicy: Never
runtimeClassName: kata-cc
containers:
- name: busybox
image: "quay.io/prometheus/busybox:latest"
stdin: true
env:
- name: ENV_FROM_CONFIGMAP
valueFrom:
configMapKeyRef:
key: key
name: configmap-sample
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.podIP
- name: SERVICE_ACCOUNT
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.serviceAccountName
- name: PROXY_CONFIG
value: "{}\n"
- name: ISTIO_META_POD_PORTS
value: "[\n]"
- name: ISTIO_META_APP_CONTAINERS
value: serviceaclient
- name: ISTIO_META_CLUSTER_ID
value: Kubernetes
- name: ISTIO_META_NODE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.nodeName
securityContext:
privileged: true
command:
- /bin/sh
args:
- "-c"
- while true; do echo $(POD_NAME); sleep 10; done

View File

@ -5,6 +5,7 @@
#[cfg(test)]
mod tests {
use anyhow::Context;
use base64::prelude::*;
use std::fmt::{self, Display};
use std::fs::{self, File};
@ -63,29 +64,13 @@ mod tests {
/// with the tag `type` listing the exact type of request.
async fn runtests(test_case_dir: &str) {
// Prepare temp dir for running genpolicy.
let workdir = path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(test_case_dir);
fs::create_dir_all(&workdir)
.expect("should be able to create directories under CARGO_TARGET_TMPDIR");
let testdata_dir = path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/testdata")
.join(test_case_dir);
fs::copy(testdata_dir.join("pod.yaml"), workdir.join("pod.yaml"))
.expect("copying files around should not fail");
let genpolicy_dir =
path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tools/genpolicy");
for base in ["rules.rego", "genpolicy-settings.json"] {
fs::copy(genpolicy_dir.join(base), workdir.join(base))
.expect("copying files around should not fail");
}
let (workdir, testdata_dir) = prepare_workdir(test_case_dir, &["pod.yaml"]);
// Run the command and return the generated policy.
let config = genpolicy::utils::Config {
base64_out: false,
config_map_files: None,
config_files: None,
containerd_socket_path: None, // Some(String::from("/var/run/containerd/containerd.sock")),
insecure_registries: Vec::new(),
layers_cache_file_path: None,
@ -153,6 +138,50 @@ mod tests {
}
}
fn prepare_workdir(
test_case_dir: &str,
files_to_copy: &[&str],
) -> (path::PathBuf, path::PathBuf) {
// Prepare temp dir for running genpolicy.
let workdir = path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(test_case_dir);
fs::create_dir_all(&workdir)
.expect("should be able to create directories under CARGO_TARGET_TMPDIR");
let testdata_dir = path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/policy/testdata")
.join(test_case_dir);
// Make sure that workdir is empty.
for entry in fs::read_dir(&workdir).expect("should be able to read directories") {
let entry = entry.expect("should be able to read directory entries");
fs::remove_file(entry.path()).expect("should be able to remove files");
}
for file in files_to_copy {
fs::copy(testdata_dir.join(file), workdir.join(file))
.context(format!(
"{:?} --> {:?}",
testdata_dir.join(file),
workdir.join(file)
))
.expect("copying files around should not fail");
}
let genpolicy_dir = path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
for base in ["rules.rego", "genpolicy-settings.json"] {
fs::copy(genpolicy_dir.join(base), workdir.join(base))
.context(format!(
"{:?} --> {:?}",
genpolicy_dir.join(base),
workdir.join(base)
))
.expect("copying files around should not fail");
}
(workdir, testdata_dir)
}
#[tokio::test]
async fn test_copyfile() {
runtests("copyfile").await;