From c415a64cb812c3b3b76647c536cf3099ae94f637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Er=C3=A8be=20-=20Romain=20Gerard?= Date: Tue, 4 Jan 2022 18:37:11 +0100 Subject: [PATCH] Add support for ssh key during git submodule (#544) --- Cargo.lock | 8 +- Cargo.toml | 2 +- src/build_platform/local_docker.rs | 59 +++--- src/build_platform/mod.rs | 13 +- src/git.rs | 284 ++++++++++++++++++++++++----- src/models.rs | 38 +++- test_utilities/Cargo.lock | 8 +- 7 files changed, 327 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01f2f676..30d29020 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -944,9 +944,9 @@ checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" [[package]] name = "git2" -version = "0.13.21" +version = "0.13.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659cd14835e75b64d9dba5b660463506763cf0aa6cb640aeeb0e98d841093490" +checksum = "f29229cc1b24c0e6062f6e742aa3e256492a5323365e5ed3413599f8a5eff7d6" dependencies = [ "bitflags", "libc", @@ -1445,9 +1445,9 @@ checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" [[package]] name = "libgit2-sys" -version = "0.12.22+1.1.0" +version = "0.12.26+1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c53ac117c44f7042ad8d8f5681378dfbc6010e49ec2c0d1f11dfedc7a4a1c3" +checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index c2a1250b..b9ce0e5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" [dependencies] chrono = "0.4.19" cmd_lib = "1.0.13" -git2 = "0.13.20" +git2 = "0.13.25" walkdir = "2.3.2" itertools = "0.10.0" base64 = "0.13.0" diff --git a/src/build_platform/local_docker.rs b/src/build_platform/local_docker.rs index f37026e3..2fa64b88 100644 --- a/src/build_platform/local_docker.rs +++ b/src/build_platform/local_docker.rs @@ -2,9 +2,10 @@ use std::path::Path; use std::{env, fs}; use chrono::Duration; +use git2::{Cred, CredentialType}; use sysinfo::{Disk, DiskExt, SystemExt}; -use crate::build_platform::{Build, BuildPlatform, BuildResult, Image, Kind}; +use crate::build_platform::{Build, BuildPlatform, BuildResult, Credentials, Image, Kind}; use crate::cmd::utilities::QoveryCommand; use crate::error::{EngineError, EngineErrorCause, SimpleError, SimpleErrorKind}; use crate::fs::workspace_directory; @@ -315,48 +316,44 @@ impl BuildPlatform for LocalDocker { ) .map_err(|err| self.engine_error(EngineErrorCause::Internal, err.to_string()))?; + // Clone git repository info!( "cloning repository: {} to {}", build.git_repository.url, repository_root_path ); - let git_clone = git::clone( - build.git_repository.url.as_str(), - &repository_root_path, - &build.git_repository.credentials, - ); + let get_credentials = || { + let mut creds: Vec<(CredentialType, Cred)> = Vec::with_capacity(build.git_repository.ssh_keys.len() + 1); + for ssh_key in build.git_repository.ssh_keys.iter() { + let public_key = ssh_key.public_key.as_ref().map(|x| x.as_str()); + let passphrase = ssh_key.passphrase.as_ref().map(|x| x.as_str()); + if let Ok(cred) = Cred::ssh_key_from_memory("git", public_key, &ssh_key.private_key, passphrase) { + creds.push((CredentialType::SSH_MEMORY, cred)); + } + } - if let Err(err) = git_clone { + if let Some(Credentials { login, password }) = &build.git_repository.credentials { + creds.push(( + CredentialType::USER_PASS_PLAINTEXT, + Cred::userpass_plaintext(&login, &password).unwrap(), + )); + } + creds + }; + + if let Err(clone_error) = git::clone_at_commit( + &build.git_repository.url, + &build.git_repository.commit_id, + &repository_root_path, + &get_credentials, + ) { let message = format!( "Error while cloning repository {}. Error: {:?}", - &build.git_repository.url, err + &build.git_repository.url, clone_error ); error!("{}", message); return Err(self.engine_error(EngineErrorCause::Internal, message)); } - // git checkout to given commit - let repo = git_clone.unwrap(); - let commit_id = &build.git_repository.commit_id; - if let Err(err) = git::checkout(&repo, commit_id) { - let message = format!( - "Error while git checkout repository {} with commit id {}. Error: {:?}", - &build.git_repository.url, commit_id, err - ); - error!("{}", message); - return Err(self.engine_error(EngineErrorCause::Internal, message)); - } - - // git checkout submodules - if let Err(err) = git::checkout_submodules(&repo) { - let message = format!( - "Error while checkout submodules from repository {}. Error: {:?}", - &build.git_repository.url, err - ); - error!("{}", message); - - return Err(self.engine_error(EngineErrorCause::Internal, message)); - } - let mut disable_build_cache = false; let mut env_var_args: Vec = Vec::with_capacity(build.options.environment_variables.len()); diff --git a/src/build_platform/mod.rs b/src/build_platform/mod.rs index 183789f5..cfee4cad 100644 --- a/src/build_platform/mod.rs +++ b/src/build_platform/mod.rs @@ -1,7 +1,6 @@ use serde::{Deserialize, Serialize}; use crate::error::{EngineError, EngineErrorCause, EngineErrorScope}; -use crate::git::Credentials; use crate::models::{Context, Listen}; use std::fmt::{Display, Formatter, Result as FmtResult}; @@ -46,9 +45,21 @@ pub struct EnvironmentVariable { pub value: String, } +pub struct Credentials { + pub login: String, + pub password: String, +} + +pub struct SshKey { + pub private_key: String, + pub passphrase: Option, + pub public_key: Option, +} + pub struct GitRepository { pub url: String, pub credentials: Option, + pub ssh_keys: Vec, pub commit_id: String, pub dockerfile_path: Option, pub root_path: String, diff --git a/src/git.rs b/src/git.rs index d48e506d..472ff673 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,36 +1,45 @@ use std::path::Path; use git2::build::{CheckoutBuilder, RepoBuilder}; +use git2::ErrorCode::Auth; use git2::ResetType::Hard; -use git2::{Error, Repository}; +use git2::{Cred, CredentialType, Error, RemoteCallbacks, Repository, SubmoduleUpdateOptions}; use url::Url; -pub struct Credentials { - pub login: String, - pub password: String, -} +// Credentials callback is called endlessly until the server return Auth Ok (or a definitive error) +// If auth is denied, it up to us to return a new credential to try different auth method +// or an error to specify that we have exhausted everything we are able to provide +fn authentication_callback<'a>( + get_credentials: &'a impl Fn() -> Vec<(CredentialType, Cred)>, +) -> impl FnMut(&str, Option<&str>, CredentialType) -> Result + 'a { + let mut current_credentials: (String, Vec<(CredentialType, Cred)>) = ("".into(), vec![]); -pub fn clone

(repository_url: &str, into_dir: P, credentials: &Option) -> Result -where - P: AsRef, -{ - let mut url = Url::parse(repository_url) - .map_err(|err| Error::from_str(format!("Invalid repository url {}: {}", repository_url, err).as_str()))?; + return move |remote_url, _username_from_url, allowed_types| { + // If we have changed remote, reset our available auth methods + if remote_url != current_credentials.0 { + current_credentials = (remote_url.to_string(), get_credentials()); + } + let auth_methods = &mut current_credentials.1; - if url.scheme() != "https" { - return Err(Error::from_str("Repository URL have to start with https://")); - } + // Try all the auth method until one match allowed_types + loop { + let (cred_type, credential) = match auth_methods.pop() { + Some(cred) => cred, + None => { + let mut error = Error::from_str("Invalid authentication: Exhausted all available auth method"); + error.set_code(Auth); + return Err(error); + } + }; - if let Some(Credentials { login, password }) = credentials { - url.set_username(login).map_err(|_| Error::from_str("Invalid login"))?; - url.set_password(Some(password)) - .map_err(|_| Error::from_str("Invalid password"))?; + if allowed_types.contains(cred_type) { + return Ok(credential); + } + } }; - - RepoBuilder::new().clone(url.as_str(), into_dir.as_ref()) } -pub fn checkout(repo: &Repository, commit_id: &str) -> Result<(), Error> { +fn checkout(repo: &Repository, commit_id: &str) -> Result<(), Error> { let obj = repo.revparse_single(commit_id).map_err(|err| { let repo_url = repo .find_remote("origin") @@ -50,18 +59,77 @@ pub fn checkout(repo: &Repository, commit_id: &str) -> Result<(), Error> { repo.reset(&obj, Hard, Some(&mut checkout_opts)) } -pub fn checkout_submodules(repo: &Repository) -> Result<(), Error> { - for mut submodule in repo.submodules()? { - info!("getting submodule {:?} from {:?}", submodule.name(), submodule.url()); - submodule.update(true, None)? +fn clone

( + repository_url: &str, + into_dir: P, + get_credentials: &impl Fn() -> Vec<(CredentialType, Cred)>, +) -> Result +where + P: AsRef, +{ + let url = Url::parse(repository_url) + .map_err(|err| Error::from_str(format!("Invalid repository url {}: {}", repository_url, err).as_str()))?; + + if url.scheme() != "https" { + return Err(Error::from_str("Repository URL have to start with https://")); } - Ok(()) + // Prepare authentication callbacks. + let mut callbacks = RemoteCallbacks::new(); + callbacks.credentials(authentication_callback(&get_credentials)); + + // Prepare fetch options. + let mut fo = git2::FetchOptions::new(); + fo.remote_callbacks(callbacks); + + // Get our repository + let mut repo = RepoBuilder::new(); + repo.fetch_options(fo); + repo.clone(url.as_str(), into_dir.as_ref()) +} + +pub fn clone_at_commit

( + repository_url: &str, + commit_id: &str, + into_dir: P, + get_credentials: &impl Fn() -> Vec<(CredentialType, Cred)>, +) -> Result +where + P: AsRef, +{ + // clone repository + let repo = clone(repository_url, into_dir, get_credentials)?; + + // position the repo at the correct commit + checkout(&repo, commit_id)?; + + // check submodules if needed + { + let submodules = repo.submodules()?; + if !submodules.is_empty() { + // for auth + let mut callbacks = RemoteCallbacks::new(); + callbacks.credentials(authentication_callback(&get_credentials)); + + let mut fo = git2::FetchOptions::new(); + fo.remote_callbacks(callbacks); + let mut opts = SubmoduleUpdateOptions::new(); + opts.fetch(fo); + + for mut submodule in submodules { + info!("getting submodule {:?} from {:?}", submodule.name(), submodule.url()); + submodule.update(true, Some(&mut opts))? + } + } + } + + Ok(repo) } #[cfg(test)] mod tests { - use crate::git::{checkout, clone, Credentials}; + use crate::git::{checkout, clone, clone_at_commit}; + use git2::{Cred, CredentialType}; struct DirectoryToDelete<'a> { pub path: &'a str, @@ -76,11 +144,11 @@ mod tests { #[test] fn test_git_clone_repository() { // We only allow https:// at the moment - let repo = clone("git@github.com:Qovery/engine.git", "/tmp", &None); + let repo = clone("git@github.com:Qovery/engine.git", "/tmp", &|| vec![]); assert!(matches!(repo, Err(e) if e.message().contains("Invalid repository"))); // Repository must be empty - let repo = clone("https://github.com/Qovery/engine.git", "/tmp", &None); + let repo = clone("https://github.com/Qovery/engine-testing.git", "/tmp", &|| vec![]); assert!(matches!(repo, Err(e) if e.message().contains("'/tmp' exists and is not an empty directory"))); // Working case @@ -88,7 +156,11 @@ mod tests { let clone_dir = DirectoryToDelete { path: "/tmp/engine_test_clone", }; - let repo = clone("https://github.com/Qovery/engine.git", clone_dir.path, &None); + let repo = clone( + "https://github.com/Qovery/engine-testing.git", + clone_dir.path, + &|| vec![], + ); assert!(matches!(repo, Ok(_repo))); } @@ -97,11 +169,13 @@ mod tests { let clone_dir = DirectoryToDelete { path: "/tmp/engine_test_clone", }; - let creds = Some(Credentials { - login: "FAKE".to_string(), - password: "FAKE".to_string(), - }); - let repo = clone("https://gitlab.com/qovery/q-core.git", clone_dir.path, &creds); + let get_credentials = || { + vec![( + CredentialType::USER_PASS_PLAINTEXT, + Cred::userpass_plaintext("FAKE", "FAKE").unwrap(), + )] + }; + let repo = clone("https://gitlab.com/qovery/q-core.git", clone_dir.path, &get_credentials); assert!(matches!(repo, Err(repo) if repo.message().contains("authentication"))); } @@ -112,14 +186,19 @@ mod tests { let clone_dir = DirectoryToDelete { path: "/tmp/engine_test_clone", }; - let creds = Some(Credentials { - login: "{a45d7986-7994-43a9-a961-044799e761d7}".to_string(), - password: "3uDbu-i3kdanLRV6iSSWzWDJf4oUQu2hbUQ250DMezFEkkmz3oxPRiAcj7RuLrNgmKu7qx6XA820uvvyfUCdx06bt4VCaOZQkEwkWVksNpAkPE1Lw8gPcnEK".to_string(), - }); + + let get_credentials = || { + vec![ + ( + CredentialType::USER_PASS_PLAINTEXT, + Cred::userpass_plaintext("{a45d7986-7994-43a9-a961-044799e761d7}", "3uDbu-i3kdanLRV6iSSWzWDJf4oUQu2hbUQ250DMezFEkkmz3oxPRiAcj7RuLrNgmKu7qx6XA820uvvyfUCdx06bt4VCaOZQkEwkWVksNpAkPE1Lw8gPcnEK").unwrap(), + ), + ] + }; let repo = clone( "https://bitbucket.org/erebe/attachment-parser.git", clone_dir.path, - &creds, + &get_credentials, ); assert!(matches!(repo, Ok(_))); } @@ -131,17 +210,138 @@ mod tests { let clone_dir = DirectoryToDelete { path: "/tmp/engine_test_checkout", }; - let repo = clone("https://github.com/Qovery/engine.git", clone_dir.path, &None).unwrap(); + let repo = clone( + "https://github.com/Qovery/engine-testing.git", + clone_dir.path, + &|| vec![], + ) + .unwrap(); // Invalid commit for this repository let check = checkout(&repo, "c2c2101f8e4c4ffadb326dc440ba8afb4aeb1310"); assert!(matches!(check, Err(_err))); // Valid commit - let commit = "9da0f98b5eb719643263abba062041708fa20d31"; + let commit = "9a9c1f4373c8128151a9def9ea3d838fa2ed33e8"; assert_ne!(repo.head().unwrap().target().unwrap().to_string(), commit); let check = checkout(&repo, commit); assert!(matches!(check, Ok(()))); assert_eq!(repo.head().unwrap().target().unwrap().to_string(), commit); } + + #[test] + fn test_git_submodule_with_ssh_key() { + // Unique Key only valid for the submodule and in read access only + // https://github.com/Qovery/dumb-logger/settings/keys + let ssh_key = r#" +-----BEGIN OPENSSH PRIVATE KEY----- +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 +i8Lcpwg96ZVfHBAAAACmVyZWJlQHN0eXg= +-----END OPENSSH PRIVATE KEY----- + "# + .trim(); + + let invalid_ssh_key = r#" +-----BEGIN OPENSSH PRIVATE KEY----- +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 +RmSmoA== +-----END OPENSSH PRIVATE KEY----- + "# + .trim(); + let clone_dir = DirectoryToDelete { + path: "/tmp/engine_test_submodule", + }; + let get_credentials = || { + vec![ + ( + CredentialType::SSH_MEMORY, + Cred::ssh_key_from_memory("git", None, invalid_ssh_key, Some("toto")).unwrap(), + ), + ( + CredentialType::SSH_MEMORY, + Cred::ssh_key_from_memory("git", None, &ssh_key, None).unwrap(), + ), + ( + CredentialType::SSH_MEMORY, + Cred::ssh_key_from_memory("git", None, invalid_ssh_key, Some("toto")).unwrap(), + ), + ] + }; + let repo = clone_at_commit( + "https://github.com/Qovery/engine-testing.git", + "9a9c1f4373c8128151a9def9ea3d838fa2ed33e8", + clone_dir.path, + &get_credentials, + ); + assert!(matches!(repo, Ok(_))); + } } diff --git a/src/models.rs b/src/models.rs index f1fac6ba..d1637db7 100644 --- a/src/models.rs +++ b/src/models.rs @@ -6,7 +6,7 @@ use rand::Rng; use serde::{Deserialize, Serialize}; use std::collections::hash_map::DefaultHasher; -use crate::build_platform::{Build, BuildOptions, GitRepository, Image}; +use crate::build_platform::{Build, BuildOptions, Credentials, GitRepository, Image, SshKey}; use crate::cloud_provider::aws::databases::mongodb::MongoDB; use crate::cloud_provider::aws::databases::mysql::MySQL; use crate::cloud_provider::aws::databases::postgresql::PostgreSQL; @@ -15,7 +15,6 @@ use crate::cloud_provider::service::{DatabaseOptions, StatefulService, Stateless use crate::cloud_provider::utilities::VersionsNumber; use crate::cloud_provider::CloudProvider; use crate::cloud_provider::Kind as CPKind; -use crate::git::Credentials; use itertools::Itertools; use std::collections::BTreeMap; use std::fmt::{Display, Formatter}; @@ -375,6 +374,40 @@ impl Application { } pub fn to_build(&self) -> Build { + // Retrieve ssh keys from env variables + const ENV_GIT_PREFIX: &str = "GIT_SSH_KEY"; + let env_ssh_keys: Vec<(String, String)> = self + .environment_vars + .iter() + .filter_map(|(name, value)| { + if name.starts_with(ENV_GIT_PREFIX) { + Some((name.clone(), value.clone())) + } else { + None + } + }) + .collect(); + + // Get passphrase and public key if provided by the user + let mut ssh_keys: Vec = Vec::with_capacity(env_ssh_keys.len()); + for (ssh_key_name, private_key) in env_ssh_keys { + let passphrase = self + .environment_vars + .get(&ssh_key_name.replace(ENV_GIT_PREFIX, "GIT_SSH_PASSPHRASE")) + .map(|val| val.clone()); + + let public_key = self + .environment_vars + .get(&ssh_key_name.replace(ENV_GIT_PREFIX, "GIT_SSH_PUBLIC_KEY")) + .map(|val| val.clone()); + + ssh_keys.push(SshKey { + private_key, + passphrase, + public_key, + }); + } + Build { git_repository: GitRepository { url: self.git_url.clone(), @@ -382,6 +415,7 @@ impl Application { login: credentials.login.clone(), password: credentials.access_token.clone(), }), + ssh_keys, commit_id: self.commit_id.clone(), dockerfile_path: self.dockerfile_path.clone(), root_path: self.root_path.clone(), diff --git a/test_utilities/Cargo.lock b/test_utilities/Cargo.lock index b9144781..05166406 100644 --- a/test_utilities/Cargo.lock +++ b/test_utilities/Cargo.lock @@ -963,9 +963,9 @@ checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" [[package]] name = "git2" -version = "0.13.20" +version = "0.13.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9831e983241f8c5591ed53f17d874833e2fa82cac2625f3888c50cbfe136cba" +checksum = "f29229cc1b24c0e6062f6e742aa3e256492a5323365e5ed3413599f8a5eff7d6" dependencies = [ "bitflags", "libc", @@ -1464,9 +1464,9 @@ checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" [[package]] name = "libgit2-sys" -version = "0.12.21+1.1.0" +version = "0.12.26+1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86271bacd72b2b9e854c3dcfb82efd538f15f870e4c11af66900effb462f6825" +checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" dependencies = [ "cc", "libc",