Skip to content

Commit

Permalink
feat: allow manifest to pull a remote policy
Browse files Browse the repository at this point in the history
Updates the manifest command to pull the policy if it is not in the
local storage.

Signed-off-by: Flavio Castelli <fcastelli@suse.com>
Signed-off-by: José Guilherme Vanz <jguilhermevanz@suse.com>
  • Loading branch information
flavio authored and jvanz committed Aug 8, 2024
1 parent ce7a88f commit 71f7e58
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 91 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ syntect = { version = "5.2", default-features = false, features = [
"parsing",
] }
tar = "0.4.40"
thiserror = "1.0"
tiny-bench = "0.3"
tokio = { version = "^1.39.0", features = ["full"] }
tracing = "0.1"
Expand Down
23 changes: 15 additions & 8 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ Use the `info` command to display system information.
};
}

fn subcommand_pull() -> Command {
let mut args = vec![
// Minimum set of flags required to pull a policy from a registry
fn pull_shared_flags() -> Vec<Arg> {
vec![

Check warning on line 21 in src/cli.rs

View check run for this annotation

Codecov / codecov/patch

src/cli.rs#L20-L21

Added lines #L20 - L21 were not covered by tests
Arg::new("docker-config-json-path")
.long("docker-config-json-path")
.value_name("DOCKER_CONFIG")
Expand Down Expand Up @@ -74,12 +75,16 @@ fn subcommand_pull() -> Command {
.number_of_values(1)
.value_name("VALUE")
.help("GitHub repository expected in the certificates generated in CD pipelines"),
Arg::new("output-path")
.short('o')
.long("output-path")
.value_name("PATH")
.help("Output file. If not provided will be downloaded to the Kubewarden store"),
];
]
}

fn subcommand_pull() -> Command {
let mut args = pull_shared_flags();
args.extend_from_slice(&[Arg::new("output-path")
.short('o')
.long("output-path")
.value_name("PATH")
.help("Output file. If not provided will be downloaded to the Kubewarden store")]);

Check warning on line 87 in src/cli.rs

View check run for this annotation

Codecov / codecov/patch

src/cli.rs#L81-L87

Added lines #L81 - L87 were not covered by tests
args.sort_by(|a, b| a.get_id().cmp(b.get_id()));
args.push(
Arg::new("uri")
Expand Down Expand Up @@ -457,6 +462,8 @@ fn subcommand_scaffold() -> Command {
.num_args(0)
.help("Uses the policy metadata to define which Kubernetes resources can be accessed by the policy. Warning: review the list of resources carefully to avoid abuses. Disabled by default"),
];
// When scaffolding the manifest of a missing policy, we can pull it from a registry
manifest_args.extend_from_slice(&pull_shared_flags());

Check warning on line 466 in src/cli.rs

View check run for this annotation

Codecov / codecov/patch

src/cli.rs#L466

Added line #L466 was not covered by tests
manifest_args.sort_by(|a, b| a.get_id().cmp(b.get_id()));
manifest_args.push(
Arg::new("uri_or_sha_prefix")
Expand Down
172 changes: 103 additions & 69 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use std::{
};
use verify::VerificationAnnotations;

use crate::utils::LookupError;
use tracing::{debug, info, warn};
use tracing_subscriber::prelude::*;
use tracing_subscriber::{
Expand Down Expand Up @@ -147,39 +148,7 @@ async fn main() -> Result<()> {
Some(destination) => PullDestination::LocalFile(destination),
None => PullDestination::MainStore,
};

let sources = remote_server_options(matches)?;

let verification_options = verification_options(matches)?;
let mut verified_manifest_digest: Option<String> = None;
if verification_options.is_some() {
let sigstore_trust_root = build_sigstore_trust_root(matches.to_owned()).await?;
// verify policy prior to pulling if keys listed, and keep the
// verified manifest digest:
verified_manifest_digest = Some(
verify::verify(
uri,
sources.as_ref(),
verification_options.as_ref().unwrap(),
sigstore_trust_root.clone(),
)
.await
.map_err(|e| anyhow!("Policy {} cannot be validated\n{:?}", uri, e))?,
);
}

let policy = pull::pull(uri, sources.as_ref(), destination).await?;

if verification_options.is_some() {
let sigstore_trust_root = build_sigstore_trust_root(matches.to_owned()).await?;
verify::verify_local_checksum(
&policy,
sources.as_ref(),
&verified_manifest_digest.unwrap(),
sigstore_trust_root.clone(),
)
.await?
}
pull_command(uri, destination, matches).await?

Check warning on line 151 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L151

Added line #L151 was not covered by tests
};
Ok(())
}
Expand Down Expand Up @@ -368,42 +337,7 @@ async fn main() -> Result<()> {
}
if let Some(matches) = matches.subcommand_matches("scaffold") {
if let Some(matches) = matches.subcommand_matches("manifest") {
let uri_or_sha_prefix = matches.get_one::<String>("uri_or_sha_prefix").unwrap();
let resource_type = matches.get_one::<String>("type").unwrap();
if matches.contains_id("settings-path") && matches.contains_id("settings-json")
{
return Err(anyhow!(
"'settings-path' and 'settings-json' cannot be used at the same time"
));
}
let settings = if matches.contains_id("settings-path") {
matches
.get_one::<String>("settings-path")
.map(|settings| -> Result<String> {
fs::read_to_string(settings).map_err(|e| {
anyhow!("Error reading settings from {}: {}", settings, e)
})
})
.transpose()?
} else if matches.contains_id("settings-json") {
Some(matches.get_one::<String>("settings-json").unwrap().clone())
} else {
None
};
let policy_title = matches.get_one::<String>("title").cloned();

let allow_context_aware_resources = matches
.get_one::<bool>("allow-context-aware")
.unwrap_or(&false)
.to_owned();

scaffold::manifest(
uri_or_sha_prefix,
resource_type.parse()?,
settings.as_deref(),
policy_title.as_deref(),
allow_context_aware_resources,
)?;
scaffold_manifest_command(matches).await?;

Check warning on line 340 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L340

Added line #L340 was not covered by tests
};
}
if let Some(matches) = matches.subcommand_matches("scaffold") {
Expand Down Expand Up @@ -836,3 +770,103 @@ async fn build_sigstore_trust_root(
Ok(Some(Arc::new(manual_root)))
}
}

// Check if the policy is already present in the local store, and if not, pull it from the remote server.
async fn pull_if_needed(uri_or_sha_prefix: &str, matches: &ArgMatches) -> Result<()> {
match crate::utils::get_wasm_path(uri_or_sha_prefix) {
Err(LookupError::PolicyMissing(uri)) => {
info!(
"cannot find policy with uri: {}, trying to pull it from remote registry",

Check warning on line 779 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L775-L779

Added lines #L775 - L779 were not covered by tests
uri
);
pull_command(&uri, PullDestination::MainStore, matches).await

Check warning on line 782 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L782

Added line #L782 was not covered by tests
}
Err(e) => Err(anyhow!("{}", e)),
Ok(_path) => Ok(()),

Check warning on line 785 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L784-L785

Added lines #L784 - L785 were not covered by tests
}
}

// Pulls a policy from a remote server and verifies it if verification options are provided.
async fn pull_command(

Check warning on line 790 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L790

Added line #L790 was not covered by tests
uri: &String,
destination: PullDestination,
matches: &ArgMatches,
) -> Result<()> {
let sources = remote_server_options(matches)?;

Check warning on line 795 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L795

Added line #L795 was not covered by tests

let verification_options = verification_options(matches)?;

Check warning on line 797 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L797

Added line #L797 was not covered by tests
let mut verified_manifest_digest: Option<String> = None;
if verification_options.is_some() {
let sigstore_trust_root = build_sigstore_trust_root(matches.to_owned()).await?;

Check warning on line 800 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L800

Added line #L800 was not covered by tests
// verify policy prior to pulling if keys listed, and keep the
// verified manifest digest:
verified_manifest_digest = Some(
verify::verify(
uri,
sources.as_ref(),
verification_options.as_ref().unwrap(),
sigstore_trust_root.clone(),

Check warning on line 808 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L803-L808

Added lines #L803 - L808 were not covered by tests
)
.await
.map_err(|e| anyhow!("Policy {} cannot be validated\n{:?}", uri, e))?,

Check warning on line 811 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L810-L811

Added lines #L810 - L811 were not covered by tests
);
}

let policy = pull::pull(uri, sources.as_ref(), destination).await?;

Check warning on line 815 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L815

Added line #L815 was not covered by tests

if verification_options.is_some() {
let sigstore_trust_root = build_sigstore_trust_root(matches.to_owned()).await?;
return verify::verify_local_checksum(
&policy,
sources.as_ref(),
&verified_manifest_digest.unwrap(),
sigstore_trust_root.clone(),

Check warning on line 823 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L817-L823

Added lines #L817 - L823 were not covered by tests
)
.await;

Check warning on line 825 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L825

Added line #L825 was not covered by tests
}
Ok(())

Check warning on line 827 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L827

Added line #L827 was not covered by tests
}

/*
* Scaffold a manifest from a policy.
* This function will pull the policy if it is not already present in the local store.
*/
async fn scaffold_manifest_command(matches: &ArgMatches) -> Result<()> {
let uri_or_sha_prefix = matches.get_one::<String>("uri_or_sha_prefix").unwrap();

Check warning on line 835 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L834-L835

Added lines #L834 - L835 were not covered by tests

pull_if_needed(uri_or_sha_prefix, matches).await?;

Check warning on line 837 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L837

Added line #L837 was not covered by tests

let resource_type = matches.get_one::<String>("type").unwrap();
if matches.contains_id("settings-path") && matches.contains_id("settings-json") {
return Err(anyhow!(
"'settings-path' and 'settings-json' cannot be used at the same time"

Check warning on line 842 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L839-L842

Added lines #L839 - L842 were not covered by tests
));
}
let settings = if matches.contains_id("settings-path") {
matches

Check warning on line 846 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L845-L846

Added lines #L845 - L846 were not covered by tests
.get_one::<String>("settings-path")
.map(|settings| -> Result<String> {
fs::read_to_string(settings)
.map_err(|e| anyhow!("Error reading settings from {}: {}", settings, e))

Check warning on line 850 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L848-L850

Added lines #L848 - L850 were not covered by tests
})
.transpose()?
} else if matches.contains_id("settings-json") {
Some(matches.get_one::<String>("settings-json").unwrap().clone())

Check warning on line 854 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L853-L854

Added lines #L853 - L854 were not covered by tests
} else {
None

Check warning on line 856 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L856

Added line #L856 was not covered by tests
};
let policy_title = matches.get_one::<String>("title").cloned();

let allow_context_aware_resources = matches
.get_one::<bool>("allow-context-aware")
.unwrap_or(&false)
.to_owned();

scaffold::manifest(
uri_or_sha_prefix,
resource_type.parse()?,
settings.as_deref(),
policy_title.as_deref(),
allow_context_aware_resources,

Check warning on line 870 in src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/main.rs#L867-L870

Added lines #L867 - L870 were not covered by tests
)
}
2 changes: 1 addition & 1 deletion src/scaffold/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ pub(crate) fn manifest(
policy_title: Option<&str>,
allow_context_aware_resources: bool,
) -> Result<()> {
let uri = crate::utils::map_path_to_uri(uri_or_sha_prefix)?;
let uri = crate::utils::get_uri(&uri_or_sha_prefix.to_owned())?;

Check warning on line 125 in src/scaffold/manifest.rs

View check run for this annotation

Codecov / codecov/patch

src/scaffold/manifest.rs#L125

Added line #L125 was not covered by tests
let wasm_path = crate::utils::wasm_path(&uri)?;

let metadata = Metadata::from_path(&wasm_path)?
Expand Down
48 changes: 38 additions & 10 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
use anyhow::{anyhow, Result};
use policy_evaluator::policy_evaluator::PolicyExecutionMode;
use policy_evaluator::policy_fetcher::store::Store;
use policy_evaluator::policy_fetcher::oci_distribution::Reference;
use policy_evaluator::policy_fetcher::store::{errors::StoreError, Store};
use regex::Regex;
use serde_json::json;
use std::path::PathBuf;
use std::str::FromStr;
use url::Url;

pub(crate) fn map_path_to_uri(uri_or_sha_prefix: &str) -> Result<String> {
#[derive(Debug, thiserror::Error)]
pub(crate) enum LookupError {
#[error("Cannot find policy with uri: {0}")]
PolicyMissing(String),
#[error("{0}")]
StoreError(#[from] StoreError),
#[error("Unknown scheme: {0}")]
UnknownScheme(String),
#[error("{0}")]
UrlParserError(#[from] url::ParseError),
#[error("Error while converting URL to string")]
UrlToStringConversionError(),
#[error("{0}")]
IoError(#[from] std::io::Error),
}

pub(crate) fn map_path_to_uri(uri_or_sha_prefix: &str) -> std::result::Result<String, LookupError> {
let uri_has_schema = Regex::new(r"^\w+://").unwrap();
if uri_has_schema.is_match(uri_or_sha_prefix) {
return Ok(String::from(uri_or_sha_prefix));
Expand All @@ -22,31 +40,41 @@ pub(crate) fn map_path_to_uri(uri_or_sha_prefix: &str) -> Result<String> {
if let Some(policy) = store.get_policy_by_sha_prefix(uri_or_sha_prefix)? {
Ok(policy.uri.clone())
} else {
Err(anyhow!(
"Cannot find policy with prefix: {}",
uri_or_sha_prefix
))
Err(LookupError::PolicyMissing(uri_or_sha_prefix.to_string()))

Check warning on line 43 in src/utils.rs

View check run for this annotation

Codecov / codecov/patch

src/utils.rs#L43

Added line #L43 was not covered by tests
}
}
}

pub(crate) fn wasm_path(uri: &str) -> Result<PathBuf> {
pub(crate) fn get_uri(uri_or_sha_prefix: &String) -> std::result::Result<String, LookupError> {
map_path_to_uri(uri_or_sha_prefix).or_else(|_| {
Reference::from_str(uri_or_sha_prefix)
.map(|oci_reference| format!("registry://{}", oci_reference.whole()))
.map_err(|_| LookupError::PolicyMissing(uri_or_sha_prefix.to_string()))

Check warning on line 52 in src/utils.rs

View check run for this annotation

Codecov / codecov/patch

src/utils.rs#L48-L52

Added lines #L48 - L52 were not covered by tests
})
}

pub(crate) fn get_wasm_path(uri_or_sha_prefix: &str) -> std::result::Result<PathBuf, LookupError> {
let uri = get_uri(&uri_or_sha_prefix.to_owned())?;

Check warning on line 57 in src/utils.rs

View check run for this annotation

Codecov / codecov/patch

src/utils.rs#L56-L57

Added lines #L56 - L57 were not covered by tests
wasm_path(&uri)
}

pub(crate) fn wasm_path(uri: &str) -> std::result::Result<PathBuf, LookupError> {

Check warning on line 61 in src/utils.rs

View check run for this annotation

Codecov / codecov/patch

src/utils.rs#L61

Added line #L61 was not covered by tests
let url = Url::parse(uri)?;
match url.scheme() {
"file" => url
.to_file_path()
.map_err(|err| anyhow!("cannot retrieve path from uri {}: {:?}", url, err)),
.map_err(|_| LookupError::UrlToStringConversionError()),

Check warning on line 66 in src/utils.rs

View check run for this annotation

Codecov / codecov/patch

src/utils.rs#L66

Added line #L66 was not covered by tests
"http" | "https" | "registry" => {
let store = Store::default();
let policy = store.get_policy_by_uri(uri)?;

if let Some(policy) = policy {
Ok(policy.local_path)
} else {
Err(anyhow!("Cannot find policy '{uri}' inside of the local store.\nTry executing `kwctl pull {uri}`", uri = uri))
Err(LookupError::PolicyMissing(uri.to_string()))

Check warning on line 74 in src/utils.rs

View check run for this annotation

Codecov / codecov/patch

src/utils.rs#L74

Added line #L74 was not covered by tests
}
}
_ => Err(anyhow!("unknown scheme: {}", url.scheme())),
_ => Err(LookupError::UnknownScheme(url.scheme().to_string())),

Check warning on line 77 in src/utils.rs

View check run for this annotation

Codecov / codecov/patch

src/utils.rs#L77

Added line #L77 was not covered by tests
}
}

Expand Down
10 changes: 7 additions & 3 deletions tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,10 +377,14 @@ fn test_push() {
.stdout(contains("my-pod-priviliged-policy:v0.1.10"));
}

#[test]
fn test_scaffold_manifest() {
#[rstest]
#[case::pull_policies_before_scaffold(true)]
#[case::pull_policies_on_demand(false)]
fn test_scaffold_manifest(#[case] pull_policies_before: bool) {
let tempdir = tempdir().unwrap();
pull_policies(tempdir.path(), POLICIES);
if pull_policies_before {
pull_policies(tempdir.path(), POLICIES);
}

let mut cmd = setup_command(tempdir.path());
cmd.arg("scaffold")
Expand Down

0 comments on commit 71f7e58

Please sign in to comment.