Browse Source

New architecture

master
Adrian Heine 4 years ago
parent
commit
907a4962c5
  1. 1
      Cargo.toml
  2. 18
      src/artifacts/mod.rs
  3. 5
      src/build.rs
  4. 767
      src/builder.rs
  5. 125
      src/factory.rs
  6. 16
      src/lib.rs
  7. 443
      src/locator.rs
  8. 36
      src/repository.rs
  9. 289
      src/resources/mod.rs
  10. 200
      src/schema.rs
  11. 345
      src/setup.rs
  12. 1
      src/static_files.rs
  13. 23
      src/storage.rs
  14. 85
      src/symbols/acme/account_key.rs
  15. 101
      src/symbols/acme/cert.rs
  16. 117
      src/symbols/acme/chain.rs
  17. 113
      src/symbols/acme/mod.rs
  18. 36
      src/symbols/concat.rs
  19. 31
      src/symbols/cron.rs
  20. 48
      src/symbols/dir.rs
  21. 436
      src/symbols/factory.rs
  22. 39
      src/symbols/file.rs
  23. 103
      src/symbols/git/checkout.rs
  24. 6
      src/symbols/git/mod.rs
  25. 196
      src/symbols/hook.rs
  26. 213
      src/symbols/list.rs
  27. 38
      src/symbols/mariadb/database.rs
  28. 75
      src/symbols/mariadb/database_dump.rs
  29. 56
      src/symbols/mariadb/dump.rs
  30. 8
      src/symbols/mariadb/mod.rs
  31. 37
      src/symbols/mariadb/user.rs
  32. 70
      src/symbols/mod.rs
  33. 1
      src/symbols/nginx/mod.rs
  34. 246
      src/symbols/nginx/server.rs
  35. 31
      src/symbols/noop.rs
  36. 27
      src/symbols/npm.rs
  37. 64
      src/symbols/owner.rs
  38. 16
      src/symbols/postgresql/database.rs
  39. 87
      src/symbols/saved_directory.rs
  40. 10
      src/symbols/systemd/mod.rs
  41. 42
      src/symbols/systemd/reload.rs
  42. 165
      src/symbols/systemd/user_service.rs
  43. 62
      src/symbols/systemd/user_session.rs
  44. 96
      src/symbols/tls/csr.rs
  45. 76
      src/symbols/tls/key.rs
  46. 6
      src/symbols/tls/mod.rs
  47. 119
      src/symbols/tls/self_signed_cert.rs
  48. 187
      src/symbols/user.rs
  49. 7
      src/symbols/wordpress/mod.rs
  50. 39
      src/symbols/wordpress/plugin.rs
  51. 48
      src/symbols/wordpress/translation.rs
  52. 3
      src/templates/mod.rs
  53. 13
      src/templates/nginx/mod.rs
  54. 202
      src/templates/nginx/server.rs
  55. 24
      src/templates/php.rs
  56. 45
      src/templates/systemd.rs
  57. 25
      src/to_artifact.rs
  58. 27
      static_files/lets_encrypt_x3_cross_signed.pem
  59. 11
      tests/file.rs
  60. 72
      tests/setup.rs
  61. 16
      tests/storage.rs

1
Cargo.toml

@ -3,6 +3,7 @@ name = "schematics"
version = "0.1.0"
authors = ["Adrian Heine <mail@adrianheine.de>"]
edition = "2018"
build = "src/build.rs"
[dependencies]
users = "0.7.0"

18
src/artifacts/mod.rs

@ -0,0 +1,18 @@
use std::path::{Path as ActualPath, PathBuf};
#[derive(Clone, Debug)]
pub struct Path(pub PathBuf);
impl AsRef<ActualPath> for Path {
fn as_ref(&self) -> &ActualPath {
&self.0
}
}
#[derive(Clone, Debug)]
pub struct UserName(pub String);
#[derive(Clone, Debug)]
pub struct ServiceName(pub String);
#[derive(Clone, Debug)]
pub struct DatabaseName(pub String);

5
src/build.rs

@ -40,3 +40,8 @@ pub fn create_static_output_files(source_dir: &str) {
.unwrap();
}
}
#[allow(unused)]
fn main() {
create_static_output_files("static_files");
}

767
src/builder.rs

@ -0,0 +1,767 @@
use crate::command_runner::{SetuidCommandRunner, StdCommandRunner};
use crate::resources::{
AcmeAccountKey, AcmeChallengesDir, AcmeChallengesNginxSnippet, AcmeRootCert, AcmeUser, Cert,
CertChain, Cron, Csr, DefaultServer, Dir, File, GitCheckout, Key, KeyAndCertBundle,
LoadedDirectory, MariaDbDatabase, MariaDbUser, NpmInstall, Owner, PhpFpmPool, Resource,
ServeCustom, ServePhp, ServeRedir, ServeService, ServeStatic, StoredDirectory,
SystemdSocketService, User, UserForDomain, WordpressPlugin, WordpressTranslation,
};
use crate::static_files::LETS_ENCRYPT_X3_CROSS_SIGNED;
use crate::storage::SimpleStorage;
use crate::symbols::acme::Cert as CertSymbol;
use crate::symbols::concat::Concat as ConcatSymbol;
use crate::symbols::cron::Cron as CronSymbol;
use crate::symbols::dir::Dir as DirSymbol;
use crate::symbols::file::File as FileSymbol;
use crate::symbols::git::Checkout as GitCheckoutSymbol;
use crate::symbols::mariadb::{
Database as MariaDbDatabaseSymbol, Dump as MariaDbDumpSymbol, User as MariaDbUserSymbol,
};
use crate::symbols::npm::Install as NpmInstallSymbol;
use crate::symbols::owner::Owner as OwnerSymbol;
use crate::symbols::saved_directory::{SavedDirectory as SavedDirectorySymbol, StorageDirection};
use crate::symbols::systemd::{
ReloadService as ReloadServiceSymbol, UserService as UserServiceSymbol,
UserSession as SystemdUserSessionSymbol,
};
use crate::symbols::tls::Csr as CsrSymbol;
use crate::symbols::tls::Key as KeySymbol;
use crate::symbols::user::User as UserSymbol;
use crate::symbols::wordpress::{
Plugin as WordpressPluginSymbol, Translation as WordpressTranslationSymbol,
};
use crate::templates::nginx;
use crate::templates::php::fpm_pool_config as php_fpm_pool_config;
use crate::templates::systemd::{
nodejs_service as systemd_nodejs_service, socket_service as systemd_socket_service,
};
use crate::to_artifact::ToArtifact;
use std::fmt::Display;
use std::path::{Path, PathBuf};
pub trait SymbolBuilder<R> {
type Prerequisites: ToArtifact;
fn prerequisites(resource: &R) -> Self::Prerequisites;
type Symbol;
fn create(
resource: &R,
target: &R::Artifact,
inputs: <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol
where
R: Resource;
}
pub struct DefaultBuilder;
impl<D> SymbolBuilder<Key<D>> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &Key<D>) -> Self::Prerequisites {}
type Symbol = KeySymbol<StdCommandRunner, PathBuf>;
fn create(
_resource: &Key<D>,
target: &<Key<D> as Resource>::Artifact,
(): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
KeySymbol::new(StdCommandRunner, target.0.clone())
}
}
impl<D: Clone> SymbolBuilder<Csr<D>> for DefaultBuilder {
type Prerequisites = Key<D>;
fn prerequisites(resource: &Csr<D>) -> Self::Prerequisites {
Key(resource.0.clone())
}
type Symbol = CsrSymbol<StdCommandRunner, D, PathBuf, PathBuf>;
fn create(
resource: &Csr<D>,
target: &<Csr<D> as Resource>::Artifact,
key: <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
CsrSymbol::new(
StdCommandRunner,
resource.0.clone(),
key.0,
target.0.clone(),
)
}
}
impl<D: Clone> SymbolBuilder<Cert<D>> for DefaultBuilder {
type Prerequisites = (
Csr<D>,
AcmeRootCert,
AcmeAccountKey,
AcmeChallengesDir,
AcmeUser,
DefaultServer,
);
fn prerequisites(resource: &Cert<D>) -> Self::Prerequisites {
(
Csr(resource.0.clone()),
AcmeRootCert,
AcmeAccountKey,
AcmeChallengesDir,
AcmeUser,
DefaultServer,
)
}
type Symbol = CertSymbol<
SetuidCommandRunner<'static, String, StdCommandRunner>,
SetuidCommandRunner<'static, String, StdCommandRunner>,
D,
PathBuf,
>;
fn create(
resource: &Cert<D>,
target: &<Cert<D> as Resource>::Artifact,
(csr, root_cert, account_key, challenges_dir, user_name, _): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
CertSymbol::new(
resource.0.clone(),
SetuidCommandRunner::new(user_name.0, &StdCommandRunner),
root_cert.0,
account_key.0,
challenges_dir.0,
csr.0,
target.0.clone(),
)
}
}
impl<D: Clone> SymbolBuilder<CertChain<D>> for DefaultBuilder {
type Prerequisites = (Cert<D>, AcmeRootCert);
fn prerequisites(resource: &CertChain<D>) -> Self::Prerequisites {
(Cert(resource.0.clone()), AcmeRootCert)
}
type Symbol = ConcatSymbol<[PathBuf; 2], PathBuf, PathBuf>;
fn create(
_resource: &CertChain<D>,
target: &<CertChain<D> as Resource>::Artifact,
(cert, root_cert): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
ConcatSymbol::new([cert.0, root_cert.0], target.0.clone())
}
}
impl<D: Clone> SymbolBuilder<KeyAndCertBundle<D>> for DefaultBuilder {
type Prerequisites = (CertChain<D>, Key<D>);
fn prerequisites(resource: &KeyAndCertBundle<D>) -> Self::Prerequisites {
(CertChain(resource.0.clone()), Key(resource.0.clone()))
}
type Symbol = ConcatSymbol<[PathBuf; 2], PathBuf, PathBuf>;
fn create(
_resource: &KeyAndCertBundle<D>,
target: &<KeyAndCertBundle<D> as Resource>::Artifact,
(cert_chain, key): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
ConcatSymbol::new([cert_chain.0, key.0], target.0.clone())
}
}
impl<P: AsRef<Path> + Clone> SymbolBuilder<File<P>> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &File<P>) -> Self::Prerequisites {}
type Symbol = FileSymbol<P, String>;
fn create(
resource: &File<P>,
_target: &<File<P> as Resource>::Artifact,
(): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
FileSymbol::new(resource.0.clone(), resource.1.clone())
}
}
impl<'a, P: AsRef<Path> + Clone> SymbolBuilder<GitCheckout<'a, P>> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &GitCheckout<'a, P>) -> Self::Prerequisites {}
type Symbol = GitCheckoutSymbol<StdCommandRunner, StdCommandRunner, P, &'a str, &'a str>;
fn create(
resource: &GitCheckout<'a, P>,
_target: &<GitCheckout<'a, P> as Resource>::Artifact,
(): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
GitCheckoutSymbol::new(resource.0.clone(), resource.1, resource.2, StdCommandRunner)
}
}
impl SymbolBuilder<DefaultServer> for DefaultBuilder {
type Prerequisites = AcmeChallengesNginxSnippet;
fn prerequisites(_resource: &DefaultServer) -> Self::Prerequisites {
AcmeChallengesNginxSnippet
}
type Symbol = (
FileSymbol<PathBuf, String>,
ReloadServiceSymbol<StdCommandRunner, StdCommandRunner, &'static str>,
);
fn create(
_resource: &DefaultServer,
target: &<DefaultServer as Resource>::Artifact,
challenges_snippet_path: <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
(
FileSymbol::new(
target.0.clone(),
nginx::default_server(challenges_snippet_path),
),
ReloadServiceSymbol::new(StdCommandRunner, "nginx"),
)
}
}
impl<D: AsRef<str> + Clone + Display> SymbolBuilder<ServeCustom<D>> for DefaultBuilder {
type Prerequisites = (CertChain<D>, Key<D>, AcmeChallengesNginxSnippet);
fn prerequisites(resource: &ServeCustom<D>) -> Self::Prerequisites {
(
CertChain(resource.0.clone()),
Key(resource.0.clone()),
AcmeChallengesNginxSnippet,
)
}
type Symbol = (
FileSymbol<PathBuf, String>,
ReloadServiceSymbol<StdCommandRunner, StdCommandRunner, &'static str>,
);
fn create(
resource: &ServeCustom<D>,
target: &<ServeCustom<D> as Resource>::Artifact,
(cert, key, challenges_snippet_path): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
(
FileSymbol::new(
target.0.clone(),
nginx::server_config(
&resource.0,
cert.0,
key.0,
&resource.1,
challenges_snippet_path,
),
),
ReloadServiceSymbol::new(StdCommandRunner, "nginx"),
)
}
}
impl<D: Clone + Display, P: AsRef<Path>> SymbolBuilder<ServePhp<D, P>> for DefaultBuilder {
type Prerequisites = (
PhpFpmPool<D>,
CertChain<D>,
Key<D>,
AcmeChallengesNginxSnippet,
);
fn prerequisites(resource: &ServePhp<D, P>) -> Self::Prerequisites {
(
PhpFpmPool(resource.0.clone(), 10),
CertChain(resource.0.clone()),
Key(resource.0.clone()),
AcmeChallengesNginxSnippet,
)
}
type Symbol = (
FileSymbol<PathBuf, String>,
ReloadServiceSymbol<StdCommandRunner, StdCommandRunner, &'static str>,
);
fn create(
resource: &ServePhp<D, P>,
target: &<ServePhp<D, P> as Resource>::Artifact,
(pool, cert, key, challenges_snippet_path): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
(
FileSymbol::new(
target.0.clone(),
nginx::server_config(
&resource.0,
cert.0,
key.0,
nginx::php_snippet(resource.2, &pool.0, &resource.1) + &resource.3,
challenges_snippet_path,
),
),
ReloadServiceSymbol::new(StdCommandRunner, "nginx"),
)
}
}
impl<D: Clone + Display, P: Clone + AsRef<Path>> SymbolBuilder<ServeService<D, P>>
for DefaultBuilder
{
type Prerequisites = (
SystemdSocketService<D, P>,
CertChain<D>,
Key<D>,
AcmeChallengesNginxSnippet,
);
fn prerequisites(resource: &ServeService<D, P>) -> Self::Prerequisites {
(
SystemdSocketService(
resource.0.clone(),
resource.1,
resource.2.clone(),
resource.4.clone(),
resource.5,
),
CertChain(resource.0.clone()),
Key(resource.0.clone()),
AcmeChallengesNginxSnippet,
)
}
type Symbol = (
FileSymbol<PathBuf, String>,
ReloadServiceSymbol<StdCommandRunner, StdCommandRunner, &'static str>,
);
fn create(
resource: &ServeService<D, P>,
target: &<ServeService<D, P> as Resource>::Artifact,
(socket, cert, key, challenges_snippet_path): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
(
FileSymbol::new(
target.0.clone(),
nginx::server_config(
&resource.0,
cert.0,
key.0,
nginx::proxy_snippet(&socket.0, &resource.3),
challenges_snippet_path,
),
),
ReloadServiceSymbol::new(StdCommandRunner, "nginx"),
)
}
}
impl<D: AsRef<str> + Clone + Display> SymbolBuilder<ServeRedir<D>> for DefaultBuilder {
type Prerequisites = (CertChain<D>, Key<D>, AcmeChallengesNginxSnippet);
fn prerequisites(resource: &ServeRedir<D>) -> Self::Prerequisites {
(
CertChain(resource.0.clone()),
Key(resource.0.clone()),
AcmeChallengesNginxSnippet,
)
}
type Symbol = (
FileSymbol<PathBuf, String>,
ReloadServiceSymbol<StdCommandRunner, StdCommandRunner, &'static str>,
);
fn create(
resource: &ServeRedir<D>,
target: &<ServeRedir<D> as Resource>::Artifact,
(cert, key, challenges_snippet_path): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
(
FileSymbol::new(
target.0.clone(),
nginx::server_config(
&resource.0,
cert.0,
key.0,
nginx::redir_snippet(resource.1.as_ref()),
challenges_snippet_path,
),
),
ReloadServiceSymbol::new(StdCommandRunner, "nginx"),
)
}
}
impl<D: AsRef<str> + Clone + Display, P: AsRef<Path>> SymbolBuilder<ServeStatic<D, P>>
for DefaultBuilder
{
type Prerequisites = (CertChain<D>, Key<D>, AcmeChallengesNginxSnippet);
fn prerequisites(resource: &ServeStatic<D, P>) -> Self::Prerequisites {
(
CertChain(resource.0.clone()),
Key(resource.0.clone()),
AcmeChallengesNginxSnippet,
)
}
type Symbol = (
FileSymbol<PathBuf, String>,
ReloadServiceSymbol<StdCommandRunner, StdCommandRunner, &'static str>,
);
fn create(
resource: &ServeStatic<D, P>,
target: &<ServeStatic<D, P> as Resource>::Artifact,
(cert, key, challenges_snippet_path): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
(
FileSymbol::new(
target.0.clone(),
nginx::server_config(
&resource.0,
cert.0,
key.0,
nginx::static_snippet(resource.1.as_ref()),
challenges_snippet_path,
),
),
ReloadServiceSymbol::new(StdCommandRunner, "nginx"),
)
}
}
impl<D: Clone> SymbolBuilder<PhpFpmPool<D>> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &PhpFpmPool<D>) -> Self::Prerequisites {}
type Symbol = (
FileSymbol<PathBuf, String>,
ReloadServiceSymbol<StdCommandRunner, StdCommandRunner, String>,
);
fn create(
resource: &PhpFpmPool<D>,
(socket_path, conf_path, user_name, service_name): &<PhpFpmPool<D> as Resource>::Artifact,
(): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
(
FileSymbol::new(
conf_path.0.clone(),
php_fpm_pool_config(&user_name.0, &socket_path.0, resource.1),
),
ReloadServiceSymbol::new(StdCommandRunner, service_name.0.clone()),
)
}
}
impl<D, P: AsRef<Path>> SymbolBuilder<SystemdSocketService<D, P>> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &SystemdSocketService<D, P>) -> Self::Prerequisites {}
type Symbol = (
FileSymbol<PathBuf, String>,
SystemdUserSessionSymbol<'static, String, StdCommandRunner>,
UserServiceSymbol<'static, PathBuf, String, StdCommandRunner>,
);
fn create(
resource: &SystemdSocketService<D, P>,
(socket_path, conf_path, user_name): &<SystemdSocketService<D, P> as Resource>::Artifact,
(): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
(
FileSymbol::new(
conf_path.0.clone(),
if resource.4 {
systemd_nodejs_service(&resource.2, socket_path, &resource.3)
} else {
systemd_socket_service(
socket_path,
resource.2.as_ref().to_str().unwrap(),
&resource.3,
"",
)
},
),
SystemdUserSessionSymbol::new(user_name.0.clone(), &StdCommandRunner),
UserServiceSymbol::new(
socket_path.0.clone(),
user_name.0.clone(),
resource.1,
&StdCommandRunner,
),
)
}
}
impl<P: Clone> SymbolBuilder<Dir<P>> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &Dir<P>) -> Self::Prerequisites {}
type Symbol = DirSymbol<P>;
fn create(
resource: &Dir<P>,
_target: &<Dir<P> as Resource>::Artifact,
(): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
DirSymbol::new(resource.0.clone())
}
}
impl<P: Clone + AsRef<Path>> SymbolBuilder<NpmInstall<P>> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &NpmInstall<P>) -> Self::Prerequisites {}
type Symbol = NpmInstallSymbol<'static, P, StdCommandRunner>;
fn create(
resource: &NpmInstall<P>,
_target: &<NpmInstall<P> as Resource>::Artifact,
(): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
NpmInstallSymbol::new(resource.0.clone(), &StdCommandRunner)
}
}
impl<P: Clone + AsRef<Path>> SymbolBuilder<StoredDirectory<P>> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &StoredDirectory<P>) -> Self::Prerequisites {}
type Symbol = SavedDirectorySymbol<StdCommandRunner, StdCommandRunner, P, SimpleStorage>;
fn create(
resource: &StoredDirectory<P>,
target: &<StoredDirectory<P> as Resource>::Artifact,
(): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
SavedDirectorySymbol::new(
resource.1.clone(),
SimpleStorage::new(target.0.clone()),
StorageDirection::Store,
StdCommandRunner,
)
}
}
impl<P: Clone + AsRef<Path>> SymbolBuilder<LoadedDirectory<P>> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &LoadedDirectory<P>) -> Self::Prerequisites {}
type Symbol = SavedDirectorySymbol<StdCommandRunner, StdCommandRunner, P, SimpleStorage>;
fn create(
resource: &LoadedDirectory<P>,
target: &<LoadedDirectory<P> as Resource>::Artifact,
(): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
SavedDirectorySymbol::new(
resource.1.clone(),
SimpleStorage::new(target.0.clone()),
StorageDirection::Load,
StdCommandRunner,
)
}
}
impl<D: Clone> SymbolBuilder<UserForDomain<D>> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &UserForDomain<D>) -> Self::Prerequisites {}
type Symbol = UserSymbol<String, StdCommandRunner>;
fn create(
_resource: &UserForDomain<D>,
(user_name, _home_path): &<UserForDomain<D> as Resource>::Artifact,
(): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
UserSymbol::new(user_name.0.clone(), StdCommandRunner)
}
}
impl SymbolBuilder<User> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &User) -> Self::Prerequisites {}
type Symbol = UserSymbol<String, StdCommandRunner>;
fn create(
resource: &User,
(): &<User as Resource>::Artifact,
(): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
UserSymbol::new(resource.0.clone(), StdCommandRunner)
}
}
impl<P: AsRef<Path> + Clone> SymbolBuilder<Owner<P>> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &Owner<P>) -> Self::Prerequisites {}
type Symbol = OwnerSymbol<StdCommandRunner, StdCommandRunner, P, String>;
fn create(
resource: &Owner<P>,
(): &<Owner<P> as Resource>::Artifact,
(): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
OwnerSymbol::new(resource.1.clone(), resource.0.clone(), StdCommandRunner)
}
}
impl SymbolBuilder<AcmeUser> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &AcmeUser) -> Self::Prerequisites {}
type Symbol = UserSymbol<String, StdCommandRunner>;
fn create(
_resource: &AcmeUser,
user_name: &<AcmeUser as Resource>::Artifact,
(): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
UserSymbol::new(user_name.0.clone(), StdCommandRunner)
}
}
impl SymbolBuilder<AcmeChallengesDir> for DefaultBuilder {
type Prerequisites = AcmeUser;
fn prerequisites(_resource: &AcmeChallengesDir) -> Self::Prerequisites {
AcmeUser
}
type Symbol = (
DirSymbol<PathBuf>,
OwnerSymbol<StdCommandRunner, StdCommandRunner, PathBuf, String>,
);
fn create(
_resource: &AcmeChallengesDir,
target: &<AcmeChallengesDir as Resource>::Artifact,
user_name: <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
(
DirSymbol::new(target.0.clone()),
OwnerSymbol::new(target.0.clone(), user_name.0, StdCommandRunner),
)
}
}
impl SymbolBuilder<AcmeChallengesNginxSnippet> for DefaultBuilder {
type Prerequisites = AcmeChallengesDir;
fn prerequisites(_resource: &AcmeChallengesNginxSnippet) -> Self::Prerequisites {
AcmeChallengesDir
}
type Symbol = FileSymbol<PathBuf, String>;
fn create(
_resource: &AcmeChallengesNginxSnippet,
target: &<AcmeChallengesNginxSnippet as Resource>::Artifact,
challenges_dir: <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
FileSymbol::new(
target.0.clone(),
nginx::acme_challenges_snippet(challenges_dir),
)
}
}
impl SymbolBuilder<AcmeAccountKey> for DefaultBuilder {
type Prerequisites = AcmeUser;
fn prerequisites(_resource: &AcmeAccountKey) -> Self::Prerequisites {
AcmeUser
}
type Symbol = (
KeySymbol<StdCommandRunner, PathBuf>,
OwnerSymbol<StdCommandRunner, StdCommandRunner, PathBuf, String>,
);
fn create(
_resource: &AcmeAccountKey,
target: &<AcmeAccountKey as Resource>::Artifact,
user_name: <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
(
KeySymbol::new(StdCommandRunner, target.0.clone()),
OwnerSymbol::new(target.0.clone(), user_name.0, StdCommandRunner),
)
}
}
impl SymbolBuilder<AcmeRootCert> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &AcmeRootCert) -> Self::Prerequisites {}
type Symbol = FileSymbol<PathBuf, &'static str>;
fn create(
_resource: &AcmeRootCert,
target: &<AcmeRootCert as Resource>::Artifact,
(): <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
FileSymbol::new(target.0.clone(), LETS_ENCRYPT_X3_CROSS_SIGNED)
}
}
impl<D> SymbolBuilder<MariaDbUser<D>> for DefaultBuilder {
type Prerequisites = ();
fn prerequisites(_resource: &MariaDbUser<D>) -> Self::Prerequisites {}
type Symbol = MariaDbUserSymbol<'static, String, StdCommandRunner>;
fn create(
_resource: &MariaDbUser<D>,
user_name: &<MariaDbUser<D> as Resource>::Artifact,
_: <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
MariaDbUserSymbol::new(user_name.0.clone(), &StdCommandRunner)
}
}
impl<D: Clone> SymbolBuilder<MariaDbDatabase<D>> for DefaultBuilder {
type Prerequisites = MariaDbUser<D>;
fn prerequisites(resource: &MariaDbDatabase<D>) -> Self::Prerequisites {
MariaDbUser(resource.0.clone())
}
type Symbol = (
MariaDbDatabaseSymbol<'static, String, SimpleStorage, StdCommandRunner>,
MariaDbDumpSymbol<'static, String, StdCommandRunner, SimpleStorage>,
);
fn create(
_resource: &MariaDbDatabase<D>,
(db_name, _, data_path): &<MariaDbDatabase<D> as Resource>::Artifact,
_: <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
let db_dump = SimpleStorage::new(data_path.0.clone());
(
MariaDbDatabaseSymbol::new(db_name.0.clone(), db_dump.clone(), &StdCommandRunner),
MariaDbDumpSymbol::new(db_name.0.clone(), db_dump, &StdCommandRunner),
)
}
}
impl<P: Clone + AsRef<Path>> SymbolBuilder<WordpressPlugin<P>> for DefaultBuilder {
type Prerequisites = Dir<PathBuf>;
fn prerequisites(resource: &WordpressPlugin<P>) -> Self::Prerequisites {
Dir(resource.0.as_ref().join("wp-content/plugins"))
}
type Symbol = WordpressPluginSymbol<'static, P, &'static str, StdCommandRunner>;
fn create(
resource: &WordpressPlugin<P>,
(): &<WordpressPlugin<P> as Resource>::Artifact,
_: <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
WordpressPluginSymbol::new(resource.0.clone(), resource.1, &StdCommandRunner)
}
}
impl<P: Clone + AsRef<Path>> SymbolBuilder<WordpressTranslation<P>> for DefaultBuilder {
type Prerequisites = Dir<PathBuf>;
fn prerequisites(resource: &WordpressTranslation<P>) -> Self::Prerequisites {
Dir(resource.0.as_ref().join("wp-content/languages"))
}
type Symbol = WordpressTranslationSymbol<'static, &'static str, PathBuf, StdCommandRunner>;
fn create(
resource: &WordpressTranslation<P>,
(): &<WordpressTranslation<P> as Resource>::Artifact,
_: <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
WordpressTranslationSymbol::new(
resource.0.as_ref().join("wp-content/languages"),
resource.1,
resource.2,
&StdCommandRunner,
)
}
}
impl<D: Clone> SymbolBuilder<Cron<D>> for DefaultBuilder {
type Prerequisites = UserForDomain<D>;
fn prerequisites(resource: &Cron<D>) -> Self::Prerequisites {
UserForDomain(resource.0.clone())
}
type Symbol = CronSymbol<'static, String, String, StdCommandRunner>;
fn create(
resource: &Cron<D>,
(): &<Cron<D> as Resource>::Artifact,
user_name: <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
CronSymbol::new((user_name.0).0, resource.1.clone(), &StdCommandRunner)
}
}

125
src/factory.rs

@ -1,125 +0,0 @@
use regex::Regex;
use crate::command_runner::CommandRunner;
use crate::loggers::StdErrLogger;
use crate::repository::SymbolRepository;
use crate::resources::Resource;
use crate::schema::{
NonRepeatingSymbolRunner, ReportingSymbolRunner, RequirementsResolvingSymbolRunner,
};
use crate::symbols::dir::Dir;
use crate::symbols::list::List;
use crate::symbols::owner::Owner;
use crate::symbols::systemd::user_session::SystemdUserSession;
use crate::symbols::tls::{TlsCsr, TlsKey};
use crate::symbols::user::SystemUserAdder;
use crate::symbols::user::{User, UserAdder};
use crate::symbols::{Symbol, SymbolRunner};
#[derive(Default)]
pub struct Factory {}
impl Factory {
pub fn new() -> Self {
Self::default()
}
pub fn get_repo<'a, CR: CommandRunner>(
&self,
command_runner: &'a CR,
) -> DefaultSymbolRepository<'a, SystemUserAdder<'a, CR>, CR> {
DefaultSymbolRepository::new(command_runner)
}
pub fn get_symbol_runner<'a, RUNNER: SymbolRunner, REPO: SymbolRepository<'a>>(
&self,
symbol_runner: &'a RUNNER,
repo: &'a REPO,
) -> Box<dyn 'a + SymbolRunner> {
let runner1 = ReportingSymbolRunner::new(symbol_runner, StdErrLogger);
let runner2 = NonRepeatingSymbolRunner::new(runner1);
Box::new(RequirementsResolvingSymbolRunner::new(runner2, repo))
}
}
pub struct DefaultSymbolRepository<'a, A: 'a + UserAdder, C: CommandRunner> {
user_adder: A,
command_runner: &'a C,
home: Regex,
home_config: Regex,
csr: Regex,
private_key: Regex,
systemd_linger: Regex,
}
impl<'a, C: 'a + CommandRunner> DefaultSymbolRepository<'a, SystemUserAdder<'a, C>, C> {
pub fn new(command_runner: &'a C) -> Self {
Self {
command_runner,
user_adder: SystemUserAdder::new(command_runner),
home: Regex::new("^/home/([^/]+)$").unwrap(),
home_config: Regex::new("^/home/([^/]+)/.config(?:/|$)").unwrap(),
csr: Regex::new("^/etc/ssl/local_certs/([^/]+).csr$").unwrap(),
private_key: Regex::new("^/etc/ssl/private/([^/]+).key$").unwrap(),
systemd_linger: Regex::new("^/var/lib/systemd/linger/([^/]+)$").unwrap(),
}
}
}
impl<'a, C: CommandRunner> SymbolRepository<'a>
for DefaultSymbolRepository<'a, SystemUserAdder<'a, C>, C>
{
fn get_symbol(&'a self, resource: &Resource) -> Option<Box<dyn Symbol + 'a>> {
match resource.get_type() {
"user" => Some(Box::new(User::new(
resource.get_value().to_string().into(),
self.command_runner,
&self.user_adder,
))),
"dir" => {
let value = resource.get_value();
Some(if let Some(matches) = self.home_config.captures(value) {
Box::new(List::new(vec![
Box::new(Dir::new(value.to_string())),
Box::new(Owner::new(
value.to_string(),
matches[1].to_string(),
self.command_runner,
)),
]))
} else if let Some(matches) = self.home.captures(value) {
Box::new(User::new(
matches[1].to_string().into(),
self.command_runner,
&self.user_adder,
))
} else {
Box::new(Dir::new(value.to_string()))
})
}
"file" => {
let value = resource.get_value();
if let Some(matches) = self.csr.captures(value) {
Some(Box::new(TlsCsr::new(
matches[1].to_string().into(),
self.command_runner,
)))
} else if let Some(matches) = self.private_key.captures(value) {
Some(Box::new(TlsKey::new(
matches[1].to_string().into(),
self.command_runner,
)))
} else if let Some(matches) = self.systemd_linger.captures(value) {
Some(Box::new(SystemdUserSession::new(
matches[1].to_string(),
self.command_runner,
)))
} else {
None
}
}
_ => None,
}
}
}

16
src/lib.rs

@ -1,4 +1,4 @@
#![deny(
#![warn(
macro_use_extern_crate,
meta_variable_misuse,
non_ascii_idents,
@ -32,10 +32,20 @@ pub mod bin;
pub mod build;
#[macro_use]
pub mod command_runner;
pub mod factory;
pub mod loggers;
pub mod repository;
pub mod resources;
pub mod schema;
pub mod storage;
pub mod symbols;
pub mod templates;
mod artifacts;
mod builder;
mod locator;
mod setup;
pub mod static_files;
mod to_artifact;
pub use builder::{DefaultBuilder, SymbolBuilder};
pub use locator::{DefaultLocator, DefaultPolicy, Policy, ResourceLocator};
pub use setup::Setup;

443
src/locator.rs

@ -0,0 +1,443 @@
use crate::artifacts::{
DatabaseName as DatabaseNameArtifact, Path as PathArtifact, ServiceName as ServiceNameArtifact,
UserName as UserNameArtifact,
};
use crate::resources::{
AcmeAccountKey, AcmeChallengesDir, AcmeChallengesNginxSnippet, AcmeRootCert, AcmeUser, Cert,
CertChain, Cron, Csr, DefaultServer, Dir, File, GitCheckout, Key, KeyAndCertBundle,
LoadedDirectory, MariaDbDatabase, MariaDbUser, NpmInstall, Owner, PhpFpmPool, Resource,
ServeCustom, ServePhp, ServeRedir, ServeService, ServeStatic, StoredDirectory,
SystemdSocketService, User, UserForDomain, WordpressPlugin, WordpressTranslation,
};
use crate::to_artifact::ToArtifact;
use std::fmt::Display;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
pub trait Policy {
fn acme_user() -> &'static str {
"acme"
}
fn user_home(user_name: &str) -> PathBuf {
format!("/home/{}", user_name).into()
}
fn user_name_for_domain(domain_name: &'_ str) -> String {
domain_name.split('.').rev().fold(String::new(), |result, part| if result.is_empty() { result } else { result + "_" } + part)
}
fn php_version() -> &'static str {
"7.0"
}
fn path_for_data(name: impl Display) -> PathBuf {
("/root/data".as_ref() as &Path).join(format!("_{}", name))
}
}
pub struct DefaultPolicy;
impl Policy for DefaultPolicy {}
pub trait ResourceLocator<R> {
type Prerequisites: ToArtifact;
fn locate(resource: &R) -> (R::Artifact, Self::Prerequisites)
where
R: Resource;
}
pub struct DefaultLocator<P = DefaultPolicy> {
phantom: PhantomData<P>,
}
impl<P, D: AsRef<str>> ResourceLocator<Key<D>> for DefaultLocator<P> {
type Prerequisites = Dir<PathBuf>;
fn locate(resource: &Key<D>) -> (<Key<D> as Resource>::Artifact, Self::Prerequisites) {
(
PathArtifact(format!("/etc/ssl/private/{}.key", resource.0.as_ref()).into()),
Dir("/etc/ssl/private".into()),
)
}
}
impl<P, D: AsRef<str>> ResourceLocator<Csr<D>> for DefaultLocator<P> {
type Prerequisites = Dir<PathBuf>;
fn locate(resource: &Csr<D>) -> (<Csr<D> as Resource>::Artifact, Self::Prerequisites) {
(
PathArtifact(format!("/etc/ssl/local_certs/{}.csr", resource.0.as_ref()).into()),
Dir("/etc/ssl/local_certs".into()),
)
}
}
impl<P, D: AsRef<str>> ResourceLocator<Cert<D>> for DefaultLocator<P> {
type Prerequisites = Dir<PathBuf>;
fn locate(resource: &Cert<D>) -> (<Cert<D> as Resource>::Artifact, Self::Prerequisites) {
(
PathArtifact(format!("/etc/ssl/local_certs/{}.crt", resource.0.as_ref()).into()),
Dir("/etc/ssl/local_certs".into()),
)
}
}
impl<P, D: AsRef<str>> ResourceLocator<CertChain<D>> for DefaultLocator<P> {
type Prerequisites = Dir<PathBuf>;
fn locate(
resource: &CertChain<D>,
) -> (<CertChain<D> as Resource>::Artifact, Self::Prerequisites) {
(
PathArtifact(format!("/etc/ssl/local_certs/{}.chained.crt", resource.0.as_ref()).into()),
Dir("/etc/ssl/local_certs".into()),
)
}
}
impl<P, D: AsRef<str>> ResourceLocator<KeyAndCertBundle<D>> for DefaultLocator<P> {
type Prerequisites = Dir<PathBuf>;
fn locate(
resource: &KeyAndCertBundle<D>,
) -> (
<KeyAndCertBundle<D> as Resource>::Artifact,
Self::Prerequisites,
) {
(
PathArtifact(format!("/etc/ssl/private/{}.with_key.crt", resource.0.as_ref()).into()),
Dir("/etc/ssl/private".into()),
)
}
}
impl<'a, POLICY, P: AsRef<Path>> ResourceLocator<File<P>> for DefaultLocator<POLICY> {
type Prerequisites = Dir<PathBuf>;
fn locate(resource: &File<P>) -> (<File<P> as Resource>::Artifact, Self::Prerequisites) {
((), Dir(resource.0.as_ref().parent().unwrap().into()))
}
}
impl<'a, POLICY, P: AsRef<Path>> ResourceLocator<GitCheckout<'a, P>> for DefaultLocator<POLICY> {
type Prerequisites = Dir<PathBuf>;
fn locate(
resource: &GitCheckout<'a, P>,
) -> (
<GitCheckout<'a, P> as Resource>::Artifact,
Self::Prerequisites,
) {
((), Dir(resource.0.as_ref().parent().unwrap().into()))
}
}
impl<POLICY, P: Clone + AsRef<Path>> ResourceLocator<Dir<P>> for DefaultLocator<POLICY> {
type Prerequisites = Option<Dir<PathBuf>>;
fn locate(resource: &Dir<P>) -> (<Dir<P> as Resource>::Artifact, Self::Prerequisites) {
((), resource.0.as_ref().parent().map(|p| Dir(p.into())))
}
}
impl<POLICY, P> ResourceLocator<NpmInstall<P>> for DefaultLocator<POLICY> {
type Prerequisites = ();
fn locate(
_resource: &NpmInstall<P>,
) -> (<NpmInstall<P> as Resource>::Artifact, Self::Prerequisites) {
((), ())
}
}
impl<POLICY: Policy, P: AsRef<Path>> ResourceLocator<StoredDirectory<P>>
for DefaultLocator<POLICY>
{
type Prerequisites = ();
fn locate(
resource: &StoredDirectory<P>,
) -> (
<StoredDirectory<P> as Resource>::Artifact,
Self::Prerequisites,
) {
(PathArtifact(POLICY::path_for_data(resource.0)), ())
}
}
impl<POLICY: Policy, P: AsRef<Path>> ResourceLocator<LoadedDirectory<P>>
for DefaultLocator<POLICY>
{
type Prerequisites = Dir<PathBuf>;
fn locate(
resource: &LoadedDirectory<P>,
) -> (
<LoadedDirectory<P> as Resource>::Artifact,
Self::Prerequisites,
) {
(
PathArtifact(POLICY::path_for_data(resource.0)),
Dir(resource.1.as_ref().parent().unwrap().into()),
)
}
}
impl<P: Policy> ResourceLocator<AcmeAccountKey> for DefaultLocator<P> {
type Prerequisites = Dir<PathBuf>;
fn locate(
_resource: &AcmeAccountKey,
) -> (<AcmeAccountKey as Resource>::Artifact, Self::Prerequisites) {
let acme_user = P::acme_user();
let home = P::user_home(acme_user);
(PathArtifact(home.join("account.key")), Dir(home))
}
}
impl<P: Policy> ResourceLocator<AcmeUser> for DefaultLocator<P> {
type Prerequisites = ();
fn locate(_resource: &AcmeUser) -> (<AcmeUser as Resource>::Artifact, Self::Prerequisites) {
let acme_user = P::acme_user();
(UserNameArtifact(acme_user.into()), ())
}
}
impl<P: Policy> ResourceLocator<AcmeChallengesDir> for DefaultLocator<P> {
type Prerequisites = Dir<PathBuf>;
fn locate(
_resource: &AcmeChallengesDir,
) -> (
<AcmeChallengesDir as Resource>::Artifact,
Self::Prerequisites,
) {
let acme_user = P::acme_user();
let home = P::user_home(acme_user);
(PathArtifact(home.join("challenges")), Dir(home))
}
}
impl<P: Policy> ResourceLocator<AcmeChallengesNginxSnippet> for DefaultLocator<P> {
type Prerequisites = ();
fn locate(
_resource: &AcmeChallengesNginxSnippet,
) -> (
<AcmeChallengesNginxSnippet as Resource>::Artifact,
Self::Prerequisites,
) {
(
PathArtifact("/etc/nginx/snippets/acme-challenge.conf".into()),
(),
)
}
}
impl<P: Policy> ResourceLocator<AcmeRootCert> for DefaultLocator<P> {
type Prerequisites = Dir<PathBuf>;
fn locate(
_resource: &AcmeRootCert,
) -> (<AcmeRootCert as Resource>::Artifact, Self::Prerequisites) {
let acme_user = P::acme_user();
let home = P::user_home(acme_user);
(
PathArtifact(home.join("lets_encrypt_x3_cross_signed.pem")),
Dir(home),
)
}
}
impl<P: Policy, D: AsRef<str>> ResourceLocator<UserForDomain<D>> for DefaultLocator<P> {
type Prerequisites = ();
fn locate(
resource: &UserForDomain<D>,
) -> (
<UserForDomain<D> as Resource>::Artifact,
Self::Prerequisites,
) {
let user_name = P::user_name_for_domain(resource.0.as_ref());
let home = P::user_home(&user_name);
((UserNameArtifact(user_name), PathArtifact(home)), ())
}
}
impl<P> ResourceLocator<User> for DefaultLocator<P> {
type Prerequisites = ();
fn locate(_resource: &User) -> (<User as Resource>::Artifact, Self::Prerequisites) {
((), ())
}
}
impl<P, POLICY> ResourceLocator<Owner<P>> for DefaultLocator<POLICY> {
type Prerequisites = User;
fn locate(resource: &Owner<P>) -> (<Owner<P> as Resource>::Artifact, Self::Prerequisites) {
((), User(resource.0.clone()))
}
}
impl<P> ResourceLocator<DefaultServer> for DefaultLocator<P> {
type Prerequisites = ();
fn locate(
_resource: &DefaultServer,
) -> (<DefaultServer as Resource>::Artifact, Self::Prerequisites) {
(PathArtifact("/etc/nginx/sites-enabled/default".into()), ())
}
}
impl<D: AsRef<Path>, POLICY> ResourceLocator<ServeCustom<D>> for DefaultLocator<POLICY> {
type Prerequisites = ();
fn locate(
resource: &ServeCustom<D>,
) -> (<ServeCustom<D> as Resource>::Artifact, Self::Prerequisites) {
(
PathArtifact(("/etc/nginx/sites-enabled/".as_ref() as &Path).join(&resource.0)),
(),
)
}
}
impl<D: AsRef<Path>, P, POLICY> ResourceLocator<ServePhp<D, P>> for DefaultLocator<POLICY> {
type Prerequisites = ();
fn locate(
resource: &ServePhp<D, P>,
) -> (<ServePhp<D, P> as Resource>::Artifact, Self::Prerequisites) {
(
PathArtifact(("/etc/nginx/sites-enabled/".as_ref() as &Path).join(&resource.0)),
(),
)
}
}
impl<D: AsRef<Path>, P, POLICY> ResourceLocator<ServeService<D, P>> for DefaultLocator<POLICY> {
type Prerequisites = ();
fn locate(
resource: &ServeService<D, P>,
) -> (
<ServeService<D, P> as Resource>::Artifact,
Self::Prerequisites,
) {
(
PathArtifact(("/etc/nginx/sites-enabled/".as_ref() as &Path).join(&resource.0)),
(),
)
}
}
impl<D: AsRef<Path>, POLICY> ResourceLocator<ServeRedir<D>> for DefaultLocator<POLICY> {
type Prerequisites = ();
fn locate(
resource: &ServeRedir<D>,
) -> (<ServeRedir<D> as Resource>::Artifact, Self::Prerequisites) {
(
PathArtifact(("/etc/nginx/sites-enabled/".as_ref() as &Path).join(&resource.0)),
(),
)
}
}
impl<D: AsRef<Path>, P, POLICY> ResourceLocator<ServeStatic<D, P>> for DefaultLocator<POLICY> {
type Prerequisites = ();
fn locate(
resource: &ServeStatic<D, P>,
) -> (
<ServeStatic<D, P> as Resource>::Artifact,
Self::Prerequisites,
) {
(
PathArtifact(("/etc/nginx/sites-enabled/".as_ref() as &Path).join(&resource.0)),
(),
)
}
}
impl<D: Clone + AsRef<str>, P: Policy> ResourceLocator<PhpFpmPool<D>> for DefaultLocator<P> {
type Prerequisites = ();
fn locate(
resource: &PhpFpmPool<D>,
) -> (<PhpFpmPool<D> as Resource>::Artifact, Self::Prerequisites) {
let ((user, _), ()) = Self::locate(&UserForDomain(&resource.0));
let php_version = P::php_version();
(
(
PathArtifact(format!("/run/php/{}.sock", user.0).into()),
PathArtifact(format!("/etc/php/{}/fpm/pool.d/{}.conf", php_version, user.0).into()),
user,
ServiceNameArtifact(format!("php{}-fpm", php_version)),
),
(),
)
}
}
impl<D: Clone + AsRef<str>, P, POLICY: Policy> ResourceLocator<SystemdSocketService<D, P>>
for DefaultLocator<POLICY>
{
type Prerequisites = (Dir<PathBuf>, Owner<PathBuf>);
fn locate(
resource: &SystemdSocketService<D, P>,
) -> (
<SystemdSocketService<D, P> as Resource>::Artifact,
Self::Prerequisites,
) {
let ((user_name, home_path), ()) = Self::locate(&UserForDomain(&resource.0));
let config = home_path.0.join(".config");
let service_dir_path = config.join("systemd/user");
(
(
PathArtifact(format!("/var/tmp/{}-{}.socket", user_name.0, resource.1).into()),
PathArtifact(service_dir_path.join(format!("{}.service", resource.1))),
user_name.clone(),
),
(Dir(service_dir_path), Owner(user_name.0, config)),
)
}
}
impl<D: AsRef<str>, P: Policy> ResourceLocator<MariaDbDatabase<D>> for DefaultLocator<P> {
type Prerequisites = ();
fn locate(
resource: &MariaDbDatabase<D>,
) -> (
<MariaDbDatabase<D> as Resource>::Artifact,
Self::Prerequisites,
) {
let ((user_name, _), ()) = Self::locate(&UserForDomain(&resource.0));
(
(
DatabaseNameArtifact(user_name.0.clone()),
user_name.clone(),
PathArtifact(P::path_for_data(format!("{}.sql", user_name.0))),
),
(),
)
}
}
impl<D: AsRef<str>, P: Policy> ResourceLocator<MariaDbUser<D>> for DefaultLocator<P> {
type Prerequisites = ();
fn locate(
resource: &MariaDbUser<D>,
) -> (<MariaDbUser<D> as Resource>::Artifact, Self::Prerequisites) {
let ((user_name, _), ()) = Self::locate(&UserForDomain(&resource.0));
((user_name), ())
}
}
impl<P, POLICY> ResourceLocator<WordpressPlugin<P>> for DefaultLocator<POLICY> {
type Prerequisites = ();
fn locate(
_resource: &WordpressPlugin<P>,
) -> (
<WordpressPlugin<P> as Resource>::Artifact,
Self::Prerequisites,
) {
((), ())
}
}
impl<P, POLICY> ResourceLocator<WordpressTranslation<P>> for DefaultLocator<POLICY> {
type Prerequisites = ();
fn locate(
_resource: &WordpressTranslation<P>,
) -> (
<WordpressTranslation<P> as Resource>::Artifact,
Self::Prerequisites,
) {
((), ())
}
}
impl<D, P> ResourceLocator<Cron<D>> for DefaultLocator<P> {
type Prerequisites = ();
fn locate(_resource: &Cron<D>) -> (<Cron<D> as Resource>::Artifact, Self::Prerequisites) {
((), ())
}
}

36
src/repository.rs

@ -1,36 +0,0 @@
use std::collections::HashMap;
use crate::resources::Resource;
use crate::symbols::Symbol;
pub trait SymbolRepository<'a> {
fn get_symbol(&'a self, resource: &Resource) -> Option<Box<dyn Symbol + 'a>>;
}
impl<'a, C> SymbolRepository<'a> for C
where
C: Fn(&Resource) -> Option<Box<dyn Symbol + 'a>>,
{
fn get_symbol(&'a self, resource: &Resource) -> Option<Box<dyn Symbol + 'a>> {
self(resource)
}
}
pub struct DispatchingSymbolRepository<'a> {
repositories: HashMap<&'a str, Box<dyn SymbolRepository<'a> + 'a>>,
}
impl<'a> DispatchingSymbolRepository<'a> {
pub fn new(repositories: HashMap<&'a str, Box<dyn SymbolRepository<'a> + 'a>>) -> Self {
DispatchingSymbolRepository { repositories }
}
}
impl<'a> SymbolRepository<'a> for DispatchingSymbolRepository<'a> {
fn get_symbol(&'a self, resource: &Resource) -> Option<Box<dyn Symbol + 'a>> {
self
.repositories
.get(resource.get_type())
.and_then(|repo| repo.get_symbol(resource))
}
}

289
src/resources/mod.rs

@ -1,15 +1,282 @@
#[derive(PartialEq, Eq, Hash, Clone)]
pub struct Resource(pub String, pub String);
use crate::artifacts::{
DatabaseName as DatabaseNameArtifact, Path as PathArtifact, ServiceName as ServiceNameArtifact,
UserName as UserNameArtifact,
};
use std::hash::Hash;
use std::path::PathBuf;
impl<'a> Resource {
pub fn new<A: Into<String>, B: Into<String>>(rtype: A, value: B) -> Self {
Self(rtype.into(), value.into())
}
pub trait Resource {
type Artifact;
}
pub fn get_type(&self) -> &str {
&self.0
}
pub fn get_value(&self) -> &str {
&self.1
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct Key<D>(pub D);
impl<D> Resource for Key<D> {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct Csr<D>(pub D);
impl<D> Resource for Csr<D> {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct Cert<D>(pub D);
impl<D> Resource for Cert<D> {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct CertChain<D>(pub D);
impl<D> Resource for CertChain<D> {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct KeyAndCertBundle<D>(pub D);
impl<D> Resource for KeyAndCertBundle<D> {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct File<P>(pub P, pub String);
impl<'a, P> Resource for File<P> {
type Artifact = ();
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct GitCheckout<'a, P>(pub P, pub &'a str, pub &'a str);
impl<'a, P> Resource for GitCheckout<'a, P> {
type Artifact = ();
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct Dir<P>(pub P);
impl<P> Resource for Dir<P> {
type Artifact = ();
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct UserForDomain<D>(pub D);
impl<D> Resource for UserForDomain<D> {
type Artifact = (UserNameArtifact, PathArtifact);
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct AcmeAccountKey;
impl Resource for AcmeAccountKey {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct AcmeUser;
impl Resource for AcmeUser {
type Artifact = UserNameArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct AcmeRootCert;
impl Resource for AcmeRootCert {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct AcmeChallengesDir;
impl Resource for AcmeChallengesDir {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct AcmeChallengesNginxSnippet;
impl Resource for AcmeChallengesNginxSnippet {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct StoredDirectory<P>(pub &'static str, pub P);
impl<P> Resource for StoredDirectory<P> {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct LoadedDirectory<P>(pub &'static str, pub P);
impl<P> Resource for LoadedDirectory<P> {
type Artifact = PathArtifact;
}
pub fn get_saved_directory<P: Clone>(
storage_name: &'static str,
target: P,
) -> (StoredDirectory<P>, LoadedDirectory<P>) {
(
StoredDirectory(storage_name, target.clone()),
LoadedDirectory(storage_name, target),
)
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct User(pub String);
impl Resource for User {
type Artifact = ();
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct SystemdSocketService<D, P>(pub D, pub &'static str, pub P, pub P, pub bool);
impl<D, P> Resource for SystemdSocketService<D, P> {
type Artifact = (PathArtifact, PathArtifact, UserNameArtifact);
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct NpmInstall<P>(pub P);
impl<P> Resource for NpmInstall<P> {
type Artifact = ();
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct Owner<P>(pub String, pub P);
impl<P> Resource for Owner<P> {
type Artifact = ();
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct ServeCustom<D>(pub D, pub String);
impl<D> Resource for ServeCustom<D> {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct ServePhp<D, P>(pub D, pub P, pub &'static str, pub String, pub usize);
impl<D, P> Resource for ServePhp<D, P> {
type Artifact = PathArtifact;
}
// Domain, service name, exec, static, dir
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct ServeService<D, P>(pub D, pub &'static str, pub P, pub P, pub P, pub bool);
impl<D, P> Resource for ServeService<D, P> {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct ServeRedir<D>(pub D, pub D);
impl<D> Resource for ServeRedir<D> {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct ServeStatic<D, P>(pub D, pub P);
impl<D, P> Resource for ServeStatic<D, P> {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct DefaultServer;
impl Resource for DefaultServer {
type Artifact = PathArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct PhpFpmPool<D>(pub D, pub usize);
impl<D> Resource for PhpFpmPool<D> {
type Artifact = (
PathArtifact,
PathArtifact,
UserNameArtifact,
ServiceNameArtifact,
);
}
// A single domain could possibly need multiple databases
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct MariaDbDatabase<D>(pub D);
impl<D> Resource for MariaDbDatabase<D> {
type Artifact = (DatabaseNameArtifact, UserNameArtifact, PathArtifact);
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct MariaDbUser<D>(pub D);
impl<D> Resource for MariaDbUser<D> {
type Artifact = UserNameArtifact;
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct WordpressPlugin<P>(pub P, pub &'static str);
impl<P> Resource for WordpressPlugin<P> {
type Artifact = ();
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct WordpressTranslation<P>(pub P, pub &'static str, pub &'static str);
impl<P> Resource for WordpressTranslation<P> {
type Artifact = ();
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct Cron<D>(pub D, pub String);
impl<D> Resource for Cron<D> {
type Artifact = ();
}
pub trait BorrowResource<R> {
fn borrow_resource(&self) -> Option<&R>;
}
macro_rules! default_resources {
( $($name:ident: $type:ty,)* ) => {
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum DefaultResources<'a, D> {
$( $name($type) ),*
}
$(impl<'a, D> From<$type> for DefaultResources<'a, D> {
fn from(from: $type) -> Self {
Self::$name(from)
}
})*
$(impl<'a, D> BorrowResource<$type> for DefaultResources<'a, D> {
fn borrow_resource(&self) -> Option<&$type> {
match self {
Self::$name(v) => Some(v),
_ => None
}
}
})*
}
}
default_resources!(
AcmeAccountKey: AcmeAccountKey,
AcmeChallengesDir: AcmeChallengesDir,
AcmeChallengesNginxSnippet: AcmeChallengesNginxSnippet,
AcmeRootCert: AcmeRootCert,
AcmeUser: AcmeUser,
Cert: Cert<D>,
CertChain: CertChain<D>,
Cron: Cron<D>,
Csr: Csr<D>,
DefaultServer: DefaultServer,
Dir: Dir<PathBuf>,
File: File<PathBuf>,
GitCheckout: GitCheckout<'a, PathBuf>,
Key: Key<D>,
KeyAndCertBundle: KeyAndCertBundle<D>,
LoadedDirectory: LoadedDirectory<PathBuf>,
MariaDbDatabase: MariaDbDatabase<D>,
MariaDbUser: MariaDbUser<D>,
SystemdSocketService: SystemdSocketService<D, PathBuf>,
NpmInstall: NpmInstall<PathBuf>,
Owner: Owner<PathBuf>,
PhpFpmPool: PhpFpmPool<D>,
ServeCustom: ServeCustom<D>,
ServeService: ServeService<D, PathBuf>,
ServePhp: ServePhp<D, PathBuf>,
ServeRedir: ServeRedir<D>,
ServeStatic: ServeStatic<D, PathBuf>,
StoredDirectory: StoredDirectory<PathBuf>,
User: User,
UserForDomain: UserForDomain<D>,
WordpressPlugin: WordpressPlugin<PathBuf>,
WordpressTranslation: WordpressTranslation<PathBuf>,
);

200
src/schema.rs

@ -1,10 +1,19 @@
use crate::loggers::Logger;
use crate::symbols::Symbol;
use std::cell::RefCell;
use std::error::Error;
use std::fmt;
use std::fmt::Debug;
use crate::loggers::Logger;
use crate::repository::SymbolRepository;
use crate::symbols::{Symbol, SymbolRunner};
pub trait SymbolRunner {
fn run_symbol<S: Symbol + Debug>(&self, symbol: &S, force: bool) -> Result<bool, Box<dyn Error>>;
}
impl<R: SymbolRunner + ?Sized> SymbolRunner for Box<R> {
fn run_symbol<S: Symbol + Debug>(&self, symbol: &S, force: bool) -> Result<bool, Box<dyn Error>> {
(**self).run_symbol(symbol, force)
}
}
#[derive(Debug)]
pub enum SymbolRunError {
@ -46,31 +55,47 @@ impl<L: Logger> InitializingSymbolRunner<L> {
logger: RefCell::new(logger),
}
}
}
impl<L: Logger> SymbolRunner for InitializingSymbolRunner<L> {
fn run_symbol(&self, symbol: &dyn Symbol) -> Result<(), Box<dyn Error>> {
fn exec_symbol<S: Symbol + Debug>(&self, symbol: &S) -> Result<(), Box<dyn Error>> {
let mut logger = self.logger.borrow_mut();
logger.write(format!("Executing {:?}", symbol).as_str());
symbol.execute()?;
let target_reached = symbol.target_reached()?;
logger.debug(
format!(
"Symbol reports target_reached: {:?} (should be true)",
target_reached
)
.as_str(),
);
if target_reached {
logger.write(format!("{} already reached", symbol).as_str());
Ok(())
} else {
Err(Box::new(SymbolRunError::ExecuteDidNotReach(())))
}
}
}
impl<L: Logger> SymbolRunner for InitializingSymbolRunner<L> {
fn run_symbol<S: Symbol + Debug>(&self, symbol: &S, force: bool) -> Result<bool, Box<dyn Error>> {
let mut logger = self.logger.borrow_mut();
let executed = if force {
logger.debug("Forcing symbol execution");
drop(logger);
self.exec_symbol(symbol)?;
true
} else {
logger.debug(format!("Symbol reports target_reached: {:?}", target_reached).as_str());
logger.write(format!("Executing {}", symbol).as_str());
symbol.execute()?;
let target_reached = symbol.target_reached()?;
logger.debug(
format!(
"Symbol reports target_reached: {:?} (should be true)",
target_reached
)
.as_str(),
);
if !target_reached {
return Err(Box::new(SymbolRunError::ExecuteDidNotReach(())));
if target_reached {
logger.write(format!("{:?} already reached", symbol).as_str());
} else {
logger.debug(format!("Symbol reports target_reached: {:?}", target_reached).as_str());
drop(logger);
self.exec_symbol(symbol)?;
}
}
Ok(())
!target_reached
};
Ok(executed)
}
}
@ -87,24 +112,26 @@ impl<L: Logger> DrySymbolRunner<L> {
}
impl<L: Logger> SymbolRunner for DrySymbolRunner<L> {
fn run_symbol(&self, symbol: &dyn Symbol) -> Result<(), Box<dyn Error>> {
fn run_symbol<S: Symbol + Debug>(&self, symbol: &S, force: bool) -> Result<bool, Box<dyn Error>> {
let mut logger = self.logger.borrow_mut();
let target_reached = symbol.target_reached()?;
logger.debug(format!("Symbol reports target_reached: {:?}", target_reached).as_str());
if !target_reached {
logger.write(format!("Would execute {}", symbol).as_str());
}
Ok(())
let would_execute = if force {
logger.write(format!("Would force-execute {:?}", symbol).as_str());
true
} else {
let target_reached = symbol.target_reached()?;
logger.debug(format!("Symbol reports target_reached: {:?}", target_reached).as_str());
if !target_reached {
logger.write(format!("Would execute {:?}", symbol).as_str());
}
!target_reached
};
Ok(would_execute)
}
}
pub struct ReportingSymbolRunner<'a, R: SymbolRunner, L: Logger>(&'a R, RefCell<L>);
pub struct ReportingSymbolRunner<'a, R, L>(&'a R, RefCell<L>);
impl<'a, R, L> ReportingSymbolRunner<'a, R, L>
where
R: SymbolRunner,
L: Logger,
{
impl<'a, R, L> ReportingSymbolRunner<'a, R, L> {
pub fn new(symbol_runner: &'a R, logger: L) -> Self {
ReportingSymbolRunner(symbol_runner, RefCell::new(logger))
}
@ -115,92 +142,20 @@ where
R: SymbolRunner,
L: Logger,
{
fn run_symbol(&self, symbol: &dyn Symbol) -> Result<(), Box<dyn Error>> {
fn run_symbol<S: Symbol + Debug>(&self, symbol: &S, force: bool) -> Result<bool, Box<dyn Error>> {
let mut logger = self.1.borrow_mut();
logger.debug(format!("Running symbol {}", symbol).as_str());
let res = self.0.run_symbol(symbol);
logger.debug(format!("Running symbol {:?}", symbol).as_str());
let res = self.0.run_symbol(symbol, force);
if let Err(ref e) = res {
logger.write(format!("Failed on {} with {}, aborting.", symbol, e).as_str())
logger.write(format!("Failed on {:?} with {}, aborting.", symbol, e).as_str())
} else {
logger.debug(format!("Successfully finished {}", symbol).as_str())
logger.debug(format!("Successfully finished {:?}", symbol).as_str())
}
res
}
}
use crate::resources::Resource;
use std::collections::HashSet;
pub struct NonRepeatingSymbolRunner<R: SymbolRunner> {
upstream: R,
done: RefCell<HashSet<Resource>>,
}
impl<R> NonRepeatingSymbolRunner<R>
where
R: SymbolRunner,
{
pub fn new(symbol_runner: R) -> Self {
Self {
upstream: symbol_runner,
done: RefCell::new(HashSet::new()),
}
}
}
impl<R: SymbolRunner> SymbolRunner for NonRepeatingSymbolRunner<R> {
fn run_symbol(&self, symbol: &dyn Symbol) -> Result<(), Box<dyn Error>> {
if let Some(resources) = symbol.provides() {
let mut done = self.done.borrow_mut();
let mut has_to_run = false;
for resource in resources {
if !done.contains(&resource) {
has_to_run = true;
assert!(done.insert(resource.clone()));
}
}
if !has_to_run {
return Ok(());
}
}
self.upstream.run_symbol(&*symbol)
}
}
use std::marker::PhantomData;
pub struct RequirementsResolvingSymbolRunner<'a, 's, R: 'a + SymbolRunner, G: SymbolRepository<'s>>(
R,
&'a G,
PhantomData<Box<dyn Symbol + 's>>,
);
impl<'s, 'a: 's, R, G> RequirementsResolvingSymbolRunner<'a, 's, R, G>
where
R: SymbolRunner,
G: SymbolRepository<'s>,
{
pub fn new(symbol_runner: R, symbol_repo: &'a G) -> Self {
RequirementsResolvingSymbolRunner(symbol_runner, symbol_repo, PhantomData)
}
}
impl<'s, 'a: 's, R, G> SymbolRunner for RequirementsResolvingSymbolRunner<'a, 's, R, G>
where
R: SymbolRunner,
G: SymbolRepository<'s>,
{
fn run_symbol(&self, symbol: &dyn Symbol) -> Result<(), Box<dyn Error>> {
for resource in symbol.get_prerequisites() {
if let Some(dep) = self.1.get_symbol(&resource) {
dep.as_action(self).run()?;
}
}
self.0.run_symbol(&*symbol)
}
}
// FIXME: Add ExpectingSymbolRunner
/*
#[cfg(test)]
mod test {
use std::cell::RefCell;
@ -210,7 +165,7 @@ mod test {
use crate::loggers::Logger;
use crate::schema::InitializingSymbolRunner;
use crate::schema::SymbolRunner;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction};
use crate::symbols::Symbol;
#[derive(Debug, PartialEq, Clone)]
enum DummySymbolError {
@ -229,6 +184,7 @@ mod test {
}
}
#[derive(Debug)]
struct DummySymbol<'a> {
_execute: &'a dyn Fn() -> Result<(), Box<dyn Error>>,
_target_reached: &'a dyn Fn() -> Result<bool, Box<dyn Error>>,
@ -241,23 +197,6 @@ mod test {
fn execute(&self) -> Result<(), Box<dyn Error>> {
(self._execute)()
}
fn as_action<'a>(&'a self, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'a>(self: Box<Self>, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a>
where
Self: 'a,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl<'a> fmt::Display for DummySymbol<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Dummy symbol")
}
}
impl<'a> DummySymbol<'a> {
@ -336,3 +275,4 @@ mod test {
assert_eq!(err.description(), "Description");
}
}
*/

345
src/setup.rs

@ -0,0 +1,345 @@
use crate::resources::{BorrowResource, DefaultResources, Resource};
use crate::schema::SymbolRunner;
use crate::symbols::Symbol;
use crate::to_artifact::ToArtifact;
use crate::{DefaultBuilder, DefaultLocator, ResourceLocator, SymbolBuilder};
use std::collections::HashSet;
use std::error::Error;
use std::fmt::Debug;
use std::hash::Hash;
use std::marker::PhantomData;
pub trait CanHandle<X: ToArtifact> {
fn handle(&mut self, x: X) -> Result<(X::Artifact, bool), Box<dyn Error>>;
}
macro_rules! can_handle {
( $($name:ident)* ) => (
#[allow(non_snake_case)]
impl<_SR: SymbolRunner, _L, _R: Hash + Eq, _B, $($name: Resource,)*>
CanHandle<($($name,)*)>
for Setup<_SR, _L, _R, _B>
where
$(
_B: SymbolBuilder<$name>,
<_B as SymbolBuilder<$name>>::Symbol: Runnable + Debug,
_L: ResourceLocator<$name>,
_R: From<$name> + BorrowResource<$name>,
Self: CanHandle<<_B as SymbolBuilder<$name>>::Prerequisites> +
CanHandle<<_L as ResourceLocator<$name>>::Prerequisites>
),*
{
fn handle(&mut self, ($($name,)*): ($($name,)*)) -> Result<(($($name::Artifact,)*), bool), Box<dyn Error>>
{
$(let $name = self.add($name)?;)*
Ok((($($name.0,)*), false $(|| $name.1)*))
}
}
);
}
for_each_tuple!(can_handle);
// This is for self-referential T
// FIXME: Wait for specialization
impl<_SR: SymbolRunner, _L, _R: Hash + Eq, _B, T: Resource> CanHandle<Option<T>>
for Setup<_SR, _L, _R, _B>
where
_B: SymbolBuilder<T>,
<_B as SymbolBuilder<T>>::Symbol: Runnable + Debug,
_L: ResourceLocator<T, Prerequisites = Option<T>>,
_R: From<T> + BorrowResource<T>,
Self: CanHandle<<_B as SymbolBuilder<T>>::Prerequisites>,
{
fn handle(
&mut self,
r: Option<T>,
) -> Result<(<Option<T> as ToArtifact>::Artifact, bool), Box<dyn Error>> {
Ok(match r {
Some(r) => {
let (result, did_run) = self.add(r)?;
(Some(result), did_run)
}
None => (None, false),
})
}
}
impl<_SR: SymbolRunner, _L, _R: Hash + Eq, _B, T: Resource> CanHandle<T> for Setup<_SR, _L, _R, _B>
where
_B: SymbolBuilder<T>,
<_B as SymbolBuilder<T>>::Symbol: Runnable + Debug,
_L: ResourceLocator<T>,
_R: From<T> + Debug + BorrowResource<T>,
Self: CanHandle<<_B as SymbolBuilder<T>>::Prerequisites>
+ CanHandle<<_L as ResourceLocator<T>>::Prerequisites>,
{
fn handle(&mut self, r: T) -> Result<(<T as ToArtifact>::Artifact, bool), Box<dyn Error>> {
self.add(r)
}
}
pub struct Setup<
SR,
L = DefaultLocator,
R = DefaultResources<'static, &'static str>,
B = DefaultBuilder,
> {
symbol_runner: SR,
resources: HashSet<R>,
phantom: PhantomData<(L, B)>,
}
// https://github.com/rust-lang/rust/issues/27336
impl<SR> Setup<SR> {
pub fn new(symbol_runner: SR) -> Self {
Self {
symbol_runner,
resources: Default::default(),
phantom: Default::default(),
}
}
}
impl<SR, L, R: Hash + Eq, B> Setup<SR, L, R, B> {
pub fn new_with(symbol_runner: SR) -> Self {
Self {
symbol_runner,
resources: Default::default(),
phantom: Default::default(),
}
}
}
pub trait Runnable {
fn run<R: SymbolRunner>(&self, runner: &R, force: bool) -> Result<bool, Box<dyn Error>>;
}
impl<S: Symbol + Debug> Runnable for S {
fn run<R: SymbolRunner>(&self, runner: &R, force: bool) -> Result<bool, Box<dyn Error>> {
runner.run_symbol(self, force)
}
}
macro_rules! runnable_for_tuple {
( $($name:ident)* ) => (
#[allow(non_snake_case)]
impl<$($name: Symbol + Debug,)*> Runnable for ($($name,)*) {
#[allow(unused)]
fn run<_R: SymbolRunner>(&self, runner: &_R, force: bool) -> Result<bool, Box<dyn Error>> {
let ($($name,)*) = self;
Ok($(runner.run_symbol($name, force)? || )* false)
}
}
);
}
for_each_tuple!(runnable_for_tuple);
impl<SR: SymbolRunner, L, Rs: Hash + Eq, B> Setup<SR, L, Rs, B> {
pub fn add<R: Resource>(&mut self, resource: R) -> Result<(R::Artifact, bool), Box<dyn Error>>
where
B: SymbolBuilder<R>,
<B as SymbolBuilder<R>>::Symbol: Runnable + Debug,
L: ResourceLocator<R>,
Rs: From<R> + BorrowResource<R>,
Self: CanHandle<B::Prerequisites> + CanHandle<<L as ResourceLocator<R>>::Prerequisites>,
{
self.add_force(resource, false)
}
pub fn add_force<R: Resource>(
&mut self,
resource: R,
force_run: bool,
) -> Result<(R::Artifact, bool), Box<dyn Error>>
where
B: SymbolBuilder<R>,
<B as SymbolBuilder<R>>::Symbol: Runnable + Debug,
L: ResourceLocator<R>,
Rs: From<R> + BorrowResource<R>,
Self: CanHandle<B::Prerequisites> + CanHandle<<L as ResourceLocator<R>>::Prerequisites>,
{
let (target, target_prereqs) = L::locate(&resource);
let storable_resource = Rs::from(resource);
let did_run = if self.resources.get(&storable_resource).is_some() {
assert!(
!force_run,
"Forcing to run an already-added resource is a logical error"
);
false
} else {
let (_, target_prereqs_did_run) = self.handle(target_prereqs)?;
let (symbol, prereqs_did_run) =
self.get_symbol(storable_resource.borrow_resource().unwrap(), &target)?;
self.resources.insert(storable_resource);
self.run_symbol(
symbol,
force_run || target_prereqs_did_run || prereqs_did_run,
)?
};
Ok((target, did_run))
}
fn get_symbol<R: Resource>(
&mut self,
resource: &R,
target: &R::Artifact,
) -> Result<(<B as SymbolBuilder<R>>::Symbol, bool), Box<dyn Error>>
where
B: SymbolBuilder<R>,
Self: CanHandle<B::Prerequisites>,
{
let (prereqs, prereqs_did_run) = self.handle(B::prerequisites(resource))?;
Ok((B::create(resource, target, prereqs), prereqs_did_run))
}
pub fn run_symbol<S: Runnable>(&self, symbol: S, force: bool) -> Result<bool, Box<dyn Error>> {
symbol.run(&self.symbol_runner, force)
}
}
#[cfg(test)]
mod test {
use crate::resources::{BorrowResource, Resource};
use crate::schema::SymbolRunner;
use crate::symbols::Symbol;
use crate::to_artifact::ToArtifact;
use crate::{ResourceLocator, Setup, SymbolBuilder};
use std::cell::RefCell;
use std::error::Error;
use std::fmt::Debug;
use std::rc::Rc;
struct TestSymbolRunner {
count: Rc<RefCell<usize>>,
}
impl SymbolRunner for TestSymbolRunner {
fn run_symbol<S: Symbol + Debug>(
&self,
symbol: &S,
force: bool,
) -> Result<bool, Box<dyn Error>> {
let run = force || !symbol.target_reached()?;
if run {
*self.count.borrow_mut() += 1;
}
Ok(run)
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
struct TestResource<T>(&'static str, T);
impl<T> Resource for TestResource<T> {
type Artifact = ();
}
#[derive(Debug, Hash, PartialEq, Eq)]
enum Resources {
A(TestResource<&'static str>),
B(TestResource<()>),
}
impl From<TestResource<&'static str>> for Resources {
fn from(from: TestResource<&'static str>) -> Self {
Self::A(from)
}
}
impl From<TestResource<()>> for Resources {
fn from(from: TestResource<()>) -> Self {
Self::B(from)
}
}
impl BorrowResource<TestResource<&'static str>> for Resources {
fn borrow_resource(&self) -> Option<&TestResource<&'static str>> {
match self {
Self::A(a) => Some(a),
_ => None,
}
}
}
impl BorrowResource<TestResource<()>> for Resources {
fn borrow_resource(&self) -> Option<&TestResource<()>> {
match self {
Self::B(b) => Some(b),
_ => None,
}
}
}
struct TestResourceLocator;
impl<T> ResourceLocator<TestResource<T>> for TestResourceLocator {
type Prerequisites = ();
fn locate(_resource: &TestResource<T>) -> (<TestResource<T> as ToArtifact>::Artifact, ()) {
((), ())
}
}
struct TestSymbolBuilder;
impl SymbolBuilder<TestResource<&'static str>> for TestSymbolBuilder {
type Symbol = TestSymbol;
type Prerequisites = TestResource<()>;
fn prerequisites(resource: &TestResource<&'static str>) -> Self::Prerequisites {
TestResource(resource.1, ())
}
fn create(
resource: &TestResource<&'static str>,
(): &(),
_inputs: <Self::Prerequisites as ToArtifact>::Artifact,
) -> Self::Symbol {
TestSymbol {
reached: resource.0.chars().next().unwrap().is_uppercase(),
}
}
}
impl SymbolBuilder<TestResource<()>> for TestSymbolBuilder {
type Symbol = TestSymbol;
type Prerequisites = ();
fn prerequisites(_resource: &TestResource<()>) -> Self::Prerequisites {}
fn create(resource: &TestResource<()>, (): &(), (): ()) -> Self::Symbol {
TestSymbol {
reached: resource.0.chars().next().unwrap().is_uppercase(),
}
}
}
#[derive(Debug)]
struct TestSymbol {
reached: bool,
}
impl Symbol for TestSymbol {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
Ok(self.reached)
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
Ok(())
}
}
fn get_setup() -> (
Rc<RefCell<usize>>,
Setup<TestSymbolRunner, TestResourceLocator, Resources, TestSymbolBuilder>,
) {
let count = Rc::new(RefCell::new(0));
let runner = TestSymbolRunner {
count: Rc::clone(&count),
};
(count, Setup::new_with(runner))
}
#[test]
fn correctly_uses_force() {
let (count, mut setup) = get_setup();
setup.add(TestResource("A", "b")).unwrap();
assert_eq!(*count.borrow(), 2);
setup.add(TestResource("A", "b")).unwrap();
assert_eq!(*count.borrow(), 2);
let (count, mut setup) = get_setup();
setup.add(TestResource("A", "B")).unwrap();
assert_eq!(*count.borrow(), 0);
}
}

1
src/static_files.rs

@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/static_files.rs"));

23
src/storage.rs

@ -1,32 +1,33 @@
use std::error::Error;
use std::fs::read_dir;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
pub trait Storage {
fn write_filename(&self) -> String;
fn read_filename(&self) -> Result<String, Box<dyn Error>>;
fn write_filename(&self) -> PathBuf;
fn read_filename(&self) -> Result<PathBuf, Box<dyn Error>>;
fn recent_date(&self) -> Result<u64, Box<dyn Error>>;
}
#[derive(Clone)]
pub struct SimpleStorage(String, String);
#[derive(Debug, Clone)]
pub struct SimpleStorage(PathBuf);
impl SimpleStorage {
pub const fn new(base: String, filename: String) -> Self {
Self(base, filename)
pub const fn new(base: PathBuf) -> Self {
Self(base)
}
fn get_path(&self, date: Option<u64>) -> String {
fn get_path(&self, date: Option<u64>) -> PathBuf {
match date {
Some(d) => format!("{}/_{}/{}", self.0, self.1, d),
None => format!("{}/_{}", self.0, self.1),
Some(d) => self.0.join(d.to_string()),
None => self.0.clone(),
}
}
}
impl Storage for SimpleStorage {
fn write_filename(&self) -> String {
fn write_filename(&self) -> PathBuf {
self.get_path(Some(
SystemTime::now()
.duration_since(UNIX_EPOCH)
@ -35,7 +36,7 @@ impl Storage for SimpleStorage {
))
}
fn read_filename(&self) -> Result<String, Box<dyn Error>> {
fn read_filename(&self) -> Result<PathBuf, Box<dyn Error>> {
Ok(self.get_path(Some(self.recent_date()?)))
}

85
src/symbols/acme/account_key.rs

@ -1,85 +0,0 @@
use std::error::Error;
use std::fmt;
use std::path::Path;
use crate::command_runner::CommandRunner;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct AcmeAccountKey<'a, P: AsRef<Path>, C: CommandRunner> {
path: P,
command_runner: &'a C,
}
impl<'a, P: AsRef<Path>, C: CommandRunner> AcmeAccountKey<'a, P, C> {
pub fn new(path: P, command_runner: &'a C) -> Self {
AcmeAccountKey {
path,
command_runner,
}
}
fn get_bytes(&self) -> u32 {
4096
}
}
impl<P: AsRef<Path>, C: CommandRunner> fmt::Display for AcmeAccountKey<'_, P, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "AcmeAccountKey {}", self.path.as_ref().display())
}
}
impl<P: AsRef<Path>, C: CommandRunner> Symbol for AcmeAccountKey<'_, P, C> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
if !self.path.as_ref().exists() {
return Ok(false);
}
let stdout = self.command_runner.get_output(
"openssl",
args![
"rsa",
"-in",
self.path.as_ref(),
"-noout",
"-check",
"-text",
],
)?;
Ok(stdout.ends_with("RSA key ok\n".as_bytes()))
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
self.command_runner.run_successfully(
"openssl",
args![
"genrsa",
"-out",
self.path.as_ref(),
self.get_bytes().to_string(),
],
)
}
fn get_prerequisites(&self) -> Vec<Resource> {
if let Some(parent) = self.path.as_ref().parent() {
vec![Resource::new("dir", parent.to_str().unwrap())]
} else {
vec![]
}
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
#[cfg(test)]
mod test {}

101
src/symbols/acme/cert.rs

@ -1,80 +1,61 @@
use crate::command_runner::CommandRunner;
use crate::symbols::Symbol;
use std::borrow::Borrow;
use std::error::Error;
use std::fmt;
use std::fs::File as FsFile;
use std::io::Write;
use std::path::{Path, PathBuf};
use crate::command_runner::CommandRunner;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
use std::marker::PhantomData;
use std::path::Path;
pub struct AcmeCert<
'a,
D: AsRef<str>,
R: AsRef<Path>,
C: CommandRunner,
K: AsRef<Path>,
CH: AsRef<Path>,
> {
#[derive(Debug)]
pub struct Cert<_C, C, D, P> {
domain: D,
command_runner: &'a C,
root_cert_path: R,
account_key_path: K,
challenges_path: CH,
command_runner: C,
root_cert_path: P,
account_key_path: P,
challenges_path: P,
csr_path: P,
cert_path: P,
phantom: PhantomData<_C>,
}
impl<'a, D: AsRef<str>, R: AsRef<Path>, C: CommandRunner, K: AsRef<Path>, CH: AsRef<Path>>
AcmeCert<'a, D, R, C, K, CH>
{
impl<_C, C, D, P> Cert<_C, C, D, P> {
pub fn new(
domain: D,
command_runner: &'a C,
root_cert_path: R,
account_key_path: K,
challenges_path: CH,
command_runner: C,
root_cert_path: P,
account_key_path: P,
challenges_path: P,
csr_path: P,
cert_path: P,
) -> Self {
AcmeCert {
Self {
domain,
command_runner,
root_cert_path,
account_key_path,
challenges_path,
csr_path,
cert_path,
phantom: PhantomData::default(),
}
}
fn get_csr_path(&self) -> PathBuf {
format!("/etc/ssl/local_certs/{}.csr", self.domain.as_ref()).into()
}
fn get_cert_path(&self) -> PathBuf {
format!("/etc/ssl/local_certs/{}.crt", self.domain.as_ref()).into()
}
}
impl<D: AsRef<str>, R: AsRef<Path>, C: CommandRunner, K: AsRef<Path>, CH: AsRef<Path>> fmt::Display
for AcmeCert<'_, D, R, C, K, CH>
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "AcmeCert {}", self.domain.as_ref())
}
}
const DAYS_IN_SECONDS: u32 = 24 * 60 * 60;
impl<D: AsRef<str>, R: AsRef<Path>, C: CommandRunner, K: AsRef<Path>, CH: AsRef<Path>> Symbol
for AcmeCert<'_, D, R, C, K, CH>
{
impl<_C: CommandRunner, C: Borrow<_C>, D: AsRef<str>, P: AsRef<Path>> Symbol for Cert<_C, C, D, P> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
if !self.get_cert_path().exists() {
if !self.cert_path.as_ref().exists() {
return Ok(false);
}
let output = self.command_runner.run_with_args(
let output = self.command_runner.borrow().run_with_args(
"openssl",
args![
"x509",
"-in",
self.get_cert_path(),
self.cert_path.as_ref(),
"-noout",
"-subject",
"-checkend",
@ -92,13 +73,14 @@ impl<D: AsRef<str>, R: AsRef<Path>, C: CommandRunner, K: AsRef<Path>, CH: AsRef<
Ok(
self
.command_runner
.borrow()
.run_successfully(
"openssl",
args![
"verify",
"--untrusted",
self.root_cert_path.as_ref(),
self.get_cert_path(),
self.cert_path.as_ref(),
],
)
.is_ok(),
@ -118,36 +100,21 @@ impl<D: AsRef<str>, R: AsRef<Path>, C: CommandRunner, K: AsRef<Path>, CH: AsRef<
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
let output = self.command_runner.get_output(
let output = self.command_runner.borrow().get_output(
"acme-tiny",
args![
"--account-key",
self.account_key_path.as_ref(),
"--csr",
self.get_csr_path(),
self.csr_path.as_ref(),
"--acme-dir",
self.challenges_path.as_ref(),
],
)?;
let mut file = FsFile::create(self.get_cert_path())?;
let mut file = FsFile::create(self.cert_path.as_ref())?;
file.write_all(&output)?;
Ok(())
}
fn get_prerequisites(&self) -> Vec<Resource> {
vec![Resource::new("file", self.get_csr_path().to_str().unwrap())]
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
#[cfg(test)]

117
src/symbols/acme/chain.rs

@ -1,117 +0,0 @@
use std::error::Error;
use std::fmt;
use std::fs::File as FsFile;
use std::io::Write;
use std::path::{Path, PathBuf};
use crate::command_runner::CommandRunner;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct AcmeCertChain<'a, D: AsRef<str>, R: AsRef<Path>, C: CommandRunner> {
domain: D,
command_runner: &'a C,
root_cert: R,
}
impl<'a, D: AsRef<str>, R: AsRef<Path>, C: CommandRunner> AcmeCertChain<'a, D, R, C> {
pub fn new(domain: D, command_runner: &'a C, root_cert: R) -> Self {
AcmeCertChain {
domain,
command_runner,
root_cert,
}
}
fn get_single_cert_path(&self) -> PathBuf {
format!("/etc/ssl/local_certs/{}.crt", self.domain.as_ref()).into()
}
fn get_cert_chain_path(&self) -> PathBuf {
format!("/etc/ssl/local_certs/{}.chained.crt", self.domain.as_ref()).into()
}
}
impl<D: AsRef<str>, R: AsRef<Path>, C: CommandRunner> fmt::Display for AcmeCertChain<'_, D, R, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "AcmeCertChain {}", self.domain.as_ref())
}
}
const DAYS_IN_SECONDS: u32 = 24 * 60 * 60;
impl<D: AsRef<str>, R: AsRef<Path>, C: CommandRunner> Symbol for AcmeCertChain<'_, D, R, C> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
if !self.get_cert_chain_path().exists() {
return Ok(false);
}
let stdout = self.command_runner.get_output(
"openssl",
args![
"x509",
"-in",
self.get_cert_chain_path(),
"-noout",
"-subject",
"-checkend",
(30 * DAYS_IN_SECONDS).to_string(),
],
)?;
if stdout
!= format!(
"subject=CN = {}\nCertificate will not expire\n",
self.domain.as_ref()
)
.as_bytes()
{
return Ok(false);
}
// FIXME: From my understanding, the -untrusted *.pem parameter shouldn't be necessary, but is necessary with openssl 1.1.0f-3
Ok(
self
.command_runner
.run_successfully(
"openssl",
args![
"verify",
"-untrusted",
self.root_cert.as_ref(),
self.get_cert_chain_path(),
],
)
.is_ok(),
)
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
let output = self.command_runner.get_output(
"cat",
args![self.get_single_cert_path(), self.root_cert.as_ref(),],
)?;
let mut file = FsFile::create(self.get_cert_chain_path())?;
file.write_all(&output)?;
Ok(())
}
fn get_prerequisites(&self) -> Vec<Resource> {
vec![Resource::new(
"file",
self.get_single_cert_path().to_str().unwrap(),
)]
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
#[cfg(test)]
mod test {}

113
src/symbols/acme/mod.rs

@ -1,113 +1,2 @@
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use crate::command_runner::CommandRunner;
use crate::command_runner::SetuidCommandRunner;
use crate::symbols::concat::Concat;
use crate::symbols::dir::Dir;
use crate::symbols::file::File;
use crate::symbols::list::List;
use crate::symbols::owner::Owner;
use crate::symbols::Symbol;
mod account_key;
mod cert;
mod chain;
pub use self::account_key::AcmeAccountKey;
pub use self::cert::AcmeCert;
pub use self::chain::AcmeCertChain;
const ROOT_CERT_FILE_NAME: &str = "lets_encrypt_x3_cross_signed.pem";
const ACCOUNT_KEY_FILE_NAME: &str = "account.key";
pub struct Factory<'a, U: AsRef<str>, H: AsRef<Path>, C: AsRef<str>, R: CommandRunner> {
user_name: U,
home_dir: H,
cert: C,
command_runner: &'a R,
acme_command_runner: SetuidCommandRunner<'a, U, R>,
}
impl<'a, U: Clone + AsRef<str>, H: AsRef<Path>, C: AsRef<str>, R: CommandRunner>
Factory<'a, U, H, C, R>
{
pub fn new(user_name: U, home_dir: H, cert: C, command_runner: &'a R) -> Self {
let acme_command_runner = SetuidCommandRunner::new(user_name.clone(), command_runner);
Self {
user_name,
home_dir,
cert,
command_runner,
acme_command_runner,
}
}
pub fn get_challenges_dir(&'a self) -> Cow<'_, Path> {
[self.home_dir.as_ref(), "challenges".as_ref()]
.iter()
.collect::<PathBuf>()
.into()
}
pub fn get_init(&'a self) -> impl Symbol + 'a {
let root_cert_path: PathBuf = [self.home_dir.as_ref(), ROOT_CERT_FILE_NAME.as_ref()]
.iter()
.collect();
let account_key_file: PathBuf = [self.home_dir.as_ref(), ACCOUNT_KEY_FILE_NAME.as_ref()]
.iter()
.collect();
List::from((
AcmeAccountKey::new(account_key_file.clone(), self.command_runner),
Owner::new(
account_key_file,
self.user_name.clone(),
self.command_runner,
),
Dir::new(self.get_challenges_dir()),
Owner::new(
self.get_challenges_dir(),
self.user_name.clone(),
self.command_runner,
),
Dir::new("/etc/ssl/local_certs"),
Owner::new(
"/etc/ssl/local_certs",
self.user_name.clone(),
self.command_runner,
),
File::new(root_cert_path, self.cert.as_ref()),
))
}
pub fn get_cert<HOST: 'a + Clone + AsRef<str>>(&'a self, host: HOST) -> impl Symbol + 'a {
let root_cert_path: PathBuf = [self.home_dir.as_ref(), ROOT_CERT_FILE_NAME.as_ref()]
.iter()
.collect();
let account_key_path: PathBuf = [self.home_dir.as_ref(), ACCOUNT_KEY_FILE_NAME.as_ref()]
.iter()
.collect();
List::from((
AcmeCert::new(
host.clone(),
&self.acme_command_runner,
root_cert_path.clone(),
account_key_path,
self.get_challenges_dir(),
),
AcmeCertChain::new(host, &self.acme_command_runner, root_cert_path),
))
}
pub fn get_key_and_cert_bundle<HOST: 'a + Clone + AsRef<str>>(
&'a self,
host: HOST,
) -> impl Symbol + 'a {
List::from((
self.get_cert(host.clone()),
Concat::new(
[
format!("/etc/ssl/private/{}.key", host.as_ref()),
format!("/etc/ssl/local_certs/{}.chained.crt", host.as_ref()),
],
format!("/etc/ssl/private/{}.with_key.crt", host.as_ref()),
),
))
}
}
pub use self::cert::Cert;

36
src/symbols/concat.rs

@ -1,13 +1,11 @@
use crate::symbols::Symbol;
use std::error::Error;
use std::fmt;
use std::fs::{metadata, File};
use std::io::copy;
use std::marker::PhantomData;
use std::path::Path;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
#[derive(Debug)]
pub struct Concat<S, D, I> {
target: D,
sources: S,
@ -46,34 +44,4 @@ impl<S: AsRef<[I]>, D: AsRef<Path>, I: AsRef<Path>> Symbol for Concat<S, D, I> {
}
Ok(())
}
fn get_prerequisites(&self) -> Vec<Resource> {
let mut r: Vec<Resource> = self
.sources
.as_ref()
.iter()
.map(|s| Resource::new("file", s.as_ref().to_str().unwrap()))
.collect();
if let Some(parent) = self.target.as_ref().parent() {
r.push(Resource::new("dir", parent.to_str().unwrap()))
}
r
}
fn as_action<'a>(&'a self, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'a>(self: Box<Self>, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a>
where
Self: 'a,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl<S, D: AsRef<Path>, I> fmt::Display for Concat<S, D, I> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "Concat {}", self.target.as_ref().display())
}
}

31
src/symbols/cron.rs

@ -1,19 +1,17 @@
use std::error::Error;
use std::fmt;
use crate::command_runner::CommandRunner;
use crate::symbols::Symbol;
use std::error::Error;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct Cron<'r, C: AsRef<str>, U: AsRef<str>, R: CommandRunner> {
#[derive(Debug)]
pub struct Cron<'r, C, U, R> {
user: U,
content: C,
command_runner: &'r R,
}
impl<'r, U: AsRef<str>, R: CommandRunner> Cron<'r, String, U, R> {
impl<'r, U, R> Cron<'r, String, U, R> {
pub fn new<C: AsRef<str>>(user: U, content: C, command_runner: &'r R) -> Self {
Cron {
Self {
user,
content: String::from(content.as_ref()) + "\n",
command_runner,
@ -40,21 +38,4 @@ impl<C: AsRef<str>, U: AsRef<str>, R: CommandRunner> Symbol for Cron<'_, C, U, R
}
Ok(())
}
fn as_action<'a>(&'a self, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'a>(self: Box<Self>, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a>
where
Self: 'a,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl<C: AsRef<str>, U: AsRef<str>, R: CommandRunner> fmt::Display for Cron<'_, C, U, R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "Cron {}", self.user.as_ref())
}
}

48
src/symbols/dir.rs

@ -1,23 +1,21 @@
use crate::symbols::Symbol;
use std::error::Error;
use std::fmt;
use std::fs;
use std::io;
use std::path::Path;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct Dir<D: AsRef<Path>> {
path: D,
#[derive(Debug)]
pub struct Dir<P> {
path: P,
}
impl<D: AsRef<Path>> Dir<D> {
pub fn new(path: D) -> Self {
impl<P> Dir<P> {
pub fn new(path: P) -> Self {
Self { path }
}
}
impl<D: AsRef<Path>> Symbol for Dir<D> {
impl<P: AsRef<Path>> Symbol for Dir<P> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
if !self.path.as_ref().exists() {
return Ok(false);
@ -35,36 +33,4 @@ impl<D: AsRef<Path>> Symbol for Dir<D> {
fn execute(&self) -> Result<(), Box<dyn Error>> {
fs::create_dir(self.path.as_ref()).map_err(|e| Box::new(e) as Box<dyn Error>)
}
fn get_prerequisites(&self) -> Vec<Resource> {
if let Some(parent) = self.path.as_ref().parent() {
vec![Resource::new("dir", parent.to_str().unwrap())]
} else {
vec![]
}
}
fn provides(&self) -> Option<Vec<Resource>> {
Some(vec![Resource::new(
"dir",
self.path.as_ref().to_str().unwrap(),
)])
}
fn as_action<'a>(&'a self, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'a>(self: Box<Self>, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a>
where
Self: 'a,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl<D: AsRef<Path>> fmt::Display for Dir<D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "Dir {}", self.path.as_ref().display())
}
}

436
src/symbols/factory.rs

@ -1,436 +0,0 @@
use std::borrow::Cow;
use std::path::Path;
use std::path::PathBuf;
use crate::command_runner::CommandRunner;
use crate::storage::{SimpleStorage, Storage};
use crate::symbols::acme::Factory as AcmeFactory;
use crate::symbols::cron::Cron;
use crate::symbols::file::File;
use crate::symbols::git::checkout::GitCheckout;
use crate::symbols::hook::Hook;
use crate::symbols::list::List;
use crate::symbols::mariadb::{DatabaseDump, MariaDBDatabase, MariaDBUser};
use crate::symbols::nginx::server::{server_config, NginxServer};
use crate::symbols::owner::Owner;
use crate::symbols::stored_directory::{StorageDirection, StoredDirectory};
use crate::symbols::systemd::reload::ReloadService;
use crate::symbols::systemd::user_service::UserService;
use crate::symbols::tls::SelfSignedTlsCert;
use crate::symbols::Symbol;
pub trait Policy {
fn user_name_for_host(&self, host_name: &'static str) -> String {
host_name.split('.').rev().fold(String::new(), |result, part| if result.is_empty() { result } else { result + "_" } + part)
}
fn home_for_user(&self, user_name: &str) -> PathBuf {
format!("/home/{}", user_name).into()
}
fn socket_path(&self, user_name: &str, service_name: &str) -> PathBuf {
format!("/var/tmp/{}-{}.socket", user_name, service_name).into()
}
fn acme_user(&self) -> Cow<'_, str> {
"acme".into()
}
fn php_version(&self) -> &'static str {
"7.0"
}
}
pub struct DefaultPolicy;
impl Policy for DefaultPolicy {}
pub struct SymbolFactory<'a, C: CommandRunner, P: Policy> {
command_runner: &'a C,
acme_factory: AcmeFactory<'a, Cow<'a, str>, PathBuf, &'a str, C>,
policy: &'a P,
}
impl<'b, C: 'b + CommandRunner, P: 'b + Policy> SymbolFactory<'b, C, P> {
pub fn new(command_runner: &'b C, policy: &'b P, cert: &'b str) -> Self {
let acme_user = policy.acme_user();
let acme_home = policy.home_for_user(&acme_user);
let acme_factory = AcmeFactory::new(acme_user, acme_home, cert, command_runner);
SymbolFactory {
command_runner,
acme_factory,
policy,
}
}
pub fn get_cert<'a, H: 'a + AsRef<str> + Clone>(&'a self, host: H) -> impl Symbol + 'a {
self.acme_factory.get_cert(host)
}
pub fn get_key_and_cert_bundle<'a, H: 'a + AsRef<str> + Clone>(
&'a self,
host: H,
) -> impl Symbol + 'a {
self.acme_factory.get_key_and_cert_bundle(host)
}
pub fn get_nginx_acme_server<'a, S: 'a + Symbol>(
&'a self,
host: &'a str,
nginx_server_symbol: S,
) -> impl Symbol + 'a {
List::from((
SelfSignedTlsCert::new(host, self.command_runner),
Hook::new(
nginx_server_symbol,
ReloadService::new("nginx", self.command_runner),
),
Hook::new(
self.get_cert(host),
ReloadService::new("nginx", self.command_runner),
),
))
}
pub fn get_nginx_acme_challenge_config<'a>(&'a self) -> impl Symbol + 'a {
File::new(
"/etc/nginx/snippets/acme-challenge.conf",
format!(
"location ^~ /.well-known/acme-challenge/ {{
alias {}/;
try_files $uri =404;
}}",
self.acme_factory.get_challenges_dir().to_str().unwrap()
),
)
}
fn get_php_fpm_pool_socket_path<'a>(&'a self, user_name: &str) -> PathBuf {
format!("/run/php/{}.sock", user_name).into()
}
fn get_php_fpm_pool<'a>(&'a self, user_name: &str, max_children: usize) -> impl Symbol + 'a {
let socket = self.get_php_fpm_pool_socket_path(user_name);
let php_version = self.policy.php_version();
Hook::new(
File::new(
format!("/etc/php/{}/fpm/pool.d/{}.conf", php_version, user_name),
format!(
"[{0}]
user = {0}
group = www-data
listen = {1}
listen.owner = www-data
pm = ondemand
pm.max_children = {2}
catch_workers_output = yes
env[PATH] = /usr/local/bin:/usr/bin:/bin
",
user_name,
socket.to_str().unwrap(),
max_children
),
),
ReloadService::new(format!("php{}-fpm", php_version), self.command_runner),
)
}
pub fn serve_php<'a, ROOT: AsRef<Path>>(
&'a self,
host_name: &'static str,
root_dir: ROOT,
max_children: usize,
additional_config: &'a str,
) -> impl Symbol + 'a {
let user_name = self.policy.user_name_for_host(host_name);
let socket = self.get_php_fpm_pool_socket_path(&user_name);
List::from((
self.get_php_fpm_pool(&user_name, max_children),
self.get_nginx_acme_server(
host_name,
NginxServer::new_php(
host_name,
socket,
root_dir,
self.command_runner,
additional_config,
),
),
))
}
pub fn serve_wordpress<'a, ROOT: 'a + AsRef<Path>>(
&'a self,
host_name: &'static str,
root_dir: ROOT,
) -> impl Symbol + 'a {
self.serve_php(
host_name,
root_dir,
10,
"
location / {
try_files $uri $uri/ /index.php?$args;
}
",
)
}
pub fn serve_dokuwiki<'a>(
&'a self,
host_name: &'static str,
root_dir: &'static str,
) -> impl Symbol + 'a {
let user_name = self.policy.user_name_for_host(host_name);
let socket = self.get_php_fpm_pool_socket_path(&user_name);
List::from((
self.get_php_fpm_pool(&user_name, 10),
self.get_nginx_acme_server(host_name,
NginxServer::new(
host_name,
server_config(host_name, &format!("
root {};
index doku.php;
location ~ [^/]\\.php(/|$) {{
fastcgi_pass unix:{};
include \"snippets/fastcgi-php.conf\";
}}
location ~ /(data/|conf/|bin/|inc/|install.php) {{ deny all; }}
location / {{ try_files $uri $uri/ @dokuwiki; }}
location @dokuwiki {{
# rewrites \"doku.php/\" out of the URLs if you set the userewrite setting to .htaccess in dokuwiki config page
rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;
rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;
rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;
rewrite ^/(.*) /doku.php?id=$1&$args last;
}}
",
root_dir,
socket.to_str().unwrap())),
self.command_runner
))
))
}
pub fn serve_nextcloud<'a, ROOT: 'a + AsRef<Path>>(
&'a self,
host_name: &'static str,
root_dir: ROOT,
) -> impl Symbol + 'a {
self.serve_php(
host_name,
root_dir,
25,
"
client_max_body_size 500M;
# Disable gzip to avoid the removal of the ETag header
gzip off;
rewrite ^/caldav(.*)$ /remote.php/caldav$1 redirect;
rewrite ^/carddav(.*)$ /remote.php/carddav$1 redirect;
rewrite ^/webdav(.*)$ /remote.php/webdav$1 redirect;
error_page 403 /core/templates/403.php;
error_page 404 /core/templates/404.php;
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
location ~ ^/(?:\\.htaccess|data|config|db_structure\\.xml|README) {
deny all;
}
location / {
# The following 2 rules are only needed with webfinger
rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;
rewrite ^/.well-known/carddav /remote.php/carddav/ redirect;
rewrite ^/.well-known/caldav /remote.php/caldav/ redirect;
rewrite ^(/core/doc/[^\\/]+/)$ $1/index.html;
try_files $uri $uri/ /index.php;
}
# Adding the cache control header for js and css files
# Make sure it is BELOW the location ~ \\.php(?:$|/) { block
location ~* \\.(?:css|js)$ {
add_header Cache-Control \"public, max-age=7200\";
# Optional: Don't log access to assets
access_log off;
}
# Optional: Don't log access to other assets
location ~* \\.(?:jpg|jpeg|gif|bmp|ico|png|swf)$ {
access_log off;
}
",
)
}
pub fn serve_redir<'a>(
&'a self,
host_name: &'static str,
target: &'static str,
) -> impl Symbol + 'a {
self.get_nginx_acme_server(
host_name,
NginxServer::new_redir(host_name, target, self.command_runner),
)
}
pub fn serve_static<'a>(&'a self, host_name: &'static str, dir: &'a str) -> impl Symbol + 'a {
self.get_nginx_acme_server(
host_name,
NginxServer::new_static(host_name, dir, self.command_runner),
)
}
pub fn get_stored_directory<'a, T: Into<String>>(
&'a self,
storage_name: &'static str,
target: T,
) -> (impl Symbol + 'a, impl Symbol + 'a) {
let data = SimpleStorage::new("/root/data".to_string(), storage_name.to_string());
let string_target = target.into();
(
StoredDirectory::new(
string_target.clone(),
data.clone(),
StorageDirection::Save,
self.command_runner,
),
StoredDirectory::new(
string_target,
data,
StorageDirection::Load,
self.command_runner,
),
)
}
pub fn get_mariadb_database<'a>(&'a self, name: &'static str) -> impl Symbol + 'a {
let db_dump = SimpleStorage::new("/root/data".to_string(), format!("{}.sql", name));
List::from((
MariaDBDatabase::new(
name,
db_dump.read_filename().expect("Initial db dump missing"),
self.command_runner,
),
DatabaseDump::new(name, db_dump, self.command_runner),
))
}
pub fn get_mariadb_user<'a>(&'a self, user_name: &'static str) -> impl Symbol + 'a {
MariaDBUser::new(user_name, self.command_runner)
}
pub fn get_git_checkout<'a, T: 'a + AsRef<Path>>(
&'a self,
target: T,
source: &'a str,
branch: &'a str,
) -> impl Symbol + 'a {
GitCheckout::new(target, source, branch, self.command_runner)
}
pub fn get_owner<'a, U: 'a + AsRef<str>, F: 'a + AsRef<Path>>(
&'a self,
file: F,
user: U,
) -> impl Symbol + 'a {
Owner::new(file, user, self.command_runner)
}
pub fn get_file<'a, F: 'a + AsRef<str>, Q: 'a + AsRef<Path>>(
&'a self,
path: Q,
content: F,
) -> impl Symbol + 'a {
File::new(path, content)
}
pub fn get_cron<'a, T: 'a + AsRef<str>, U: 'a + AsRef<str>>(
&'a self,
user: U,
content: T,
) -> impl Symbol + 'a {
Cron::new(user, content, self.command_runner)
}
pub fn get_acme_user<'a>(&'a self) -> impl Symbol + 'a {
self.acme_factory.get_init()
}
pub fn get_systemd_user_service<'a, U: 'a + AsRef<str> + Clone>(
&'a self,
user_name: U,
service_name: &'a str,
config: String,
) -> impl Symbol + 'a {
let socket_path = self.policy.socket_path(user_name.as_ref(), service_name);
let home = self.policy.home_for_user(user_name.as_ref());
UserService::new(
socket_path,
&home,
user_name,
service_name,
self.command_runner,
config,
)
}
fn get_nodejs_systemd_user_service<'a, U: 'a + AsRef<str> + Clone>(
&'a self,
user_name: U,
service_name: &'a str,
path: &'a Path,
) -> impl Symbol + 'a {
let socket_path = self.policy.socket_path(user_name.as_ref(), service_name);
let home = self.policy.home_for_user(user_name.as_ref());
UserService::new_nodejs(
&home,
user_name,
service_name,
path,
self.command_runner,
socket_path,
)
}
pub fn proxy_socket<'a, T: AsRef<Path>>(
&'a self,
host_name: &'static str,
service_name: &'a str,
root_dir: T,
) -> impl Symbol + 'a {
let user_name = self.policy.user_name_for_host(host_name);
let socket_path = self.policy.socket_path(&user_name, service_name);
self.get_nginx_acme_server(
host_name,
NginxServer::new_proxy(host_name, &socket_path, root_dir, self.command_runner),
)
}
pub fn serve_nodejs<'a, 'c: 'a, S: 'a + Symbol, SP: 'a + AsRef<Path>>(
&'c self,
host: &'static str,
service_name: &'a str,
path: &'a Path,
static_path: SP,
nodejs_symbol: S,
) -> impl Symbol + 'a {
let user_name = self.policy.user_name_for_host(host);
List::from((
Hook::new(
nodejs_symbol,
self.get_nodejs_systemd_user_service(user_name, service_name, path),
),
self.proxy_socket(host, service_name, static_path),
))
}
}

39
src/symbols/file.rs

@ -1,26 +1,22 @@
use crate::symbols::Symbol;
use std::error::Error;
use std::fmt;
use std::fs::File as FsFile;
use std::io::{Read, Write};
use std::path::Path;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct File<C: AsRef<str>, D: AsRef<Path>> {
#[derive(Debug)]
pub struct File<D, C> {
path: D,
content: C,
}
impl<C: AsRef<str>, D: AsRef<Path>> File<C, D> {
impl<D, C> File<D, C> {
pub fn new(path: D, content: C) -> Self {
Self { path, content }
}
}
impl<C: AsRef<str>, D: AsRef<Path>> Symbol for File<C, D> {
impl<D: AsRef<Path>, C: AsRef<str>> Symbol for File<D, C> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
if !self.path.as_ref().exists() {
return Ok(false);
@ -44,29 +40,4 @@ impl<C: AsRef<str>, D: AsRef<Path>> Symbol for File<C, D> {
file.write_all(self.content.as_ref().as_bytes())?;
Ok(())
}
fn get_prerequisites(&self) -> Vec<Resource> {
if let Some(parent) = self.path.as_ref().parent() {
vec![Resource::new("dir", parent.to_str().unwrap())]
} else {
vec![]
}
}
fn as_action<'a>(&'a self, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'a>(self: Box<Self>, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a>
where
Self: 'a,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl<C: AsRef<str>, D: AsRef<Path>> fmt::Display for File<C, D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "File {}", self.path.as_ref().display())
}
}

103
src/symbols/git/checkout.rs

@ -1,62 +1,48 @@
use crate::command_runner::CommandRunner;
use crate::symbols::Symbol;
use std::borrow::Borrow;
use std::error::Error;
use std::ffi::OsStr;
use std::fmt;
use std::fs::metadata;
use std::io;
use std::marker::PhantomData;
use std::path::Path;
use crate::command_runner::CommandRunner;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct GitCheckout<'a, C: CommandRunner, T: AsRef<Path>> {
target: T,
source: &'a str,
branch: &'a str,
command_runner: &'a C,
#[derive(Debug)]
pub struct Checkout<_C, C, P, S, B> {
target: P,
source: S,
branch: B,
command_runner: C,
phantom: PhantomData<_C>,
}
impl<'a, C: CommandRunner, T: AsRef<Path>> GitCheckout<'a, C, T> {
pub fn new(target: T, source: &'a str, branch: &'a str, command_runner: &'a C) -> Self {
GitCheckout {
impl<C, _C, P, S, B> Checkout<_C, C, P, S, B> {
pub fn new(target: P, source: S, branch: B, command_runner: C) -> Self {
Self {
target,
source,
branch,
command_runner,
phantom: PhantomData::default(),
}
}
}
impl<C: CommandRunner, T: AsRef<Path>> fmt::Display for GitCheckout<'_, C, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Checkout {} (branch {}) into {}",
self.source,
self.branch,
self.target.as_ref().display()
)
}
}
impl<C: CommandRunner, T: AsRef<Path>> GitCheckout<'_, C, T> {
fn _run_in_target_repo<S: AsRef<OsStr>>(&self, args: &[S]) -> Result<Vec<u8>, Box<dyn Error>> {
impl<C: CommandRunner, _C: Borrow<C>, P: AsRef<Path>, S, B> Checkout<C, _C, P, S, B> {
fn _run_in_target_repo(&self, args: &[impl AsRef<OsStr>]) -> Result<Vec<u8>, Box<dyn Error>> {
let mut new_args = vec![OsStr::new("-C"), self.target.as_ref().as_ref()];
new_args.extend(args.iter().map(AsRef::as_ref));
self.command_runner.get_output("git", &new_args)
self.command_runner.borrow().get_output("git", &new_args)
}
}
impl<C: CommandRunner, T: AsRef<Path>> Symbol for GitCheckout<'_, C, T> {
impl<C: CommandRunner, _C: Borrow<C>, P: AsRef<Path>, S: AsRef<str>, B: AsRef<str>> Symbol
for Checkout<C, _C, P, S, B>
{
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
if let Err(e) = metadata(self.target.as_ref()) {
return if e.kind() == io::ErrorKind::NotFound {
Ok(false)
} else {
Err(Box::new(e))
};
if !self.target.as_ref().exists() {
return Ok(false);
}
self._run_in_target_repo(&["fetch", self.source, self.branch])?;
self._run_in_target_repo(&["fetch", self.source.as_ref(), self.branch.as_ref()])?;
// git rev-list resolves tag objects
let fetch_head = self._run_in_target_repo(&["rev-list", "-1", "FETCH_HEAD"])?;
let head = self._run_in_target_repo(&["rev-list", "-1", "HEAD"])?;
@ -65,48 +51,23 @@ impl<C: CommandRunner, T: AsRef<Path>> Symbol for GitCheckout<'_, C, T> {
fn execute(&self) -> Result<(), Box<dyn Error>> {
if !self.target.as_ref().exists() {
return self.command_runner.run_successfully(
return self.command_runner.borrow().run_successfully(
"git",
&[
OsStr::new("clone"),
"--depth".as_ref(),
"1".as_ref(),
"-b".as_ref(),
args![
"clone",
"--depth",
"1",
"-b",
self.branch.as_ref(),
self.source.as_ref(),
self.target.as_ref().as_ref(),
self.target.as_ref(),
],
);
}
self._run_in_target_repo(&["fetch", self.source, self.branch])?;
self._run_in_target_repo(&["fetch", self.source.as_ref(), self.branch.as_ref()])?;
self._run_in_target_repo(&["merge", "FETCH_HEAD"])?;
Ok(())
}
fn get_prerequisites(&self) -> Vec<Resource> {
vec![Resource::new(
"dir",
self.target.as_ref().parent().unwrap().to_string_lossy(),
)]
}
fn provides(&self) -> Option<Vec<Resource>> {
Some(vec![Resource::new(
"dir",
self.target.as_ref().to_str().unwrap(),
)])
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
#[cfg(test)]

6
src/symbols/git/mod.rs

@ -1,2 +1,4 @@
pub mod checkout;
pub mod submodules;
mod checkout;
//pub mod submodules;
pub use checkout::Checkout;

196
src/symbols/hook.rs

@ -1,196 +0,0 @@
use std::error::Error;
use std::fmt;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct Hook<A, B>
where
A: Symbol,
B: Symbol,
{
a: A,
b: B,
}
// A and B are executed if either A or B are not reached
impl<A, B> Hook<A, B>
where
A: Symbol,
B: Symbol,
{
pub fn new(a: A, b: B) -> Self {
Self { a, b }
}
}
impl<A, B> Symbol for Hook<A, B>
where
A: Symbol,
B: Symbol,
{
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
self.a.target_reached().and_then(|reached| {
if reached {
self.b.target_reached()
} else {
Ok(reached)
}
})
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
self.a.execute()?;
self.b.execute()
}
fn get_prerequisites(&self) -> Vec<Resource> {
let mut r = vec![];
r.extend(self.a.get_prerequisites().into_iter());
r.extend(self.b.get_prerequisites().into_iter());
r
}
fn provides(&self) -> Option<Vec<Resource>> {
let mut r = vec![];
if let Some(provides) = self.a.provides() {
r.extend(provides.into_iter());
}
if let Some(provides) = self.b.provides() {
r.extend(provides.into_iter());
}
if r.is_empty() {
None
} else {
Some(r)
}
}
fn as_action<'a>(&'a self, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'a>(self: Box<Self>, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a>
where
Self: 'a,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl<A, B> fmt::Display for Hook<A, B>
where
A: Symbol,
B: Symbol,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "Hook {} and then {}", self.a, self.b)
}
}
#[cfg(test)]
mod test {
use std::error::Error;
use std::fmt;
use crate::symbols::hook::Hook;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
struct ErrSymbol(String);
impl Symbol for ErrSymbol {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
Err(self.0.clone().into())
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
Err(self.0.clone().into())
}
fn as_action<'a>(&'a self, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'a>(self: Box<Self>, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a>
where
Self: 'a,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl fmt::Display for ErrSymbol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "")
}
}
struct OkSymbol(bool);
impl Symbol for OkSymbol {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
Ok(self.0)
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
Ok(())
}
fn as_action<'a>(&'a self, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'a>(self: Box<Self>, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a>
where
Self: 'a,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl fmt::Display for OkSymbol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "")
}
}
#[test]
fn first_target_reached_fails() {
let res = Hook::new(ErrSymbol("first".into()), ErrSymbol("second".into())).target_reached();
assert_eq!(res.unwrap_err().description(), "first");
}
#[test]
fn first_target_not_reached() {
let res = Hook::new(OkSymbol(false), ErrSymbol("second".into())).target_reached();
assert_eq!(res.unwrap(), false);
}
#[test]
fn second_target_reached_fails() {
let res = Hook::new(OkSymbol(true), ErrSymbol("second".into())).target_reached();
assert_eq!(res.unwrap_err().description(), "second");
}
#[test]
fn second_target_not_reached() {
let res = Hook::new(OkSymbol(true), OkSymbol(false)).target_reached();
assert_eq!(res.unwrap(), false);
}
#[test]
fn everything_reached() {
let res = Hook::new(OkSymbol(true), OkSymbol(true)).target_reached();
assert_eq!(res.unwrap(), true);
}
#[test]
fn first_execute_fails() {
let res = Hook::new(ErrSymbol("first".into()), ErrSymbol("second".into())).execute();
assert_eq!(res.unwrap_err().description(), "first");
}
#[test]
fn second_execute_fails() {
let res = Hook::new(OkSymbol(true), ErrSymbol("second".into())).execute();
assert_eq!(res.unwrap_err().description(), "second");
}
#[test]
fn everything_executes() {
let res = Hook::new(OkSymbol(true), OkSymbol(true)).execute();
assert_eq!(res.unwrap(), ());
}
}

213
src/symbols/list.rs

@ -1,213 +0,0 @@
use std::error::Error;
use std::fmt;
use crate::resources::Resource;
use crate::symbols::{Action, Symbol, SymbolRunner};
pub struct List<'a> {
symbols: Vec<Box<dyn Symbol + 'a>>,
}
impl<'a> List<'a> {
pub fn new(symbols: Vec<Box<dyn Symbol + 'a>>) -> Self {
List { symbols }
}
}
impl Symbol for List<'_> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
for symbol in &self.symbols {
if !symbol.target_reached()? {
return Ok(false);
}
}
Ok(true)
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
for symbol in &self.symbols {
symbol.execute()?;
}
Ok(())
}
fn get_prerequisites(&self) -> Vec<Resource> {
let mut r = vec![];
for symbol in &self.symbols {
for req in symbol.get_prerequisites() {
if self.provides().map_or(true, |p| !p.contains(&req)) {
r.push(req)
}
}
}
r
}
fn provides(&self) -> Option<Vec<Resource>> {
let mut r = vec![];
for symbol in &self.symbols {
if let Some(provides) = symbol.provides() {
r.extend(provides.into_iter());
} else {
return None;
}
}
if r.is_empty() {
None
} else {
Some(r)
}
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolListAction::new(runner, &self.symbols))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(ListAction::new(
self
.symbols
.into_iter()
.map(|s| s.into_action(runner))
.collect(),
))
}
}
macro_rules! from_tuple {
( $($name:ident)* ) => (
#[allow(non_snake_case)]
impl<'a, $($name: 'a + Symbol,)*> From<($($name,)*)> for List<'a> {
fn from(($($name,)*): ($($name,)*)) -> Self {
Self::new(vec![$(Box::new($name),)*])
}
}
);
}
for_each_tuple!(from_tuple);
struct SymbolListAction<'a> {
runner: &'a dyn SymbolRunner,
symbols: &'a [Box<dyn Symbol + 'a>],
}
impl<'a> SymbolListAction<'a> {
fn new(runner: &'a dyn SymbolRunner, symbols: &'a [Box<dyn Symbol + 'a>]) -> Self {
Self { runner, symbols }
}
}
impl Action for SymbolListAction<'_> {
fn run(&self) -> Result<(), Box<dyn Error>> {
for symbol in self.symbols {
symbol.as_action(self.runner).run()?;
}
Ok(())
}
}
pub struct ListAction<'a> {
actions: Vec<Box<dyn Action + 'a>>,
}
impl<'a> ListAction<'a> {
pub fn new(actions: Vec<Box<dyn Action + 'a>>) -> Self {
Self { actions }
}
}
impl Action for ListAction<'_> {
fn run(&self) -> Result<(), Box<dyn Error>> {
for action in &self.actions {
action.run()?;
}
Ok(())
}
}
impl fmt::Display for List<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "List [ ")?;
for symbol in &self.symbols {
write!(f, "{} ", symbol)?;
}
write!(f, "]")
}
}
/*
#[cfg(test)]
mod test {
use std::error::Error;
use std::fmt;
use symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
use symbols::hook::List;
struct ErrSymbol(String);
impl Symbol for ErrSymbol {
fn target_reached(&self) -> Result<bool, Box<Error>> { Err(self.0.clone().into()) }
fn execute(&self) -> Result<(), Box<Error>> { Err(self.0.clone().into()) }
}
impl fmt::Display for ErrSymbol { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error>{ write!(f, "") } }
struct OkSymbol(bool);
impl Symbol for OkSymbol {
fn target_reached(&self) -> Result<bool, Box<Error>> { Ok(self.0) }
fn execute(&self) -> Result<(), Box<Error>> { Ok(()) }
}
impl fmt::Display for OkSymbol { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error>{ write!(f, "") } }
#[test]
fn first_target_reached_fails() {
let res = List::new(ErrSymbol("first".into()), ErrSymbol("second".into())).target_reached();
assert_eq!(res.unwrap_err().description(), "first");
}
#[test]
fn first_target_not_reached() {
let res = List::new(OkSymbol(false), ErrSymbol("second".into())).target_reached();
assert_eq!(res.unwrap(), false);
}
#[test]
fn second_target_reached_fails() {
let res = List::new(OkSymbol(true), ErrSymbol("second".into())).target_reached();
assert_eq!(res.unwrap_err().description(), "second");
}
#[test]
fn second_target_not_reached() {
let res = List::new(OkSymbol(true), OkSymbol(false)).target_reached();
assert_eq!(res.unwrap(), false);
}
#[test]
fn everything_reached() {
let res = List::new(OkSymbol(true), OkSymbol(true)).target_reached();
assert_eq!(res.unwrap(), true);
}
#[test]
fn first_execute_fails() {
let res = List::new(ErrSymbol("first".into()), ErrSymbol("second".into())).execute();
assert_eq!(res.unwrap_err().description(), "first");
}
#[test]
fn second_execute_fails() {
let res = List::new(OkSymbol(true), ErrSymbol("second".into())).execute();
assert_eq!(res.unwrap_err().description(), "second");
}
#[test]
fn everything_executes() {
let res = List::new(OkSymbol(true), OkSymbol(true)).execute();
assert_eq!(res.unwrap(), ());
}
}
*/

38
src/symbols/mariadb/database.rs

@ -1,19 +1,18 @@
use std::error::Error;
use std::fmt;
use std::path::Path;
use crate::command_runner::CommandRunner;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
use crate::storage::Storage;
use crate::symbols::Symbol;
use std::error::Error;
pub struct MariaDBDatabase<'a, D: AsRef<str>, S: AsRef<Path>, C: CommandRunner> {
#[derive(Debug)]
pub struct Database<'a, D, S, C> {
db_name: D,
seed_file: S,
command_runner: &'a C,
}
impl<'a, D: AsRef<str>, S: AsRef<Path>, C: CommandRunner> MariaDBDatabase<'a, D, S, C> {
impl<'a, D, S, C: CommandRunner> Database<'a, D, S, C> {
pub fn new(db_name: D, seed_file: S, command_runner: &'a C) -> Self {
MariaDBDatabase {
Self {
db_name,
seed_file,
command_runner,
@ -28,15 +27,7 @@ impl<'a, D: AsRef<str>, S: AsRef<Path>, C: CommandRunner> MariaDBDatabase<'a, D,
}
}
impl<D: AsRef<str>, S: AsRef<Path>, C: CommandRunner> fmt::Display
for MariaDBDatabase<'_, D, S, C>
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MariaDB Database {}", self.db_name.as_ref())
}
}
impl<D: AsRef<str>, S: AsRef<Path>, C: CommandRunner> Symbol for MariaDBDatabase<'_, D, S, C> {
impl<D: AsRef<str>, S: Storage, C: CommandRunner> Symbol for Database<'_, D, S, C> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
Ok(
self
@ -55,22 +46,11 @@ impl<D: AsRef<str>, S: AsRef<Path>, C: CommandRunner> Symbol for MariaDBDatabase
format!(
"mariadb '{}' < {}",
self.db_name.as_ref(),
self.seed_file.as_ref().to_str().unwrap()
self.seed_file.read_filename()?.to_str().unwrap()
),
],
)
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
#[cfg(test)]

75
src/symbols/mariadb/database_dump.rs

@ -1,75 +0,0 @@
use std::error::Error;
use std::fmt;
use std::str::FromStr;
use crate::command_runner::CommandRunner;
use crate::storage::Storage;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct DatabaseDump<'a, N: AsRef<str>, C: CommandRunner, S: Storage> {
db_name: N,
storage: S,
command_runner: &'a C,
}
impl<'a, N: AsRef<str>, C: CommandRunner, S: Storage> DatabaseDump<'a, N, C, S> {
pub fn new(db_name: N, storage: S, command_runner: &'a C) -> Self {
DatabaseDump {
db_name,
storage,
command_runner,
}
}
fn run_sql(&self, sql: &str) -> Result<String, Box<dyn Error>> {
let b = self
.command_runner
.get_output("mariadb", args!["--skip-column-names", "-B", "-e", sql])?;
Ok(String::from_utf8(b)?)
}
}
impl<N: AsRef<str>, C: CommandRunner, S: Storage> fmt::Display for DatabaseDump<'_, N, C, S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Dump MariaDB Database {}", self.db_name.as_ref())
}
}
impl<N: AsRef<str>, C: CommandRunner, S: Storage> Symbol for DatabaseDump<'_, N, C, S> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
let dump_date = self.storage.recent_date()?;
let modified_date = self.run_sql(&format!("select UNIX_TIMESTAMP(MAX(UPDATE_TIME)) from information_schema.tables WHERE table_schema = '{}'", self.db_name.as_ref()))?;
if modified_date.trim_end() == "NULL" {
return Ok(false);
}
Ok(u64::from_str(modified_date.trim_end())? <= dump_date)
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
self.command_runner.run_successfully(
"sh",
args![
"-c",
format!(
"mysqldump '{}' > {}",
self.db_name.as_ref(),
self.storage.write_filename()
),
],
)
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
#[cfg(test)]
mod test {}

56
src/symbols/mariadb/dump.rs

@ -0,0 +1,56 @@
use std::error::Error;
use std::str::FromStr;
use crate::command_runner::CommandRunner;
use crate::storage::Storage;
use crate::symbols::Symbol;
#[derive(Debug)]
pub struct Dump<'a, N, C, S> {
db_name: N,
storage: S,
command_runner: &'a C,
}
impl<'a, N, C: CommandRunner, S> Dump<'a, N, C, S> {
pub fn new(db_name: N, storage: S, command_runner: &'a C) -> Self {
Self {
db_name,
storage,
command_runner,
}
}
fn run_sql(&self, sql: &str) -> Result<String, Box<dyn Error>> {
let b = self
.command_runner
.get_output("mariadb", args!["--skip-column-names", "-B", "-e", sql])?;
Ok(String::from_utf8(b)?)
}
}
impl<N: AsRef<str>, C: CommandRunner, S: Storage> Symbol for Dump<'_, N, C, S> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
let dump_date = self.storage.recent_date()?;
let _modified_date = self.run_sql(&format!("select UNIX_TIMESTAMP(MAX(UPDATE_TIME)) from information_schema.tables WHERE table_schema = '{}'", self.db_name.as_ref()))?;
let modified_date = _modified_date.trim_end();
Ok(modified_date != "NULL" && u64::from_str(modified_date)? <= dump_date)
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
self.command_runner.run_successfully(
"sh",
args![
"-c",
format!(
"mysqldump '{}' > {}",
self.db_name.as_ref(),
self.storage.write_filename().to_str().unwrap()
),
],
)
}
}
#[cfg(test)]
mod test {}

8
src/symbols/mariadb/mod.rs

@ -1,7 +1,7 @@
mod database;
mod database_dump;
mod dump;
mod user;
pub use self::database::MariaDBDatabase;
pub use self::database_dump::DatabaseDump;
pub use self::user::MariaDBUser;
pub use self::database::Database;
pub use self::dump::Dump;
pub use self::user::User;

37
src/symbols/mariadb/user.rs

@ -1,18 +1,16 @@
use std::error::Error;
use std::fmt;
use crate::command_runner::CommandRunner;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
use crate::symbols::Symbol;
use std::error::Error;
pub struct MariaDBUser<'a, U: AsRef<str>, C: CommandRunner> {
#[derive(Debug)]
pub struct User<'a, U, C> {
user_name: U,
command_runner: &'a C,
}
impl<'a, U: AsRef<str>, C: CommandRunner> MariaDBUser<'a, U, C> {
impl<'a, U: AsRef<str>, C: CommandRunner> User<'a, U, C> {
pub fn new(user_name: U, command_runner: &'a C) -> Self {
MariaDBUser {
Self {
user_name,
command_runner,
}
@ -26,13 +24,7 @@ impl<'a, U: AsRef<str>, C: CommandRunner> MariaDBUser<'a, U, C> {
}
}
impl<U: AsRef<str>, C: CommandRunner> fmt::Display for MariaDBUser<'_, U, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MariaDB User {}", self.user_name.as_ref())
}
}
impl<U: AsRef<str>, C: CommandRunner> Symbol for MariaDBUser<'_, U, C> {
impl<U: AsRef<str>, C: CommandRunner> Symbol for User<'_, U, C> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
Ok(
self
@ -52,21 +44,6 @@ impl<U: AsRef<str>, C: CommandRunner> Symbol for MariaDBUser<'_, U, C> {
))?;
Ok(())
}
fn get_prerequisites(&self) -> Vec<Resource> {
vec![Resource::new("user", self.user_name.as_ref())]
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
#[cfg(test)]

70
src/symbols/mod.rs

@ -1,88 +1,22 @@
use crate::resources::Resource;
use std::error::Error;
use std::fmt::Display;
pub trait Action {
fn run(&self) -> Result<(), Box<dyn Error>>;
}
pub trait SymbolRunner {
fn run_symbol(&self, symbol: &dyn Symbol) -> Result<(), Box<dyn Error>>;
}
impl<R: SymbolRunner + ?Sized> SymbolRunner for Box<R> {
fn run_symbol(&self, symbol: &dyn Symbol) -> Result<(), Box<dyn Error>> {
(**self).run_symbol(symbol)
}
}
// Symbol
pub trait Symbol: Display {
pub trait Symbol {
fn target_reached(&self) -> Result<bool, Box<dyn Error>>;
fn execute(&self) -> Result<(), Box<dyn Error>>;
fn get_prerequisites(&self) -> Vec<Resource> {
vec![]
}
fn provides(&self) -> Option<Vec<Resource>> {
None
}
fn as_action<'a>(&'a self, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a>;
fn into_action<'a>(self: Box<Self>, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a>
where
Self: 'a;
}
// SymbolAction
pub struct SymbolAction<'a, S: Symbol> {
runner: &'a dyn SymbolRunner,
symbol: &'a S,
}
impl<'a, S: Symbol> SymbolAction<'a, S> {
pub fn new(runner: &'a dyn SymbolRunner, symbol: &'a S) -> Self {
Self { runner, symbol }
}
}
impl<S: Symbol> Action for SymbolAction<'_, S> {
fn run(&self) -> Result<(), Box<dyn Error>> {
self.runner.run_symbol(self.symbol)
}
}
pub struct OwnedSymbolAction<'a, S: Symbol + 'a> {
runner: &'a dyn SymbolRunner,
symbol: S,
}
impl<'a, S: Symbol + 'a> OwnedSymbolAction<'a, S> {
pub fn new(runner: &'a dyn SymbolRunner, symbol: S) -> Self {
Self { runner, symbol }
}
}
impl<'a, S: Symbol + 'a> Action for OwnedSymbolAction<'a, S> {
fn run(&self) -> Result<(), Box<dyn Error>> {
self.runner.run_symbol(&self.symbol)
}
}
pub mod acme;
pub mod concat;
pub mod cron;
pub mod dir;
pub mod factory;
pub mod file;
pub mod git;
pub mod hook;
pub mod list;
pub mod mariadb;
pub mod nginx;
pub mod noop;
pub mod npm;
pub mod owner;
pub mod postgresql;
pub mod stored_directory;
pub mod saved_directory;
pub mod systemd;
pub mod tls;
pub mod user;

1
src/symbols/nginx/mod.rs

@ -1 +0,0 @@
pub mod server;

246
src/symbols/nginx/server.rs

@ -1,246 +0,0 @@
use std::error::Error;
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use crate::command_runner::CommandRunner;
use crate::resources::Resource;
use crate::symbols::file::File as FileSymbol;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
#[derive(Debug)]
pub enum NginxServerError<E: Error> {
ExecError(E),
GenericError,
}
impl From<io::Error> for NginxServerError<io::Error> {
fn from(err: io::Error) -> Self {
Self::ExecError(err)
}
}
impl<E: Error> Error for NginxServerError<E> {
fn description(&self) -> &str {
match self {
Self::ExecError(ref e) => e.description(),
Self::GenericError => "Generic error",
}
}
fn cause(&self) -> Option<&dyn Error> {
match self {
Self::ExecError(ref e) => Some(e),
_ => None,
}
}
}
impl<E: Error> fmt::Display for NginxServerError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "{}", self.description())
}
}
pub struct NginxServer<'a, C: CommandRunner, T: AsRef<str>, P: AsRef<Path>> {
command_runner: &'a C,
file: FileSymbol<T, P>,
}
pub fn server_config(domain: &str, content: &str) -> String {
format!(
"server {{
listen 443 ssl http2;
server_name {0};
include \"snippets/acme-challenge.conf\";
ssl_certificate /etc/ssl/local_certs/{0}.chained.crt;
ssl_certificate_key /etc/ssl/private/{0}.key;
add_header Strict-Transport-Security \"max-age=31536000\";
{1}
}}
# Redirect all HTTP links to the matching HTTPS page
server {{
listen 80;
server_name {0};
include \"snippets/acme-challenge.conf\";
location / {{
return 301 https://$host$request_uri;
}}
}}
",
domain, content
)
}
pub fn php_server_config_snippet<SOCKET: AsRef<Path>, STATIC: AsRef<Path>>(
socket_path: SOCKET,
static_path: STATIC,
) -> String {
format!(
"
root {};
index index.html index.php;
location ~ [^/]\\.php(/|$) {{
fastcgi_pass unix:{};
include \"snippets/fastcgi-php.conf\";
}}",
static_path.as_ref().to_str().unwrap(),
socket_path.as_ref().to_str().unwrap()
)
}
pub trait SocketSpec {
fn to_nginx(&self) -> String;
}
impl<T> SocketSpec for T
where
T: AsRef<Path>,
{
fn to_nginx(&self) -> String {
format!("unix:{}:", self.as_ref().to_str().unwrap())
}
}
pub struct LocalTcpSocket(usize);
impl LocalTcpSocket {
pub const fn new(x: usize) -> Self {
Self(x)
}
}
impl SocketSpec for LocalTcpSocket {
fn to_nginx(&self) -> String {
format!("localhost:{}", self.0)
}
}
impl<'a, C: CommandRunner> NginxServer<'a, C, String, PathBuf> {
pub fn new_redir(domain: &'a str, target: &'a str, command_runner: &'a C) -> Self {
let content = server_config(
domain,
&format!(
"location / {{
return 301 $scheme://{}$request_uri;
}}",
target
),
);
Self::new(domain, content, command_runner)
}
pub fn new_proxy<S: SocketSpec, STATIC: AsRef<Path>>(
domain: &'a str,
socket_path: &S,
static_path: STATIC,
command_runner: &'a C,
) -> Self {
let proxy_content = format!(
"location / {{
try_files $uri @proxy;
}}
location @proxy {{
include fastcgi_params;
proxy_pass http://{};
proxy_redirect off;
}}",
socket_path.to_nginx()
);
let content = server_config(
domain,
&format!(
"
root {};
{}
",
static_path.as_ref().to_str().unwrap(),
proxy_content
),
);
Self::new(domain, content, command_runner)
}
pub fn new_php<SOCKET: AsRef<Path>, STATIC: AsRef<Path>>(
domain: &'a str,
socket_path: SOCKET,
static_path: STATIC,
command_runner: &'a C,
additional_config: &'a str,
) -> Self {
let content = server_config(
domain,
&(php_server_config_snippet(socket_path, static_path) + additional_config),
);
Self::new(domain, content, command_runner)
}
pub fn new_static<S: AsRef<Path>>(
domain: &'a str,
static_path: S,
command_runner: &'a C,
) -> Self {
let content = server_config(
domain,
&format!(
"
root {};
try_files $uri $uri/ $uri.html =404;
",
static_path.as_ref().to_str().unwrap()
),
);
Self::new(domain, content, command_runner)
}
pub fn new(domain: &'a str, content: String, command_runner: &'a C) -> Self {
let file_path: PathBuf = ["/etc/nginx/sites-enabled/", domain].iter().collect();
NginxServer {
command_runner,
file: FileSymbol::new(file_path, content),
}
}
}
impl<C: CommandRunner, T: AsRef<str>, P: AsRef<Path>> Symbol for NginxServer<'_, C, T, P> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
if !self.file.target_reached()? {
return Ok(false);
}
// TODO: Could try to find out if the server is in the live config
Ok(true)
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
self.file.execute()?;
self
.command_runner
.run_successfully("systemctl", args!["reload-or-restart", "nginx"])
}
fn get_prerequisites(&self) -> Vec<Resource> {
self.file.get_prerequisites()
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl<C: CommandRunner, T: AsRef<str>, P: AsRef<Path>> fmt::Display for NginxServer<'_, C, T, P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "Nginx server config")
}
}

31
src/symbols/noop.rs

@ -1,31 +0,0 @@
use std::error::Error;
use std::fmt::{self, Display};
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct NoopSymbol;
impl Symbol for NoopSymbol {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
Ok(true)
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
Ok(())
}
fn as_action<'a>(&'a self, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'a>(self: Box<Self>, runner: &'a dyn SymbolRunner) -> Box<dyn Action + 'a>
where
Self: 'a,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl Display for NoopSymbol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "Noop")
}
}

27
src/symbols/npm.rs

@ -1,25 +1,25 @@
use crate::command_runner::CommandRunner;
use crate::symbols::Symbol;
use std::error::Error;
use std::fmt;
use std::path::Path;
use crate::command_runner::CommandRunner;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct NpmInstall<'a, T: AsRef<Path>, C: CommandRunner> {
#[derive(Debug)]
pub struct Install<'a, T: AsRef<Path>, C: CommandRunner> {
target: T,
command_runner: &'a C,
}
impl<'a, T: AsRef<Path>, C: CommandRunner> NpmInstall<'a, T, C> {
impl<'a, T: AsRef<Path>, C: CommandRunner> Install<'a, T, C> {
pub fn new(target: T, command_runner: &'a C) -> Self {
NpmInstall {
Install {
target,
command_runner,
}
}
}
impl<T: AsRef<Path>, C: CommandRunner> fmt::Display for NpmInstall<'_, T, C> {
impl<T: AsRef<Path>, C: CommandRunner> fmt::Display for Install<'_, T, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
@ -29,7 +29,7 @@ impl<T: AsRef<Path>, C: CommandRunner> fmt::Display for NpmInstall<'_, T, C> {
}
}
impl<T: AsRef<Path>, C: CommandRunner> Symbol for NpmInstall<'_, T, C> {
impl<T: AsRef<Path>, C: CommandRunner> Symbol for Install<'_, T, C> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
if !self.target.as_ref().exists() {
return Ok(false);
@ -61,17 +61,6 @@ impl<T: AsRef<Path>, C: CommandRunner> Symbol for NpmInstall<'_, T, C> {
],
)
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
#[cfg(test)]

64
src/symbols/owner.rs

@ -1,35 +1,37 @@
use crate::command_runner::CommandRunner;
use crate::symbols::Symbol;
use std::borrow::Borrow;
use std::error::Error;
use std::fmt;
use std::fs;
use std::marker::PhantomData;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use users::get_user_by_name;
use crate::command_runner::CommandRunner;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct Owner<'a, C: CommandRunner, D: AsRef<Path>, U: AsRef<str>> {
path: D,
#[derive(Debug)]
pub struct Owner<_C, C, P, U> {
path: P,
user_name: U,
command_runner: &'a C,
command_runner: C,
phantom: PhantomData<_C>,
}
impl<'a, C: CommandRunner, D: AsRef<Path>, U: AsRef<str>> Owner<'a, C, D, U> {
pub fn new(path: D, user_name: U, command_runner: &'a C) -> Self {
Owner {
impl<_C, C, P, U> Owner<_C, C, P, U> {
pub fn new(path: P, user_name: U, command_runner: C) -> Self {
Self {
path,
user_name,
command_runner,
phantom: PhantomData::default(),
}
}
}
impl<C: CommandRunner, D: AsRef<Path>, U: AsRef<str>> Symbol for Owner<'_, C, D, U> {
impl<_C: CommandRunner, C: Borrow<_C>, P: AsRef<Path>, U: AsRef<str>> Symbol
for Owner<_C, C, P, U>
{
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
if !Path::new(self.path.as_ref()).exists() {
if !self.path.as_ref().exists() {
return Ok(false);
}
let actual_uid = fs::metadata(self.path.as_ref()).unwrap().uid();
@ -38,35 +40,9 @@ impl<C: CommandRunner, D: AsRef<Path>, U: AsRef<str>> Symbol for Owner<'_, C, D,
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
let uname: &str = self.user_name.as_ref();
self
.command_runner
.run_successfully("chown", args!["-R", uname, self.path.as_ref(),])
}
fn get_prerequisites(&self) -> Vec<Resource> {
vec![Resource::new("user", self.user_name.as_ref())]
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl<C: CommandRunner, D: AsRef<Path>, U: AsRef<str>> fmt::Display for Owner<'_, C, D, U> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(
f,
"Owner {} for {}",
self.user_name.as_ref(),
self.path.as_ref().display()
self.command_runner.borrow().run_successfully(
"chown",
args!["-R", self.user_name.as_ref(), self.path.as_ref()],
)
}
}

16
src/symbols/postgresql/database.rs

@ -1,9 +1,8 @@
use crate::command_runner::CommandRunner;
use crate::symbols::Symbol;
use std::error::Error;
use std::fmt;
use crate::command_runner::CommandRunner;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct PostgreSQLDatabase<'a, N: AsRef<str>, S: AsRef<str>, C: CommandRunner> {
name: N,
seed_file: S,
@ -85,17 +84,6 @@ impl<N: AsRef<str>, S: AsRef<str>, C: CommandRunner> Symbol for PostgreSQLDataba
],
)
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
#[cfg(test)]

87
src/symbols/stored_directory.rs → src/symbols/saved_directory.rs

@ -1,57 +1,50 @@
use crate::command_runner::CommandRunner;
use crate::storage::Storage;
use crate::symbols::Symbol;
use std::borrow::Borrow;
use std::error::Error;
use std::fmt;
use std::fs;
use std::io;
use std::marker::PhantomData;
use std::path::Path;
use std::str::FromStr;
use crate::command_runner::CommandRunner;
use crate::resources::Resource;
use crate::storage::Storage;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
#[derive(Debug, PartialEq)]
pub enum StorageDirection {
Load,
Save,
Store,
}
pub struct StoredDirectory<'a, P: AsRef<Path>, S: Storage, C: CommandRunner> {
#[derive(Debug)]
pub struct SavedDirectory<_C, C, P, S> {
path: P,
storage: S,
dir: StorageDirection,
command_runner: &'a C,
command_runner: C,
phantom: PhantomData<_C>,
}
impl<'a, P: AsRef<Path>, S: Storage, C: CommandRunner> StoredDirectory<'a, P, S, C> {
pub fn new(path: P, storage: S, dir: StorageDirection, command_runner: &'a C) -> Self {
StoredDirectory {
impl<_C, C, P, S> SavedDirectory<_C, C, P, S> {
pub fn new(path: P, storage: S, dir: StorageDirection, command_runner: C) -> Self {
Self {
path,
storage,
dir,
command_runner,
phantom: PhantomData::default(),
}
}
}
impl<P: AsRef<Path>, S: Storage, C: CommandRunner> fmt::Display for StoredDirectory<'_, P, S, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Stored directory {} ({:?})",
self.path.as_ref().display(),
self.dir
)
}
}
impl<P: AsRef<Path>, S: Storage, C: CommandRunner> Symbol for StoredDirectory<'_, P, S, C> {
impl<_C: CommandRunner, C: Borrow<_C>, P: AsRef<Path>, S: Storage> Symbol
for SavedDirectory<_C, C, P, S>
{
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
let metadata = fs::metadata(self.path.as_ref());
// Check if dir exists
if let Err(e) = metadata {
return if e.kind() == io::ErrorKind::NotFound {
Ok(self.dir == StorageDirection::Save)
Ok(self.dir == StorageDirection::Store)
} else {
Err(Box::new(e))
};
@ -64,7 +57,7 @@ impl<P: AsRef<Path>, S: Storage, C: CommandRunner> Symbol for StoredDirectory<'_
}
let dump_date = self.storage.recent_date()?;
let output = self.command_runner.get_output(
let output = self.command_runner.borrow().get_output(
"sh",
args![
"-c",
@ -75,12 +68,12 @@ impl<P: AsRef<Path>, S: Storage, C: CommandRunner> Symbol for StoredDirectory<'_
],
)?;
let modified_date = u64::from_str(String::from_utf8(output)?.trim_end())?;
if if self.dir == StorageDirection::Save {
if if self.dir == StorageDirection::Store {
modified_date > dump_date
} else {
dump_date > modified_date
} {
let output = self.command_runner.run_with_args(
let output = self.command_runner.borrow().run_with_args(
"diff",
args!["-rq", self.storage.read_filename()?, self.path.as_ref()],
)?;
@ -98,51 +91,19 @@ impl<P: AsRef<Path>, S: Storage, C: CommandRunner> Symbol for StoredDirectory<'_
if self.dir == StorageDirection::Load {
self
.command_runner
.borrow()
.run_successfully("rm", args!["-rf", self.path.as_ref()])?;
self.command_runner.run_successfully(
self.command_runner.borrow().run_successfully(
"cp",
args!["-a", self.storage.read_filename()?, self.path.as_ref()],
)
} else {
self.command_runner.run_successfully(
self.command_runner.borrow().run_successfully(
"cp",
args!["-a", self.path.as_ref(), self.storage.write_filename()],
)
}
}
fn get_prerequisites(&self) -> Vec<Resource> {
if self.dir == StorageDirection::Save {
return vec![];
}
if let Some(parent) = self.path.as_ref().parent() {
vec![Resource::new("dir", parent.to_str().unwrap())]
} else {
vec![]
}
}
fn provides(&self) -> Option<Vec<Resource>> {
if self.dir == StorageDirection::Load {
Some(vec![Resource::new(
"dir",
self.path.as_ref().to_str().unwrap(),
)])
} else {
None
}
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
#[cfg(test)]

10
src/symbols/systemd/mod.rs

@ -1,3 +1,7 @@
pub mod reload;
pub mod user_service;
pub mod user_session;
mod reload;
mod user_service;
mod user_session;
pub use reload::ReloadService;
pub use user_service::UserService;
pub use user_session::UserSession;

42
src/symbols/systemd/reload.rs

@ -1,49 +1,35 @@
use std::error::Error;
use std::fmt;
use crate::command_runner::CommandRunner;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
use crate::symbols::Symbol;
use std::borrow::Borrow;
use std::error::Error;
use std::marker::PhantomData;
pub struct ReloadService<'a, S, C: CommandRunner> {
#[derive(Debug)]
pub struct ReloadService<_C, C, S> {
service: S,
command_runner: &'a C,
command_runner: C,
phantom: PhantomData<_C>,
}
impl<'a, S, C: CommandRunner> ReloadService<'a, S, C> {
pub fn new(service: S, command_runner: &'a C) -> Self {
ReloadService {
impl<_C, C, S> ReloadService<_C, C, S> {
pub fn new(command_runner: C, service: S) -> Self {
Self {
service,
command_runner,
phantom: PhantomData::default(),
}
}
}
impl<S: AsRef<str>, C: CommandRunner> Symbol for ReloadService<'_, S, C> {
impl<S: AsRef<str>, _C: CommandRunner, C: Borrow<_C>> Symbol for ReloadService<_C, C, S> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
Ok(true)
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
self.command_runner.run_successfully(
self.command_runner.borrow().run_successfully(
"systemctl",
args!["reload-or-restart", self.service.as_ref()],
)
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl<S: AsRef<str>, C: CommandRunner> fmt::Display for ReloadService<'_, S, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "Reload service {}", self.service.as_ref())
}
}

165
src/symbols/systemd/user_service.rs

@ -1,132 +1,34 @@
use crate::command_runner::{CommandRunner, SetuidCommandRunner};
use crate::symbols::Symbol;
use std::error::Error;
use std::ffi::OsStr;
use std::fmt;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::process::Output;
use std::thread::sleep;
use std::time::Duration;
use crate::command_runner::{CommandRunner, SetuidCommandRunner};
use crate::resources::Resource;
use crate::symbols::file::File as FileSymbol;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
#[derive(Debug)]
pub enum UserServiceError<E: Error> {
ActivationFailed(io::Result<Output>),
ExecError(E),
GenericError,
}
impl From<io::Error> for UserServiceError<io::Error> {
fn from(err: io::Error) -> Self {
Self::ExecError(err)
}
}
impl<E: Error> Error for UserServiceError<E> {
fn description(&self) -> &str {
match self {
Self::ExecError(ref e) => e.description(),
Self::GenericError => "Generic error",
Self::ActivationFailed(_) => "Activation of service failed",
}
}
fn cause(&self) -> Option<&dyn Error> {
match self {
Self::ExecError(ref e) => Some(e),
_ => None,
}
}
}
impl<E: Error> fmt::Display for UserServiceError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "{}", self.description())?;
if let Self::ActivationFailed(Ok(ref log)) = self {
write!(f, ": {:?}", log)?;
};
Ok(())
}
}
pub struct UserService<'a, S: AsRef<Path>, U: AsRef<str>, C: AsRef<str>, R: CommandRunner> {
pub struct UserService<'a, S: AsRef<Path>, U: AsRef<str>, R: CommandRunner> {
socket_path: S,
service_name: &'a str,
user_name: U,
command_runner: R,
config: FileSymbol<C, PathBuf>,
command_runner: SetuidCommandRunner<'a, U, R>,
}
impl<'a, S: AsRef<Path>, U: AsRef<str> + Clone, R: CommandRunner>
UserService<'a, S, U, String, SetuidCommandRunner<'a, U, R>>
{
pub fn new_nodejs(
home: &'_ Path,
user_name: U,
service_name: &'a str,
path: &'_ Path,
command_runner: &'a R,
socket_path: S,
) -> Self {
let content = format!(
"[Service]
Environment=NODE_ENV=production
Environment=PORT={1}
ExecStartPre=/bin/rm -f {1}
ExecStart=/usr/bin/nodejs {0}
ExecStartPost=/bin/sh -c 'sleep 1 && chmod 666 {1}'
# FIXME: This only works if the nodejs path is a directory
WorkingDirectory={0}
#RuntimeDirectory=service
#RuntimeDirectoryMode=766
Restart=always
[Install]
WantedBy=default.target
",
path.to_str().unwrap(),
socket_path.as_ref().to_str().unwrap()
);
Self::new(
socket_path,
home,
user_name,
service_name,
command_runner,
content,
)
}
impl<S: AsRef<Path>, U: AsRef<str>, R: CommandRunner> UserService<'static, S, U, R> {
pub fn new(
socket_path: S,
home: &'_ Path,
user_name: U,
service_name: &'a str,
command_runner: &'a R,
content: String,
service_name: &'static str,
command_runner: &'static R,
) -> Self {
let config_path: PathBuf = [
home,
format!(".config/systemd/user/{}.service", service_name).as_ref(),
]
.iter()
.collect();
UserService {
Self {
socket_path,
service_name,
user_name: user_name.clone(),
command_runner: SetuidCommandRunner::new(user_name, command_runner),
config: FileSymbol::new(config_path, content),
}
}
}
impl<S: AsRef<Path>, U: AsRef<str>, C: AsRef<str>, R: CommandRunner> UserService<'_, S, U, C, R> {
impl<S: AsRef<Path>, U: AsRef<str>, R: CommandRunner> UserService<'_, S, U, R> {
fn systemctl_wait_for_dbus(&self, args: &[&OsStr]) -> Result<String, Box<dyn Error>> {
let mut tries = 5;
loop {
@ -161,12 +63,13 @@ impl<S: AsRef<Path>, U: AsRef<str>, C: AsRef<str>, R: CommandRunner> UserService
"ActiveState=activating" => sleep(Duration::from_millis(500)),
"ActiveState=active" => return Ok(true),
"ActiveState=failed" => {
return Err(Box::new(
UserServiceError::ActivationFailed(self.command_runner.run_with_args(
return Err(
String::from_utf8(self.command_runner.get_output(
"journalctl",
args!["--user", format!("--user-unit={}", self.service_name)],
)) as UserServiceError<io::Error>,
))
)?)?
.into(),
)
}
_ => return Ok(false),
}
@ -174,26 +77,18 @@ impl<S: AsRef<Path>, U: AsRef<str>, C: AsRef<str>, R: CommandRunner> UserService
}
}
impl<S: AsRef<Path>, U: AsRef<str>, C: AsRef<str>, R: CommandRunner> Symbol
for UserService<'_, S, U, C, R>
{
impl<S: AsRef<Path>, U: AsRef<str>, R: CommandRunner> Symbol for UserService<'_, S, U, R> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
if !(self.config.target_reached()?) {
return Ok(false);
}
self.check_if_service()
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
self.config.execute()?;
self.systemctl_wait_for_dbus(args!["--user", "enable", self.service_name])?;
self.systemctl_wait_for_dbus(args!["--user", "restart", self.service_name])?;
loop {
if !(self.check_if_service()?) {
return Err(Box::new(
UserServiceError::GenericError as UserServiceError<io::Error>,
));
return Err("Generic error".into());
}
if self.socket_path.as_ref().exists() {
@ -202,32 +97,4 @@ impl<S: AsRef<Path>, U: AsRef<str>, C: AsRef<str>, R: CommandRunner> Symbol
sleep(Duration::from_millis(500));
}
}
fn get_prerequisites(&self) -> Vec<Resource> {
let mut r = vec![Resource::new(
"file",
format!("/var/lib/systemd/linger/{}", self.user_name.as_ref()),
)];
r.extend(self.config.get_prerequisites().into_iter());
r
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl<S: AsRef<Path>, U: AsRef<str>, C: AsRef<str>, R: CommandRunner> fmt::Display
for UserService<'_, S, U, C, R>
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "Systemd user service unit for {}", self.service_name)
}
}

62
src/symbols/systemd/user_session.rs

@ -1,55 +1,26 @@
use std::error::Error;
use std::fmt;
use std::path::PathBuf;
use crate::command_runner::CommandRunner;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
use crate::symbols::Symbol;
use std::error::Error;
use std::path::Path;
#[derive(Debug)]
pub enum SystemdUserSessionError<E: Error> {
ExecError(E),
GenericError,
}
impl<E: Error> Error for SystemdUserSessionError<E> {
fn description(&self) -> &str {
match self {
Self::ExecError(ref e) => e.description(),
Self::GenericError => "Generic error",
}
}
fn cause(&self) -> Option<&dyn Error> {
match self {
Self::ExecError(ref e) => Some(e),
_ => None,
}
}
}
impl<E: Error> fmt::Display for SystemdUserSessionError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "{}", self.description())
}
}
pub struct SystemdUserSession<'a, U: AsRef<str>, C: CommandRunner> {
pub struct UserSession<'a, U, C> {
user_name: U,
command_runner: &'a C,
}
impl<'a, U: AsRef<str>, C: CommandRunner> SystemdUserSession<'a, U, C> {
impl<'a, U, C> UserSession<'a, U, C> {
pub fn new(user_name: U, command_runner: &'a C) -> Self {
SystemdUserSession {
Self {
user_name,
command_runner,
}
}
}
impl<U: AsRef<str>, C: CommandRunner> Symbol for SystemdUserSession<'_, U, C> {
impl<U: AsRef<str>, C: CommandRunner> Symbol for UserSession<'_, U, C> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
let mut path = PathBuf::from("/var/lib/systemd/linger");
path.push(self.user_name.as_ref());
let path = Path::new("/var/lib/systemd/linger").join(self.user_name.as_ref());
Ok(path.exists())
// Could also do `loginctl show-user ${self.user_name} | grep -F 'Linger=yes`
}
@ -59,21 +30,4 @@ impl<U: AsRef<str>, C: CommandRunner> Symbol for SystemdUserSession<'_, U, C> {
.command_runner
.run_successfully("loginctl", args!["enable-linger", self.user_name.as_ref()])
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl<U: AsRef<str>, C: CommandRunner> fmt::Display for SystemdUserSession<'_, U, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "Systemd user session for {}", self.user_name.as_ref())
}
}

96
src/symbols/tls/csr.rs

@ -1,56 +1,39 @@
use std::borrow::Cow;
use std::error::Error;
use std::ffi::OsStr;
use std::fmt;
use std::path::PathBuf;
use crate::command_runner::CommandRunner;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct TlsCsr<'a, C: CommandRunner> {
domain: Cow<'a, str>,
command_runner: &'a C,
use crate::symbols::Symbol;
use std::borrow::Borrow;
use std::error::Error;
use std::path::Path;
#[derive(Debug)]
pub struct Csr<C, D, K, P> {
command_runner: C,
domain: D,
key_path: K,
csr_path: P,
}
impl<'a, C: CommandRunner> TlsCsr<'a, C> {
pub fn new(domain: Cow<'a, str>, command_runner: &'a C) -> Self {
TlsCsr {
domain,
impl<C, D, K, P> Csr<C, D, K, P> {
pub fn new(command_runner: C, domain: D, key_path: K, csr_path: P) -> Self {
Self {
command_runner,
domain,
key_path,
csr_path,
}
}
fn get_key_path(&self) -> PathBuf {
format!("/etc/ssl/private/{}.key", self.domain).into()
}
fn get_csr_path(&self) -> PathBuf {
format!("/etc/ssl/local_certs/{}.csr", self.domain).into()
}
}
impl<C: CommandRunner> fmt::Display for TlsCsr<'_, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "TlsCsr {}", self.domain)
}
}
impl<C: CommandRunner> Symbol for TlsCsr<'_, C> {
impl<C: CommandRunner, D: Borrow<str>, K: Borrow<Path>, P: Borrow<Path>> Symbol
for Csr<C, D, K, P>
{
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
if !self.get_csr_path().exists() {
if !self.csr_path.borrow().exists() {
return Ok(false);
}
let output = self.command_runner.get_stderr(
"openssl",
&[
OsStr::new("req"),
"-in".as_ref(),
self.get_csr_path().as_ref(),
"-noout".as_ref(),
"-verify".as_ref(),
],
args!["req", "-in", self.csr_path.borrow(), "-noout", "-verify",],
)?;
Ok(output == b"verify OK\n")
}
@ -58,35 +41,20 @@ impl<C: CommandRunner> Symbol for TlsCsr<'_, C> {
fn execute(&self) -> Result<(), Box<dyn Error>> {
self.command_runner.run_successfully(
"openssl",
&[
OsStr::new("req"),
"-new".as_ref(),
"-sha256".as_ref(),
"-key".as_ref(),
self.get_key_path().as_ref(),
"-out".as_ref(),
self.get_csr_path().as_ref(),
"-subj".as_ref(),
format!("/CN={}", self.domain).as_ref(),
args![
"req",
"-new",
"-sha256",
"-key",
self.key_path.borrow(),
"-out",
self.csr_path.borrow(),
"-subj",
format!("/CN={}", self.domain.borrow()),
],
)?;
Ok(())
}
fn get_prerequisites(&self) -> Vec<Resource> {
vec![Resource::new("file", self.get_key_path().to_str().unwrap())]
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
#[cfg(test)]

76
src/symbols/tls/key.rs

@ -1,57 +1,42 @@
use std::borrow::Cow;
use std::error::Error;
use std::ffi::OsStr;
use std::fmt;
use std::path::PathBuf;
use crate::command_runner::CommandRunner;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
use crate::symbols::Symbol;
use std::error::Error;
use std::path::Path;
pub struct TlsKey<'a, C: CommandRunner> {
domain: Cow<'a, str>,
command_runner: &'a C,
#[derive(Debug)]
pub struct Key<C, P> {
file_path: P,
command_runner: C,
}
impl<'a, C: CommandRunner> TlsKey<'a, C> {
pub fn new(domain: Cow<'a, str>, command_runner: &'a C) -> Self {
TlsKey {
domain,
impl<C, P> Key<C, P> {
pub fn new(command_runner: C, file_path: P) -> Self {
Self {
file_path,
command_runner,
}
}
fn get_path(&self) -> PathBuf {
["/etc/ssl/private", &format!("{}.key", self.domain)]
.iter()
.collect()
}
fn get_bytes(&self) -> u32 {
4096
}
}
impl<C: CommandRunner> fmt::Display for TlsKey<'_, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "TlsKey {}", self.domain)
}
}
impl<C: CommandRunner> Symbol for TlsKey<'_, C> {
impl<C: CommandRunner, P: AsRef<Path>> Symbol for Key<C, P> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
if !self.get_path().exists() {
if !self.file_path.as_ref().exists() {
return Ok(false);
}
let stdout = self.command_runner.get_output(
"openssl",
&[
OsStr::new("rsa"),
"-in".as_ref(),
self.get_path().as_ref(),
"-noout".as_ref(),
"-check".as_ref(),
"-text".as_ref(),
args![
"rsa",
"-in",
self.file_path.as_ref(),
"-noout",
"-check",
"-text",
],
)?;
// FIXME check bytes
@ -61,25 +46,14 @@ impl<C: CommandRunner> Symbol for TlsKey<'_, C> {
fn execute(&self) -> Result<(), Box<dyn Error>> {
self.command_runner.run_successfully(
"openssl",
&[
OsStr::new("genrsa"),
"-out".as_ref(),
self.get_path().as_ref(),
self.get_bytes().to_string().as_ref(),
args![
"genrsa",
"-out",
self.file_path.as_ref(),
self.get_bytes().to_string(),
],
)
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
#[cfg(test)]

6
src/symbols/tls/mod.rs

@ -1,7 +1,5 @@
mod csr;
mod key;
mod self_signed_cert;
pub use self::csr::TlsCsr;
pub use self::key::TlsKey;
pub use self::self_signed_cert::SelfSignedTlsCert;
pub use self::csr::Csr;
pub use self::key::Key;

119
src/symbols/tls/self_signed_cert.rs

@ -1,119 +0,0 @@
use std::error::Error;
use std::fmt;
use std::path::PathBuf;
use crate::command_runner::CommandRunner;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct SelfSignedTlsCert<'a, D: AsRef<str>, C: CommandRunner> {
domain: D,
command_runner: &'a C,
}
impl<'a, D: AsRef<str>, C: CommandRunner> SelfSignedTlsCert<'a, D, C> {
pub fn new(domain: D, command_runner: &'a C) -> Self {
SelfSignedTlsCert {
domain,
command_runner,
}
}
fn get_key_path(&self) -> PathBuf {
format!("/etc/ssl/private/{}.key", self.domain.as_ref()).into()
}
fn get_cert_path(&self) -> PathBuf {
format!("/etc/ssl/local_certs/{}.chained.crt", self.domain.as_ref()).into()
}
}
impl<D: AsRef<str>, C: CommandRunner> fmt::Display for SelfSignedTlsCert<'_, D, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SelfSignedTlsCert {}", self.domain.as_ref())
}
}
const DAYS_IN_SECONDS: u32 = 24 * 60 * 60;
impl<D: AsRef<str>, C: CommandRunner> Symbol for SelfSignedTlsCert<'_, D, C> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
if !self.get_cert_path().exists() {
return Ok(false);
}
let output = self.command_runner.run_with_args(
"openssl",
args![
"x509",
"-in",
self.get_cert_path(),
"-noout",
"-subject",
"-checkend",
(30 * DAYS_IN_SECONDS).to_string(),
],
)?;
println!("{}", output.status.code().unwrap());
match output.status.code() {
Some(0) => Ok(
output.stdout
== format!(
"subject=CN = {}\nCertificate will not expire\n",
self.domain.as_ref()
)
.as_bytes(),
),
Some(_) => {
if output.stdout
== format!(
"subject=CN = {}\nCertificate will expire\n",
self.domain.as_ref()
)
.as_bytes()
{
Ok(false)
} else {
Err("Exit code non-zero, but wrong stdout".to_string().into())
}
}
_ => Err("Apparently killed by signal".to_string().into()),
}
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
self.command_runner.run_successfully(
"openssl",
args![
"req",
"-x509",
"-sha256",
"-days",
"90",
"-key",
self.get_key_path(),
"-out",
self.get_cert_path(),
"-subj",
format!("/CN={}", self.domain.as_ref()),
],
)
}
fn get_prerequisites(&self) -> Vec<Resource> {
vec![Resource::new("file", self.get_key_path().to_str().unwrap())]
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
#[cfg(test)]
mod test {}

187
src/symbols/user.rs

@ -1,97 +1,23 @@
use std::borrow::Cow;
use std::error::Error;
use std::fmt;
use crate::command_runner::CommandRunner;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
use crate::symbols::Symbol;
use std::error::Error;
#[derive(Debug)]
pub enum UserAdderError {
AlreadyExists,
UnknownError,
ImplError(Box<dyn Error>),
}
impl Error for UserAdderError {
fn description(&self) -> &str {
match self {
Self::AlreadyExists => "User already exists",
Self::UnknownError => "Unknown error",
Self::ImplError(_) => "User adding error",
}
}
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::ImplError(ref e) => Some(e.as_ref()),
_ => None,
}
}
}
impl fmt::Display for UserAdderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.source() {
Some(e) => write!(f, "{} (cause: {})", self.description(), e),
None => write!(f, "{}", self.description()),
}
}
}
pub trait UserAdder {
fn add_user(&self, user_name: &str) -> Result<(), UserAdderError>;
}
#[derive(Debug, PartialEq)]
pub enum UserError {
GenericError,
}
impl Error for UserError {
fn description(&self) -> &str {
match self {
Self::GenericError => "Could not find out if user exists",
}
}
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
_ => None,
}
}
}
impl fmt::Display for UserError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.source() {
Some(e) => write!(f, "{} (cause: {})", self.description(), e),
None => write!(f, "{}", self.description()),
}
}
}
pub struct User<'a, C: CommandRunner, A: UserAdder> {
user_name: Cow<'a, str>,
command_runner: &'a C,
user_adder: &'a A,
pub struct User<U, C> {
user_name: U,
command_runner: C,
}
impl<'a, C: CommandRunner, A: 'a + UserAdder> User<'a, C, A> {
pub fn new(user_name: Cow<'a, str>, command_runner: &'a C, user_adder: &'a A) -> Self {
User {
impl<U, C> User<U, C> {
pub fn new(user_name: U, command_runner: C) -> Self {
Self {
user_name,
command_runner,
user_adder,
}
}
}
impl<'a, C: CommandRunner, A: 'a + UserAdder> fmt::Display for User<'a, C, A> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "User {}", self.user_name)
}
}
impl<'a, C: CommandRunner, A: 'a + UserAdder> Symbol for User<'a, C, A> {
impl<U: AsRef<str>, C: CommandRunner> Symbol for User<U, C> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
let output = self
.command_runner
@ -99,108 +25,34 @@ impl<'a, C: CommandRunner, A: 'a + UserAdder> Symbol for User<'a, C, A> {
match output.status.code() {
Some(2) => Ok(false),
Some(0) => Ok(true),
_ => Err(Box::new(UserError::GenericError)),
_ => Err("Unknown error".into()),
}
}
fn execute(&self) -> Result<(), Box<dyn Error>> {
self
.user_adder
.add_user(self.user_name.as_ref())
.map_err(|e| Box::new(e) as Box<dyn Error>)
}
fn provides(&self) -> Option<Vec<Resource>> {
Some(vec![Resource::new("user", self.user_name.to_owned())])
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
pub struct SystemUserAdder<'a, C: CommandRunner> {
command_runner: &'a C,
}
impl<'a, C: CommandRunner> SystemUserAdder<'a, C> {
pub fn new(command_runner: &'a C) -> Self {
SystemUserAdder { command_runner }
}
}
impl<C: CommandRunner> UserAdder for SystemUserAdder<'_, C> {
fn add_user(&self, user_name: &str) -> Result<(), UserAdderError> {
let output = self.command_runner.run_with_args(
self.command_runner.run_successfully(
"adduser",
args![
// "-m", // Necessary for Fedora, not accepted in Debian
"--system", user_name,
"--system",
self.user_name.as_ref(),
],
);
match output {
Ok(output) => match output.status.code() {
Some(0) => Ok(()),
Some(1) => {
println!("{:?}", output);
Err(UserAdderError::AlreadyExists)
}
_ => {
println!("{:?}", output);
Err(UserAdderError::UnknownError)
}
},
Err(e) => Err(UserAdderError::ImplError(Box::new(e))),
}
)?;
Ok(())
}
}
#[cfg(test)]
mod test {
use std::error::Error;
use std::fmt;
use crate::command_runner::StdCommandRunner;
use crate::symbols::user::User;
use crate::symbols::user::UserAdder;
use crate::symbols::user::UserAdderError;
use crate::symbols::Symbol;
#[derive(Debug, PartialEq)]
struct DummyError;
impl Error for DummyError {
fn description(&self) -> &str {
"DummyError"
}
}
impl fmt::Display for DummyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DummyError")
}
}
struct DummyUserAdder;
impl UserAdder for DummyUserAdder {
fn add_user(&self, _user_name: &str) -> Result<(), UserAdderError> {
Ok(())
}
}
#[test]
fn test_target_reached_nonexisting() {
let symbol = User {
user_name: "nonexisting".into(),
command_runner: &StdCommandRunner,
user_adder: &DummyUserAdder,
user_name: "nonexisting",
command_runner: StdCommandRunner,
};
assert_eq!(symbol.target_reached().unwrap(), false);
}
@ -208,9 +60,8 @@ mod test {
#[test]
fn test_target_reached_root() {
let symbol = User {
user_name: "root".into(),
command_runner: &StdCommandRunner,
user_adder: &DummyUserAdder,
user_name: "root",
command_runner: StdCommandRunner,
};
assert_eq!(symbol.target_reached().unwrap(), true);
}

7
src/symbols/wordpress/mod.rs

@ -1,2 +1,5 @@
pub mod plugin;
pub mod translation;
mod plugin;
mod translation;
pub use plugin::Plugin;
pub use translation::Translation;

39
src/symbols/wordpress/plugin.rs

@ -1,24 +1,23 @@
use regex::Regex;
use std::error::Error;
use std::fmt;
use std::fs::File as FsFile;
use std::io;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use crate::command_runner::CommandRunner;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
use crate::symbols::Symbol;
pub struct WordpressPlugin<'a, P: AsRef<Path>, N: AsRef<str>, R: CommandRunner> {
#[derive(Debug)]
pub struct Plugin<'a, P, N, R> {
base: P,
name: N,
command_runner: &'a R,
}
impl<'a, P: AsRef<Path>, N: AsRef<str>, R: CommandRunner> WordpressPlugin<'a, P, N, R> {
impl<'a, P: AsRef<Path>, N: AsRef<str>, R: CommandRunner> Plugin<'a, P, N, R> {
pub fn new(base: P, name: N, command_runner: &'a R) -> Self {
WordpressPlugin {
Self {
base,
name,
command_runner,
@ -34,7 +33,7 @@ impl<'a, P: AsRef<Path>, N: AsRef<str>, R: CommandRunner> WordpressPlugin<'a, P,
}
}
impl<P: AsRef<Path>, N: AsRef<str>, R: CommandRunner> Symbol for WordpressPlugin<'_, P, N, R> {
impl<P: AsRef<Path>, N: AsRef<str>, R: CommandRunner> Symbol for Plugin<'_, P, N, R> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
let base_path = self.get_path();
if !base_path.exists() {
@ -99,30 +98,4 @@ impl<P: AsRef<Path>, N: AsRef<str>, R: CommandRunner> Symbol for WordpressPlugin
args![zip, "-d", self.base.as_ref().join("wp-content/plugins")],
)
}
fn get_prerequisites(&self) -> Vec<Resource> {
match self.get_path().parent() {
Some(p) => vec![Resource::new("dir", p.to_string_lossy())],
None => vec![],
}
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl<P: AsRef<Path>, N: AsRef<str>, R: CommandRunner> fmt::Display
for WordpressPlugin<'_, P, N, R>
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "WordpressPlugin {}", self.name.as_ref())
}
}

48
src/symbols/wordpress/translation.rs

@ -1,31 +1,26 @@
use crate::command_runner::CommandRunner;
use crate::symbols::Symbol;
use regex::Regex;
use std::cmp::max;
use std::error::Error;
use std::fmt;
use std::fs::File as FsFile;
use std::io;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::path::PathBuf;
use crate::command_runner::CommandRunner;
use crate::resources::Resource;
use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
pub struct WordpressTranslation<'a, C: AsRef<str>, D: AsRef<Path>, R: CommandRunner> {
#[derive(Debug)]
pub struct Translation<'a, C, D, R> {
path: D,
version: &'a str,
locale: C,
command_runner: &'a R,
}
impl<'a, C: AsRef<str>, R: CommandRunner> WordpressTranslation<'a, C, PathBuf, R> {
pub fn new<D: AsRef<Path>>(path: D, version: &'a str, locale: C, command_runner: &'a R) -> Self {
WordpressTranslation {
path: [path.as_ref(), "wp-content/languages".as_ref()]
.iter()
.collect(),
impl<'a, D, C: AsRef<str>, R: CommandRunner> Translation<'a, C, D, R> {
pub fn new(path: D, version: &'a str, locale: C, command_runner: &'a R) -> Self {
Self {
path,
version,
locale,
command_runner,
@ -33,7 +28,7 @@ impl<'a, C: AsRef<str>, R: CommandRunner> WordpressTranslation<'a, C, PathBuf, R
}
}
impl<C: AsRef<str>, D: AsRef<Path>, R: CommandRunner> WordpressTranslation<'_, C, D, R> {
impl<C: AsRef<str>, D: AsRef<Path>, R: CommandRunner> Translation<'_, C, D, R> {
fn get_pairs(&self) -> Vec<(String, PathBuf)> {
let version_x = self
.version
@ -64,7 +59,7 @@ impl<C: AsRef<str>, D: AsRef<Path>, R: CommandRunner> WordpressTranslation<'_, C
}
}
impl<C: AsRef<str>, D: AsRef<Path>, R: CommandRunner> Symbol for WordpressTranslation<'_, C, D, R> {
impl<C: AsRef<str>, D: AsRef<Path>, R: CommandRunner> Symbol for Translation<'_, C, D, R> {
fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
let mut newest = String::new();
let match_date = Regex::new("(?m)^\"PO-Revision-Date: (.+)\\+0000\\\\n\"$").unwrap();
@ -115,27 +110,4 @@ impl<C: AsRef<str>, D: AsRef<Path>, R: CommandRunner> Symbol for WordpressTransl
}
Ok(())
}
fn get_prerequisites(&self) -> Vec<Resource> {
vec![Resource::new("dir", self.path.as_ref().to_str().unwrap())]
}
fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
Box::new(SymbolAction::new(runner, self))
}
fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
where
Self: 'b,
{
Box::new(OwnedSymbolAction::new(runner, *self))
}
}
impl<C: AsRef<str>, D: AsRef<Path>, R: CommandRunner> fmt::Display
for WordpressTranslation<'_, C, D, R>
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "WordpressTranslation {}", self.path.as_ref().display())
}
}

3
src/templates/mod.rs

@ -0,0 +1,3 @@
pub mod nginx;
pub mod php;
pub mod systemd;

13
src/templates/nginx/mod.rs

@ -0,0 +1,13 @@
mod server;
pub use server::*;
use std::path::Path;
pub fn acme_challenges_snippet<P: AsRef<Path>>(path: P) -> String {
format!(
"location ^~ /.well-known/acme-challenge/ {{
alias {}/;
try_files $uri =404;
}}",
path.as_ref().to_str().unwrap()
)
}

202
src/templates/nginx/server.rs

@ -0,0 +1,202 @@
use std::fmt::Display;
use std::path::Path;
pub fn default_server<P: AsRef<Path>>(challenges_snippet_path: P) -> String {
format!(
"server {{
listen 80 default_server;
listen [::]:80 default_server;
include \"{}\";
}}",
challenges_snippet_path.as_ref().to_str().unwrap()
)
}
pub fn server_config<D: Display, C: AsRef<Path>, K: AsRef<Path>, T: Display, S: AsRef<Path>>(
domain: D,
cert_path: C,
key_path: K,
content: T,
challenges_snippet_path: S,
) -> String {
format!(
"server {{
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name {};
include \"{}\";
ssl_certificate {};
ssl_certificate_key {};
add_header Strict-Transport-Security \"max-age=31536000\";
{}
}}
# Redirect all HTTP links to the matching HTTPS page
server {{
listen 80;
listen [::]:80;
server_name {0};
include \"{1}\";
location / {{
return 301 https://$host$request_uri;
}}
}}
",
domain,
challenges_snippet_path.as_ref().to_str().unwrap(),
cert_path.as_ref().to_str().unwrap(),
key_path.as_ref().to_str().unwrap(),
content
)
}
pub fn php_snippet<SOCKET: AsRef<Path>, STATIC: AsRef<Path>>(
index: &'static str,
socket_path: SOCKET,
static_path: STATIC,
) -> String {
format!(
"root {};
index {};
location ~ [^/]\\.php(/|$) {{
fastcgi_pass unix:{};
include \"snippets/fastcgi-php.conf\";
}}",
static_path.as_ref().to_str().unwrap(),
index,
socket_path.as_ref().to_str().unwrap()
)
}
pub fn redir_snippet(target: &str) -> String {
format!(
"location / {{
return 301 $scheme://{}$request_uri;
}}",
target
)
}
pub trait SocketSpec {
fn to_nginx(&self) -> String;
}
impl<T: AsRef<Path>> SocketSpec for T {
fn to_nginx(&self) -> String {
format!("unix:{}:", self.as_ref().to_str().unwrap())
}
}
pub struct LocalTcpSocket(usize);
impl LocalTcpSocket {
pub const fn new(x: usize) -> Self {
Self(x)
}
}
impl SocketSpec for LocalTcpSocket {
fn to_nginx(&self) -> String {
format!("localhost:{}", self.0)
}
}
pub fn proxy_snippet<S: SocketSpec, STATIC: AsRef<Path>>(
socket_path: &S,
static_path: STATIC,
) -> String {
format!(
"root {};
location / {{
try_files $uri @proxy;
}}
location @proxy {{
include fastcgi_params;
proxy_pass http://{};
proxy_redirect off;
}}",
static_path.as_ref().to_str().unwrap(),
socket_path.to_nginx()
)
}
pub fn static_snippet<S: AsRef<Path>>(static_path: S) -> String {
format!(
"root {};
try_files $uri $uri/ $uri.html =404;
",
static_path.as_ref().to_str().unwrap()
)
}
pub fn dokuwiki_snippet() -> String {
"
location ~ /(data/|conf/|bin/|inc/|install.php) { deny all; }
location / { try_files $uri $uri/ @dokuwiki; }
location @dokuwiki {
# rewrites \"doku.php/\" out of the URLs if you set the userewrite setting to .htaccess in dokuwiki config page
rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;
rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;
rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;
rewrite ^/(.*) /doku.php?id=$1&$args last;
}".into()
}
pub fn nextcloud_snippet() -> String {
"
client_max_body_size 500M;
# Disable gzip to avoid the removal of the ETag header
gzip off;
rewrite ^/caldav(.*)$ /remote.php/caldav$1 redirect;
rewrite ^/carddav(.*)$ /remote.php/carddav$1 redirect;
rewrite ^/webdav(.*)$ /remote.php/webdav$1 redirect;
error_page 403 /core/templates/403.php;
error_page 404 /core/templates/404.php;
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
location ~ ^/(?:\\.htaccess|data|config|db_structure\\.xml|README) {
deny all;
}
location / {
# The following 2 rules are only needed with webfinger
rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;
rewrite ^/.well-known/carddav /remote.php/carddav/ redirect;
rewrite ^/.well-known/caldav /remote.php/caldav/ redirect;
rewrite ^(/core/doc/[^\\/]+/)$ $1/index.html;
try_files $uri $uri/ /index.php;
}
# Adding the cache control header for js and css files
# Make sure it is BELOW the location ~ \\.php(?:$|/) { block
location ~* \\.(?:css|js)$ {
add_header Cache-Control \"public, max-age=7200\";
# Optional: Don't log access to assets
access_log off;
}
# Optional: Don't log access to other assets
location ~* \\.(?:jpg|jpeg|gif|bmp|ico|png|swf)$ {
access_log off;
}
"
.into()
}

24
src/templates/php.rs

@ -0,0 +1,24 @@
use std::path::Path;
pub fn fpm_pool_config<U: AsRef<str>, S: AsRef<Path>>(
user_name: U,
socket_path: S,
max_children: usize,
) -> String {
format!(
"[{0}]
user = {0}
group = www-data
listen = {1}
listen.owner = www-data
pm = ondemand
pm.max_children = {2}
catch_workers_output = yes
env[PATH] = /usr/local/bin:/usr/bin:/bin
",
user_name.as_ref(),
socket_path.as_ref().to_str().unwrap(),
max_children,
)
}

45
src/templates/systemd.rs

@ -0,0 +1,45 @@
use std::path::Path;
pub fn socket_service(
socket_path: impl AsRef<Path>,
exec: &str,
dir: impl AsRef<Path>,
additional: &str,
) -> String {
format!(
"[Service]
ExecStartPre=/bin/rm -f {}
ExecStart={}
WorkingDirectory={}
Restart=always
{}
[Install]
WantedBy=default.target
",
socket_path.as_ref().to_str().unwrap(),
exec,
dir.as_ref().to_str().unwrap(),
additional
)
}
pub fn nodejs_service<N: AsRef<Path>, S: AsRef<Path>>(
nodejs_path: N,
socket_path: S,
dir: impl AsRef<Path>,
) -> String {
socket_service(
&socket_path,
&format!("/usr/bin/nodejs {}", nodejs_path.as_ref().to_str().unwrap()),
dir,
&format!(
"Environment=NODE_ENV=production
Environment=PORT={0}
ExecStartPost=/bin/sh -c 'sleep 1 && chmod 666 {0}'
#RuntimeDirectory=service
#RuntimeDirectoryMode=766",
socket_path.as_ref().to_str().unwrap()
),
)
}

25
src/to_artifact.rs

@ -0,0 +1,25 @@
use crate::resources::Resource;
pub trait ToArtifact {
type Artifact;
}
macro_rules! to_artifact {
( $($name:ident)* ) => (
#[allow(non_snake_case)]
impl<$($name: Resource,)*> ToArtifact for ($($name,)*)
{
type Artifact = ($($name::Artifact,)*);
}
);
}
for_each_tuple!(to_artifact);
impl<T: Resource> ToArtifact for Option<T> {
type Artifact = Option<T::Artifact>;
}
impl<T: Resource> ToArtifact for T {
type Artifact = T::Artifact;
}

27
static_files/lets_encrypt_x3_cross_signed.pem

@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE-----
MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
-----END CERTIFICATE-----

11
tests/file.rs

@ -2,7 +2,7 @@ use schematics::symbols::file::File as FileSymbol;
use schematics::symbols::Symbol;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::path::{Path, PathBuf};
use tempdir::TempDir;
fn get_dir(content: Option<&str>) -> TempDir {
@ -18,11 +18,8 @@ fn get_dir(content: Option<&str>) -> TempDir {
tmp_dir
}
fn get_symbol(path: &Path) -> FileSymbol<&str, String> {
FileSymbol::new(
String::from(path.join("filename").to_str().unwrap()),
"target content",
)
fn get_symbol(path: &Path) -> FileSymbol<PathBuf, &str> {
FileSymbol::new(path.join("filename"), "target content")
}
// Normal cases
@ -81,7 +78,7 @@ fn no_file() {
#[test]
fn may_not_read_file() {
let symbol = get_symbol(&Path::new("/etc/passwd"));
let symbol = get_symbol(&Path::new("/etc/shadow"));
assert_eq!(symbol.target_reached().is_err(), true);
}

72
tests/setup.rs

@ -0,0 +1,72 @@
use schematics::resources::{Cert, Csr, GitCheckout};
use schematics::schema::SymbolRunner;
use schematics::symbols::Symbol;
use schematics::Setup;
use std::cell::RefCell;
use std::error::Error;
use std::fmt::Debug;
use std::rc::Rc;
struct TestSymbolRunner {
count: Rc<RefCell<usize>>,
}
impl SymbolRunner for TestSymbolRunner {
fn run_symbol<S: Symbol + Debug>(
&self,
_symbol: &S,
_force: bool,
) -> Result<bool, Box<dyn Error>> {
*self.count.borrow_mut() += 1;
Ok(false)
}
}
#[test]
fn runs_only_once() {
let count = Rc::new(RefCell::new(0));
let runner = TestSymbolRunner {
count: Rc::clone(&count),
};
let mut setup = Setup::new(runner);
assert_eq!(
(setup.add(Csr("somehost")).unwrap().0).0.to_str().unwrap(),
"/etc/ssl/local_certs/somehost.csr",
);
assert_eq!(
(setup.add(Csr("somehost")).unwrap().0).0.to_str().unwrap(),
"/etc/ssl/local_certs/somehost.csr",
);
assert_eq!(*count.borrow(), 2 + 5); // Key and CSR + 5 dirs
}
#[test]
fn can_create_an_acme_cert() {
let count = Rc::new(RefCell::new(0));
let runner = TestSymbolRunner {
count: Rc::clone(&count),
};
let mut setup = Setup::new(runner);
assert_eq!(
(setup.add(Cert("somehost")).unwrap().0).0.to_str().unwrap(),
"/etc/ssl/local_certs/somehost.crt",
);
assert_eq!(*count.borrow(), 15);
}
#[test]
fn can_create_a_git_checkout() {
let count = Rc::new(RefCell::new(0));
let runner = TestSymbolRunner {
count: Rc::clone(&count),
};
let mut setup = Setup::new(runner);
setup
.add(GitCheckout(
"/tmp/somepath".into(),
"/tmp/some_src_repo",
"master",
))
.unwrap();
assert_eq!(*count.borrow(), 3);
}

16
tests/storage.rs

@ -16,7 +16,7 @@ fn get_dir<'a, I: IntoIterator<Item = &'a &'a str>>(content: I) -> TempDir {
}
fn get_storage(path: &Path) -> SimpleStorage {
SimpleStorage::new(path.to_str().unwrap().into(), "filename".into())
SimpleStorage::new(path.join("_filename"))
}
// Normal cases
@ -29,7 +29,7 @@ fn single_file() {
assert!(
Regex::new(&format!("^{}/_filename/\\d+$", dir.path().display()))
.unwrap()
.is_match(&storage.write_filename())
.is_match(storage.write_filename().to_str().unwrap())
);
assert_eq!(
dir.path().join("_filename").join("12345"),
@ -46,7 +46,7 @@ fn two_files() {
assert!(
Regex::new(&format!("^{}/_filename/\\d+$", dir.path().display()))
.unwrap()
.is_match(&storage.write_filename())
.is_match(storage.write_filename().to_str().unwrap())
);
assert_eq!(
dir.path().join("_filename").join("23456"),
@ -63,7 +63,7 @@ fn another_two_files() {
assert!(
Regex::new(&format!("^{}/_filename/\\d+$", dir.path().display()))
.unwrap()
.is_match(&storage.write_filename())
.is_match(storage.write_filename().to_str().unwrap())
);
assert_eq!(
dir.path().join("_filename").join("23456"),
@ -80,7 +80,7 @@ fn three_files() {
assert!(
Regex::new(&format!("^{}/_filename/\\d+$", dir.path().display()))
.unwrap()
.is_match(&storage.write_filename())
.is_match(storage.write_filename().to_str().unwrap())
);
assert_eq!(
dir.path().join("_filename").join("23456"),
@ -99,7 +99,7 @@ fn empty_storage() {
assert!(
Regex::new(&format!("^{}/_filename/\\d+$", dir.path().display()))
.unwrap()
.is_match(&storage.write_filename())
.is_match(storage.write_filename().to_str().unwrap())
);
assert!(storage.read_filename().is_err());
assert_eq!(
@ -121,7 +121,7 @@ fn empty_storage_for_filename() {
assert!(
Regex::new(&format!("^{}/_filename/\\d+$", dir.path().display()))
.unwrap()
.is_match(&storage.write_filename())
.is_match(storage.write_filename().to_str().unwrap())
);
assert!(storage.read_filename().is_err());
assert_eq!(
@ -143,7 +143,7 @@ fn bad_file() {
assert!(
Regex::new(&format!("^{}/_filename/\\d+$", dir.path().display()))
.unwrap()
.is_match(&storage.write_filename())
.is_match(storage.write_filename().to_str().unwrap())
);
assert!(storage.read_filename().is_err());
assert_eq!(

Loading…
Cancel
Save