mirror of
https://github.com/moghtech/komodo.git
synced 2026-03-11 17:44:19 -05:00
1.15.8 (#124)
* fix all containers restart and unpause * add CommitSync to Procedure * validate resource query tags causes failure on non exist * files on host init working. match tags fail if tag doesnt exist * intelligent sync match tag selector * fix linting * Wait for user initialize file on host
This commit is contained in:
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -41,7 +41,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alerter"
|
||||
version = "1.15.7"
|
||||
version = "1.15.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -943,7 +943,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "1.15.7"
|
||||
version = "1.15.8"
|
||||
dependencies = [
|
||||
"komodo_client",
|
||||
"run_command",
|
||||
@@ -1355,7 +1355,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment_file"
|
||||
version = "1.15.7"
|
||||
version = "1.15.8"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
@@ -1439,7 +1439,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "1.15.7"
|
||||
version = "1.15.8"
|
||||
dependencies = [
|
||||
"serror",
|
||||
]
|
||||
@@ -1571,7 +1571,7 @@ checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "1.15.7"
|
||||
version = "1.15.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command",
|
||||
@@ -2192,7 +2192,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_cli"
|
||||
version = "1.15.7"
|
||||
version = "1.15.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2208,7 +2208,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_client"
|
||||
version = "1.15.7"
|
||||
version = "1.15.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2239,7 +2239,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_core"
|
||||
version = "1.15.7"
|
||||
version = "1.15.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2296,7 +2296,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_periphery"
|
||||
version = "1.15.7"
|
||||
version = "1.15.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2383,7 +2383,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logger"
|
||||
version = "1.15.7"
|
||||
version = "1.15.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -2447,7 +2447,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "migrator"
|
||||
version = "1.15.7"
|
||||
version = "1.15.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dotenvy",
|
||||
@@ -3102,7 +3102,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "1.15.7"
|
||||
version = "1.15.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -4880,7 +4880,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "update_logger"
|
||||
version = "1.15.7"
|
||||
version = "1.15.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
|
||||
@@ -3,7 +3,7 @@ resolver = "2"
|
||||
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.15.7"
|
||||
version = "1.15.8"
|
||||
edition = "2021"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
@@ -129,6 +129,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::RunSync(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::CommitSync(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::DeployStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
@@ -273,6 +276,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::RunSync(request) => {
|
||||
komodo_client().execute(request).await
|
||||
}
|
||||
Execution::CommitSync(request) => {
|
||||
komodo_client().write(request).await
|
||||
}
|
||||
Execution::DeployStack(request) => {
|
||||
komodo_client().execute(request).await
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ impl Resolve<RestartAllContainers, (User, Update)> for State {
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let logs = periphery_client(&server)?
|
||||
.request(api::container::StartAllContainers {})
|
||||
.request(api::container::RestartAllContainers {})
|
||||
.await
|
||||
.context("failed to restart all containers on host")?;
|
||||
|
||||
@@ -525,7 +525,7 @@ impl Resolve<UnpauseAllContainers, (User, Update)> for State {
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let logs = periphery_client(&server)?
|
||||
.request(api::container::StartAllContainers {})
|
||||
.request(api::container::UnpauseAllContainers {})
|
||||
.await
|
||||
.context("failed to unpause all containers on host")?;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use mungos::mongodb::bson::doc;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
resource,
|
||||
state::{db_client, State},
|
||||
};
|
||||
@@ -37,7 +38,12 @@ impl Resolve<ListAlerters, User> for State {
|
||||
ListAlerters { query }: ListAlerters,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<AlerterListItem>> {
|
||||
resource::list_for_user::<Alerter>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_for_user::<Alerter>(query, &user, &all_tags).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +53,13 @@ impl Resolve<ListFullAlerters, User> for State {
|
||||
ListFullAlerters { query }: ListFullAlerters,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListFullAlertersResponse> {
|
||||
resource::list_full_for_user::<Alerter>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_full_for_user::<Alerter>(query, &user, &all_tags)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::query::get_all_tags,
|
||||
resource,
|
||||
state::{
|
||||
action_states, build_state_cache, db_client, github_client, State,
|
||||
@@ -49,7 +50,12 @@ impl Resolve<ListBuilds, User> for State {
|
||||
ListBuilds { query }: ListBuilds,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<BuildListItem>> {
|
||||
resource::list_for_user::<Build>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_for_user::<Build>(query, &user, &all_tags).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +65,13 @@ impl Resolve<ListFullBuilds, User> for State {
|
||||
ListFullBuilds { query }: ListFullBuilds,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListFullBuildsResponse> {
|
||||
resource::list_full_for_user::<Build>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_full_for_user::<Build>(query, &user, &all_tags)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +106,7 @@ impl Resolve<GetBuildsSummary, User> for State {
|
||||
let builds = resource::list_full_for_user::<Build>(
|
||||
Default::default(),
|
||||
&user,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get all builds")?;
|
||||
@@ -252,9 +265,15 @@ impl Resolve<ListCommonBuildExtraArgs, User> for State {
|
||||
ListCommonBuildExtraArgs { query }: ListCommonBuildExtraArgs,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListCommonBuildExtraArgsResponse> {
|
||||
let builds = resource::list_full_for_user::<Build>(query, &user)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let builds =
|
||||
resource::list_full_for_user::<Build>(query, &user, &all_tags)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
|
||||
// first collect with guaranteed uniqueness
|
||||
let mut res = HashSet::<String>::new();
|
||||
|
||||
@@ -12,6 +12,7 @@ use mungos::mongodb::bson::doc;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
resource,
|
||||
state::{db_client, State},
|
||||
};
|
||||
@@ -37,7 +38,12 @@ impl Resolve<ListBuilders, User> for State {
|
||||
ListBuilders { query }: ListBuilders,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<BuilderListItem>> {
|
||||
resource::list_for_user::<Builder>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_for_user::<Builder>(query, &user, &all_tags).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +53,13 @@ impl Resolve<ListFullBuilders, User> for State {
|
||||
ListFullBuilders { query }: ListFullBuilders,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListFullBuildersResponse> {
|
||||
resource::list_full_for_user::<Builder>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_full_for_user::<Builder>(query, &user, &all_tags)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::periphery_client,
|
||||
helpers::{periphery_client, query::get_all_tags},
|
||||
resource,
|
||||
state::{action_states, deployment_status_cache, State},
|
||||
};
|
||||
@@ -45,7 +45,13 @@ impl Resolve<ListDeployments, User> for State {
|
||||
ListDeployments { query }: ListDeployments,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<DeploymentListItem>> {
|
||||
resource::list_for_user::<Deployment>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_for_user::<Deployment>(query, &user, &all_tags)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +61,15 @@ impl Resolve<ListFullDeployments, User> for State {
|
||||
ListFullDeployments { query }: ListFullDeployments,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListFullDeploymentsResponse> {
|
||||
resource::list_full_for_user::<Deployment>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_full_for_user::<Deployment>(
|
||||
query, &user, &all_tags,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +231,7 @@ impl Resolve<GetDeploymentsSummary, User> for State {
|
||||
let deployments = resource::list_full_for_user::<Deployment>(
|
||||
Default::default(),
|
||||
&user,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get deployments from db")?;
|
||||
@@ -254,10 +269,16 @@ impl Resolve<ListCommonDeploymentExtraArgs, User> for State {
|
||||
ListCommonDeploymentExtraArgs { query }: ListCommonDeploymentExtraArgs,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListCommonDeploymentExtraArgsResponse> {
|
||||
let deployments =
|
||||
resource::list_full_for_user::<Deployment>(query, &user)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let deployments = resource::list_full_for_user::<Deployment>(
|
||||
query, &user, &all_tags,
|
||||
)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
|
||||
// first collect with guaranteed uniqueness
|
||||
let mut res = HashSet::<String>::new();
|
||||
|
||||
@@ -403,12 +403,18 @@ impl Resolve<ListGitProvidersFromConfig, User> for State {
|
||||
let (builds, repos, syncs) = tokio::try_join!(
|
||||
resource::list_full_for_user::<Build>(
|
||||
Default::default(),
|
||||
&user
|
||||
&user,
|
||||
&[]
|
||||
),
|
||||
resource::list_full_for_user::<Repo>(
|
||||
Default::default(),
|
||||
&user,
|
||||
&[]
|
||||
),
|
||||
resource::list_full_for_user::<Repo>(Default::default(), &user),
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
Default::default(),
|
||||
&user
|
||||
&user,
|
||||
&[]
|
||||
),
|
||||
)?;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use komodo_client::{
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
resource,
|
||||
state::{action_states, procedure_state_cache, State},
|
||||
};
|
||||
@@ -35,7 +36,13 @@ impl Resolve<ListProcedures, User> for State {
|
||||
ListProcedures { query }: ListProcedures,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListProceduresResponse> {
|
||||
resource::list_for_user::<Procedure>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_for_user::<Procedure>(query, &user, &all_tags)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +52,13 @@ impl Resolve<ListFullProcedures, User> for State {
|
||||
ListFullProcedures { query }: ListFullProcedures,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListFullProceduresResponse> {
|
||||
resource::list_full_for_user::<Procedure>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_full_for_user::<Procedure>(query, &user, &all_tags)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +71,7 @@ impl Resolve<GetProceduresSummary, User> for State {
|
||||
let procedures = resource::list_full_for_user::<Procedure>(
|
||||
Default::default(),
|
||||
&user,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get procedures from db")?;
|
||||
|
||||
@@ -12,6 +12,7 @@ use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::query::get_all_tags,
|
||||
resource,
|
||||
state::{action_states, github_client, repo_state_cache, State},
|
||||
};
|
||||
@@ -37,7 +38,12 @@ impl Resolve<ListRepos, User> for State {
|
||||
ListRepos { query }: ListRepos,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<RepoListItem>> {
|
||||
resource::list_for_user::<Repo>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_for_user::<Repo>(query, &user, &all_tags).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +53,13 @@ impl Resolve<ListFullRepos, User> for State {
|
||||
ListFullRepos { query }: ListFullRepos,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListFullReposResponse> {
|
||||
resource::list_full_for_user::<Repo>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_full_for_user::<Repo>(query, &user, &all_tags)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,10 +91,13 @@ impl Resolve<GetReposSummary, User> for State {
|
||||
GetReposSummary {}: GetReposSummary,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetReposSummaryResponse> {
|
||||
let repos =
|
||||
resource::list_full_for_user::<Repo>(Default::default(), &user)
|
||||
.await
|
||||
.context("failed to get repos from db")?;
|
||||
let repos = resource::list_full_for_user::<Repo>(
|
||||
Default::default(),
|
||||
&user,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get repos from db")?;
|
||||
|
||||
let mut res = GetReposSummaryResponse::default();
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ use resolver_api::{Resolve, ResolveToString};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
helpers::periphery_client,
|
||||
helpers::{periphery_client, query::get_all_tags},
|
||||
resource,
|
||||
stack::compose_container_match_regex,
|
||||
state::{action_states, db_client, server_status_cache, State},
|
||||
@@ -55,9 +55,12 @@ impl Resolve<GetServersSummary, User> for State {
|
||||
GetServersSummary {}: GetServersSummary,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetServersSummaryResponse> {
|
||||
let servers =
|
||||
resource::list_for_user::<Server>(Default::default(), &user)
|
||||
.await?;
|
||||
let servers = resource::list_for_user::<Server>(
|
||||
Default::default(),
|
||||
&user,
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
let mut res = GetServersSummaryResponse::default();
|
||||
for server in servers {
|
||||
res.total += 1;
|
||||
@@ -119,7 +122,12 @@ impl Resolve<ListServers, User> for State {
|
||||
ListServers { query }: ListServers,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<ServerListItem>> {
|
||||
resource::list_for_user::<Server>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_for_user::<Server>(query, &user, &all_tags).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +137,13 @@ impl Resolve<ListFullServers, User> for State {
|
||||
ListFullServers { query }: ListFullServers,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListFullServersResponse> {
|
||||
resource::list_full_for_user::<Server>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_full_for_user::<Server>(query, &user, &all_tags)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,15 +388,18 @@ impl Resolve<ListAllDockerContainers, User> for State {
|
||||
ListAllDockerContainers { servers }: ListAllDockerContainers,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<ContainerListItem>> {
|
||||
let servers =
|
||||
resource::list_for_user::<Server>(Default::default(), &user)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|server| {
|
||||
servers.is_empty()
|
||||
|| servers.contains(&server.id)
|
||||
|| servers.contains(&server.name)
|
||||
});
|
||||
let servers = resource::list_for_user::<Server>(
|
||||
Default::default(),
|
||||
&user,
|
||||
&[],
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|server| {
|
||||
servers.is_empty()
|
||||
|| servers.contains(&server.id)
|
||||
|| servers.contains(&server.name)
|
||||
});
|
||||
|
||||
let mut containers = Vec::<ContainerListItem>::new();
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use mungos::mongodb::bson::doc;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
resource,
|
||||
state::{db_client, State},
|
||||
};
|
||||
@@ -36,7 +37,13 @@ impl Resolve<ListServerTemplates, User> for State {
|
||||
ListServerTemplates { query }: ListServerTemplates,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListServerTemplatesResponse> {
|
||||
resource::list_for_user::<ServerTemplate>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_for_user::<ServerTemplate>(query, &user, &all_tags)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +53,15 @@ impl Resolve<ListFullServerTemplates, User> for State {
|
||||
ListFullServerTemplates { query }: ListFullServerTemplates,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListFullServerTemplatesResponse> {
|
||||
resource::list_full_for_user::<ServerTemplate>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_full_for_user::<ServerTemplate>(
|
||||
query, &user, &all_tags,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::periphery_client,
|
||||
helpers::{periphery_client, query::get_all_tags},
|
||||
resource,
|
||||
stack::get_stack_and_server,
|
||||
state::{action_states, github_client, stack_status_cache, State},
|
||||
@@ -133,9 +133,15 @@ impl Resolve<ListCommonStackExtraArgs, User> for State {
|
||||
ListCommonStackExtraArgs { query }: ListCommonStackExtraArgs,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListCommonStackExtraArgsResponse> {
|
||||
let stacks = resource::list_full_for_user::<Stack>(query, &user)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let stacks =
|
||||
resource::list_full_for_user::<Stack>(query, &user, &all_tags)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
|
||||
// first collect with guaranteed uniqueness
|
||||
let mut res = HashSet::<String>::new();
|
||||
@@ -158,9 +164,15 @@ impl Resolve<ListCommonStackBuildExtraArgs, User> for State {
|
||||
ListCommonStackBuildExtraArgs { query }: ListCommonStackBuildExtraArgs,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListCommonStackBuildExtraArgsResponse> {
|
||||
let stacks = resource::list_full_for_user::<Stack>(query, &user)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let stacks =
|
||||
resource::list_full_for_user::<Stack>(query, &user, &all_tags)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
|
||||
// first collect with guaranteed uniqueness
|
||||
let mut res = HashSet::<String>::new();
|
||||
@@ -183,7 +195,12 @@ impl Resolve<ListStacks, User> for State {
|
||||
ListStacks { query }: ListStacks,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<StackListItem>> {
|
||||
resource::list_for_user::<Stack>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_for_user::<Stack>(query, &user, &all_tags).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +210,13 @@ impl Resolve<ListFullStacks, User> for State {
|
||||
ListFullStacks { query }: ListFullStacks,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListFullStacksResponse> {
|
||||
resource::list_full_for_user::<Stack>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_full_for_user::<Stack>(query, &user, &all_tags)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +251,7 @@ impl Resolve<GetStacksSummary, User> for State {
|
||||
let stacks = resource::list_full_for_user::<Stack>(
|
||||
Default::default(),
|
||||
&user,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get stacks from db")?;
|
||||
|
||||
@@ -15,6 +15,7 @@ use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::query::get_all_tags,
|
||||
resource,
|
||||
state::{
|
||||
action_states, github_client, resource_sync_state_cache, State,
|
||||
@@ -42,7 +43,13 @@ impl Resolve<ListResourceSyncs, User> for State {
|
||||
ListResourceSyncs { query }: ListResourceSyncs,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<ResourceSyncListItem>> {
|
||||
resource::list_for_user::<ResourceSync>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_for_user::<ResourceSync>(query, &user, &all_tags)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +59,15 @@ impl Resolve<ListFullResourceSyncs, User> for State {
|
||||
ListFullResourceSyncs { query }: ListFullResourceSyncs,
|
||||
user: User,
|
||||
) -> anyhow::Result<ListFullResourceSyncsResponse> {
|
||||
resource::list_full_for_user::<ResourceSync>(query, &user).await
|
||||
let all_tags = if query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
query, &user, &all_tags,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +103,7 @@ impl Resolve<GetResourceSyncsSummary, User> for State {
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
Default::default(),
|
||||
&user,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get resource_syncs from db")?;
|
||||
|
||||
@@ -29,7 +29,9 @@ use mungos::find::find_collect;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::{get_id_to_tags, get_user_user_group_ids},
|
||||
helpers::query::{
|
||||
get_all_tags, get_id_to_tags, get_user_user_group_ids,
|
||||
},
|
||||
resource,
|
||||
state::{db_client, State},
|
||||
sync::{
|
||||
@@ -46,10 +48,17 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
|
||||
) -> anyhow::Result<ExportAllResourcesToTomlResponse> {
|
||||
let mut targets = Vec::<ResourceTarget>::new();
|
||||
|
||||
let all_tags = if tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
|
||||
targets.extend(
|
||||
resource::list_for_user::<Alerter>(
|
||||
ResourceQuery::builder().tags(tags.clone()).build(),
|
||||
&user,
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -59,6 +68,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
|
||||
resource::list_for_user::<Builder>(
|
||||
ResourceQuery::builder().tags(tags.clone()).build(),
|
||||
&user,
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -68,6 +78,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
|
||||
resource::list_for_user::<Server>(
|
||||
ResourceQuery::builder().tags(tags.clone()).build(),
|
||||
&user,
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -77,6 +88,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
|
||||
resource::list_for_user::<Deployment>(
|
||||
ResourceQuery::builder().tags(tags.clone()).build(),
|
||||
&user,
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -86,6 +98,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
|
||||
resource::list_for_user::<Stack>(
|
||||
ResourceQuery::builder().tags(tags.clone()).build(),
|
||||
&user,
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -95,6 +108,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
|
||||
resource::list_for_user::<Build>(
|
||||
ResourceQuery::builder().tags(tags.clone()).build(),
|
||||
&user,
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -104,6 +118,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
|
||||
resource::list_for_user::<Repo>(
|
||||
ResourceQuery::builder().tags(tags.clone()).build(),
|
||||
&user,
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -113,6 +128,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
|
||||
resource::list_for_user::<Procedure>(
|
||||
ResourceQuery::builder().tags(tags.clone()).build(),
|
||||
&user,
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -122,6 +138,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
|
||||
resource::list_for_user::<ServerTemplate>(
|
||||
ResourceQuery::builder().tags(tags.clone()).build(),
|
||||
&user,
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -131,6 +148,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
ResourceQuery::builder().tags(tags.clone()).build(),
|
||||
&user,
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
|
||||
@@ -219,15 +219,17 @@ impl Resolve<CommitSync, User> for State {
|
||||
&self,
|
||||
CommitSync { sync }: CommitSync,
|
||||
user: User,
|
||||
) -> anyhow::Result<ResourceSync> {
|
||||
) -> anyhow::Result<Update> {
|
||||
let sync = resource::get_check_permissions::<
|
||||
entities::sync::ResourceSync,
|
||||
>(&sync, &user, PermissionLevel::Write)
|
||||
.await?;
|
||||
|
||||
let file_contents_empty = sync.config.file_contents_empty();
|
||||
|
||||
let fresh_sync = !sync.config.files_on_host
|
||||
&& sync.config.file_contents.is_empty()
|
||||
&& sync.config.repo.is_empty();
|
||||
&& sync.config.repo.is_empty()
|
||||
&& file_contents_empty;
|
||||
|
||||
if !sync.config.managed && !fresh_sync {
|
||||
return Err(anyhow!(
|
||||
@@ -235,21 +237,30 @@ impl Resolve<CommitSync, User> for State {
|
||||
));
|
||||
}
|
||||
|
||||
let resource_path = sync
|
||||
.config
|
||||
.resource_path
|
||||
.first()
|
||||
.context("Sync does not have resource path configured.")?
|
||||
.parse::<PathBuf>()
|
||||
.context("Invalid resource path")?;
|
||||
// Get this here so it can fail before update created.
|
||||
let resource_path =
|
||||
if sync.config.files_on_host || !sync.config.repo.is_empty() {
|
||||
let resource_path = sync
|
||||
.config
|
||||
.resource_path
|
||||
.first()
|
||||
.context("Sync does not have resource path configured.")?
|
||||
.parse::<PathBuf>()
|
||||
.context("Invalid resource path")?;
|
||||
|
||||
if resource_path
|
||||
.extension()
|
||||
.context("Resource path missing '.toml' extension")?
|
||||
!= "toml"
|
||||
{
|
||||
return Err(anyhow!("Resource path missing '.toml' extension"));
|
||||
}
|
||||
if resource_path
|
||||
.extension()
|
||||
.context("Resource path missing '.toml' extension")?
|
||||
!= "toml"
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Resource path missing '.toml' extension"
|
||||
));
|
||||
}
|
||||
Some(resource_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let res = State
|
||||
.resolve(
|
||||
@@ -266,6 +277,10 @@ impl Resolve<CommitSync, User> for State {
|
||||
update.logs.push(Log::simple("Resources", res.toml.clone()));
|
||||
|
||||
if sync.config.files_on_host {
|
||||
let Some(resource_path) = resource_path else {
|
||||
// Resource path checked above for files_on_host mode.
|
||||
unreachable!()
|
||||
};
|
||||
let file_path = core_config()
|
||||
.sync_directory
|
||||
.join(to_komodo_name(&sync.name))
|
||||
@@ -284,8 +299,8 @@ impl Resolve<CommitSync, User> for State {
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
return resource::get::<ResourceSync>(&sync.name).await;
|
||||
add_update(update.clone()).await?;
|
||||
return Ok(update);
|
||||
} else {
|
||||
update.push_simple_log(
|
||||
"Write contents",
|
||||
@@ -293,6 +308,10 @@ impl Resolve<CommitSync, User> for State {
|
||||
);
|
||||
}
|
||||
} else if !sync.config.repo.is_empty() {
|
||||
let Some(resource_path) = resource_path else {
|
||||
// Resource path checked above for repo mode.
|
||||
unreachable!()
|
||||
};
|
||||
// GIT REPO
|
||||
let args: CloneArgs = (&sync).into();
|
||||
let root = args.unique_path(&core_config().repo_directory)?;
|
||||
@@ -311,8 +330,8 @@ impl Resolve<CommitSync, User> for State {
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
return resource::get::<ResourceSync>(&sync.name).await;
|
||||
add_update(update.clone()).await?;
|
||||
return Ok(update);
|
||||
}
|
||||
}
|
||||
// ===========
|
||||
@@ -331,22 +350,18 @@ impl Resolve<CommitSync, User> for State {
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
return resource::get::<ResourceSync>(&sync.name).await;
|
||||
add_update(update.clone()).await?;
|
||||
return Ok(update);
|
||||
}
|
||||
|
||||
let res = match State
|
||||
if let Err(e) = State
|
||||
.resolve(RefreshResourceSyncPending { sync: sync.name }, user)
|
||||
.await
|
||||
{
|
||||
Ok(sync) => Ok(sync),
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Refresh sync pending",
|
||||
format_serror(&(&e).into()),
|
||||
);
|
||||
Err(e)
|
||||
}
|
||||
update.push_error_log(
|
||||
"Refresh sync pending",
|
||||
format_serror(&(&e).into()),
|
||||
);
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
@@ -365,9 +380,9 @@ impl Resolve<CommitSync, User> for State {
|
||||
.await;
|
||||
refresh_resource_sync_state_cache().await;
|
||||
}
|
||||
update_update(update).await?;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
res
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -706,6 +706,11 @@ async fn execute_execution(
|
||||
)
|
||||
.await?
|
||||
}
|
||||
// Exception: This is a write operation.
|
||||
Execution::CommitSync(req) => State
|
||||
.resolve(req, user)
|
||||
.await
|
||||
.context("Failed at CommitSync")?,
|
||||
Execution::DeployStack(req) => {
|
||||
let req = ExecuteRequest::DeployStack(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
|
||||
@@ -201,6 +201,14 @@ pub async fn get_tag_check_owner(
|
||||
Err(anyhow!("user must be tag owner or admin"))
|
||||
}
|
||||
|
||||
pub async fn get_all_tags(
|
||||
filter: impl Into<Option<Document>>,
|
||||
) -> anyhow::Result<Vec<Tag>> {
|
||||
find_collect(&db_client().tags, filter, None)
|
||||
.await
|
||||
.context("failed to query db for tags")
|
||||
}
|
||||
|
||||
pub async fn get_id_to_tags(
|
||||
filter: impl Into<Option<Document>>,
|
||||
) -> anyhow::Result<HashMap<String, Tag>> {
|
||||
|
||||
@@ -42,6 +42,7 @@ async fn get_all_servers_map() -> anyhow::Result<(
|
||||
admin: true,
|
||||
..Default::default()
|
||||
},
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get servers from db (in alert_servers)")?;
|
||||
|
||||
@@ -382,8 +382,9 @@ pub async fn get_user_permission_on_resource<T: KomodoResource>(
|
||||
pub async fn list_for_user<T: KomodoResource>(
|
||||
mut query: ResourceQuery<T::QuerySpecifics>,
|
||||
user: &User,
|
||||
all_tags: &[Tag],
|
||||
) -> anyhow::Result<Vec<T::ListItem>> {
|
||||
validate_resource_query_tags(&mut query).await;
|
||||
validate_resource_query_tags(&mut query, all_tags)?;
|
||||
let mut filters = Document::new();
|
||||
query.add_filters(&mut filters);
|
||||
list_for_user_using_document::<T>(filters, user).await
|
||||
@@ -404,8 +405,9 @@ pub async fn list_for_user_using_document<T: KomodoResource>(
|
||||
pub async fn list_full_for_user<T: KomodoResource>(
|
||||
mut query: ResourceQuery<T::QuerySpecifics>,
|
||||
user: &User,
|
||||
all_tags: &[Tag],
|
||||
) -> anyhow::Result<Vec<Resource<T::Config, T::Info>>> {
|
||||
validate_resource_query_tags(&mut query).await;
|
||||
validate_resource_query_tags(&mut query, all_tags)?;
|
||||
let mut filters = Document::new();
|
||||
query.add_filters(&mut filters);
|
||||
list_full_for_user_using_document::<T>(filters, user).await
|
||||
@@ -510,7 +512,7 @@ pub async fn create<T: KomodoResource>(
|
||||
|
||||
// Ensure an existing resource with same name doesn't already exist
|
||||
// The database indexing also ensures this but doesn't give a good error message.
|
||||
if list_full_for_user::<T>(Default::default(), system_user())
|
||||
if list_full_for_user::<T>(Default::default(), system_user(), &[])
|
||||
.await
|
||||
.context("Failed to list all resources for duplicate name check")?
|
||||
.into_iter()
|
||||
@@ -793,14 +795,24 @@ pub async fn delete<T: KomodoResource>(
|
||||
// =======
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn validate_resource_query_tags<
|
||||
T: Default + std::fmt::Debug,
|
||||
>(
|
||||
pub fn validate_resource_query_tags<T: Default + std::fmt::Debug>(
|
||||
query: &mut ResourceQuery<T>,
|
||||
) {
|
||||
let futures = query.tags.iter().map(|tag| get_tag(tag));
|
||||
let res = join_all(futures).await;
|
||||
query.tags = res.into_iter().flatten().map(|tag| tag.id).collect();
|
||||
all_tags: &[Tag],
|
||||
) -> anyhow::Result<()> {
|
||||
query.tags = query
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| {
|
||||
all_tags
|
||||
.iter()
|
||||
.find(|t| t.name == *tag || t.id == *tag)
|
||||
.map(|tag| tag.id.clone())
|
||||
.with_context(|| {
|
||||
format!("No tag found matching name or id: {}", tag)
|
||||
})
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
|
||||
@@ -494,6 +494,16 @@ async fn validate_config(
|
||||
.await?;
|
||||
params.sync = sync.id;
|
||||
}
|
||||
Execution::CommitSync(params) => {
|
||||
// This one is actually a write operation.
|
||||
let sync = super::get_check_permissions::<ResourceSync>(
|
||||
¶ms.sync,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
params.sync = sync.id;
|
||||
}
|
||||
Execution::DeployStack(params) => {
|
||||
let stack = super::get_check_permissions::<Stack>(
|
||||
¶ms.stack,
|
||||
|
||||
@@ -80,9 +80,25 @@ pub fn read_resources(
|
||||
} else {
|
||||
logs.push(Log::simple("Read remote resources", log));
|
||||
};
|
||||
} else if !full_path.exists() {
|
||||
file_errors.push(SyncFileContents {
|
||||
resource_path: String::new(),
|
||||
path: resource_path.display().to_string(),
|
||||
contents: format_serror(
|
||||
&anyhow!("Initialize the file to proceed.")
|
||||
.context(format!("Path {full_path:?} does not exist."))
|
||||
.into(),
|
||||
),
|
||||
});
|
||||
log.push_str(&format!(
|
||||
"{}: Resoure path {} does not exist.",
|
||||
colored("ERROR", Color::Red),
|
||||
bold(resource_path.display())
|
||||
));
|
||||
logs.push(Log::error("Read remote resources", log));
|
||||
} else {
|
||||
log.push_str(&format!(
|
||||
"{}: Resoure path {} is neither a file nor a directory.",
|
||||
"{}: Resoure path {} exists, but is neither a file nor a directory.",
|
||||
colored("WARN", Color::Red),
|
||||
bold(resource_path.display())
|
||||
));
|
||||
@@ -134,7 +150,7 @@ fn read_resource_file(
|
||||
log.push('\n');
|
||||
let path_for_view =
|
||||
if let Some(resource_path) = resource_path.as_ref() {
|
||||
resource_path.join(&file_path)
|
||||
resource_path.join(file_path)
|
||||
} else {
|
||||
file_path.to_path_buf()
|
||||
};
|
||||
@@ -164,7 +180,7 @@ fn read_resources_directory(
|
||||
file_errors: &mut Vec<SyncFileContents>,
|
||||
) -> anyhow::Result<()> {
|
||||
let full_resource_path = root_path.join(resource_path);
|
||||
let full_path = full_resource_path.join(&curr_path);
|
||||
let full_path = full_resource_path.join(curr_path);
|
||||
let directory = fs::read_dir(&full_path).with_context(|| {
|
||||
format!("Failed to read directory contents at {full_path:?}")
|
||||
})?;
|
||||
|
||||
@@ -588,6 +588,13 @@ impl ResourceSyncTrait for Procedure {
|
||||
.map(|s| s.name.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
Execution::CommitSync(config) => {
|
||||
config.sync = resources
|
||||
.syncs
|
||||
.get(&config.sync)
|
||||
.map(|s| s.name.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
Execution::DeployStack(config) => {
|
||||
config.stack = resources
|
||||
.stacks
|
||||
|
||||
@@ -687,6 +687,13 @@ impl ToToml for Procedure {
|
||||
.map(|r| &r.name)
|
||||
.unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::CommitSync(exec) => exec.sync.clone_from(
|
||||
all
|
||||
.syncs
|
||||
.get(&exec.sync)
|
||||
.map(|r| &r.name)
|
||||
.unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::DeployStack(exec) => exec.stack.clone_from(
|
||||
all
|
||||
.stacks
|
||||
|
||||
@@ -137,24 +137,6 @@ impl Resolve<GetComposeServiceLogSearch> for State {
|
||||
|
||||
//
|
||||
|
||||
const DEFAULT_COMPOSE_CONTENTS: &str = "## 🦎 Hello Komodo 🦎
|
||||
services:
|
||||
hello_world:
|
||||
image: hello-world
|
||||
# networks:
|
||||
# - default
|
||||
# ports:
|
||||
# - 3000:3000
|
||||
# volumes:
|
||||
# - data:/data
|
||||
|
||||
# networks:
|
||||
# default: {}
|
||||
|
||||
# volumes:
|
||||
# data:
|
||||
";
|
||||
|
||||
impl Resolve<GetComposeContentsOnHost, ()> for State {
|
||||
#[instrument(
|
||||
name = "GetComposeContentsOnHost",
|
||||
@@ -186,11 +168,6 @@ impl Resolve<GetComposeContentsOnHost, ()> for State {
|
||||
for path in file_paths {
|
||||
let full_path =
|
||||
run_directory.join(&path).components().collect::<PathBuf>();
|
||||
if !full_path.exists() {
|
||||
fs::write(&full_path, DEFAULT_COMPOSE_CONTENTS)
|
||||
.await
|
||||
.context("Failed to init missing compose file on host")?;
|
||||
}
|
||||
match fs::read_to_string(&full_path).await.with_context(|| {
|
||||
format!(
|
||||
"Failed to read compose file contents at {full_path:?}"
|
||||
|
||||
@@ -23,7 +23,10 @@ pub use server_template::*;
|
||||
pub use stack::*;
|
||||
pub use sync::*;
|
||||
|
||||
use crate::entities::{NoData, I64};
|
||||
use crate::{
|
||||
api::write::CommitSync,
|
||||
entities::{NoData, I64},
|
||||
};
|
||||
|
||||
pub trait KomodoExecuteRequest: HasResponse {}
|
||||
|
||||
@@ -101,6 +104,7 @@ pub enum Execution {
|
||||
|
||||
// SYNC
|
||||
RunSync(RunSync),
|
||||
CommitSync(CommitSync), // This is a special case, its actually a write operation.
|
||||
|
||||
// STACK
|
||||
DeployStack(DeployStack),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use clap::Parser;
|
||||
use derive_empty_traits::EmptyTraits;
|
||||
use resolver_api::derive::Request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -121,15 +122,22 @@ pub struct WriteSyncFileContents {
|
||||
|
||||
//
|
||||
|
||||
/// Commits matching resources updated configuration to the target resource sync. Response: [Update]
|
||||
/// Exports matching resources, and writes to the target sync's resource file. Response: [Update]
|
||||
///
|
||||
/// Note. Will fail if the Sync is not `managed`.
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Request,
|
||||
EmptyTraits,
|
||||
Parser,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(ResourceSync)]
|
||||
#[response(Update)]
|
||||
pub struct CommitSync {
|
||||
/// Id or name
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
|
||||
@@ -265,6 +265,17 @@ impl ResourceSyncConfig {
|
||||
pub fn builder() -> ResourceSyncConfigBuilder {
|
||||
ResourceSyncConfigBuilder::default()
|
||||
}
|
||||
|
||||
/// Checks for empty file contents, ignoring whitespace / comments.
|
||||
pub fn file_contents_empty(&self) -> bool {
|
||||
self
|
||||
.file_contents
|
||||
.split('\n')
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
||||
.count()
|
||||
== 0
|
||||
}
|
||||
}
|
||||
|
||||
fn default_git_provider() -> String {
|
||||
|
||||
@@ -864,6 +864,7 @@ export type Execution =
|
||||
| { type: "PruneBuildx", params: PruneBuildx }
|
||||
| { type: "PruneSystem", params: PruneSystem }
|
||||
| { type: "RunSync", params: RunSync }
|
||||
| { type: "CommitSync", params: CommitSync }
|
||||
| { type: "DeployStack", params: DeployStack }
|
||||
| { type: "DeployStackIfChanged", params: DeployStackIfChanged }
|
||||
| { type: "StartStack", params: StartStack }
|
||||
@@ -6084,7 +6085,7 @@ export interface WriteSyncFileContents {
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits matching resources updated configuration to the target resource sync. Response: [Update]
|
||||
* Exports matching resources, and writes to the target sync's resource file. Response: [Update]
|
||||
*
|
||||
* Note. Will fail if the Sync is not `managed`.
|
||||
*/
|
||||
|
||||
@@ -1059,6 +1059,17 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
|
||||
/>
|
||||
),
|
||||
},
|
||||
CommitSync: {
|
||||
params: { sync: "" },
|
||||
Component: ({ params, setParams, disabled }) => (
|
||||
<ResourceSelector
|
||||
type="ResourceSync"
|
||||
selected={params.sync}
|
||||
onSelect={(id) => setParams({ sync: id })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
Sleep: {
|
||||
params: { duration_ms: 0 },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ActionButton, ActionWithDialog } from "@components/util";
|
||||
import { useExecute, useInvalidate, useRead, useWrite } from "@lib/hooks";
|
||||
import { sync_no_changes } from "@lib/utils";
|
||||
import { file_contents_empty, sync_no_changes } from "@lib/utils";
|
||||
import { useEditPermissions } from "@pages/resource";
|
||||
import { NotebookPen, RefreshCcw, SquarePlay } from "lucide-react";
|
||||
import { useFullResourceSync } from ".";
|
||||
@@ -68,7 +68,7 @@ export const CommitSync = ({ id }: { id: string }) => {
|
||||
|
||||
const freshSync =
|
||||
!sync.config?.files_on_host &&
|
||||
!sync.config?.file_contents &&
|
||||
file_contents_empty(sync.config?.file_contents) &&
|
||||
!sync.config?.repo;
|
||||
|
||||
if (!freshSync && (!sync.config?.managed || sync_no_changes(sync))) {
|
||||
@@ -97,6 +97,4 @@ export const CommitSync = ({ id }: { id: string }) => {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import { CopyGithubWebhook } from "../common";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { text_color_class_by_intention } from "@lib/color";
|
||||
import { ConfirmButton, ShowHideButton } from "@components/util";
|
||||
import { Ban, CirclePlus } from "lucide-react";
|
||||
import { Ban, CirclePlus, MinusCircle, SearchX, Tag } from "lucide-react";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
import {
|
||||
Select,
|
||||
@@ -21,6 +21,17 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/select";
|
||||
import { filterBySplit } from "@lib/utils";
|
||||
import { Button } from "@ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@ui/command";
|
||||
|
||||
type SyncMode = "UI Defined" | "Files On Server" | "Git Repo" | undefined;
|
||||
const SYNC_MODES: SyncMode[] = ["UI Defined", "Files On Server", "Git Repo"];
|
||||
@@ -139,19 +150,9 @@ export const ResourceSyncConfig = ({
|
||||
|
||||
const match_tags: ConfigComponent<Types.ResourceSyncConfig> = {
|
||||
label: "Match Tags",
|
||||
description: "Only sync resources matching all of these tags.",
|
||||
components: {
|
||||
match_tags: (values, set) => (
|
||||
<ConfigList
|
||||
label="Match Tags"
|
||||
addLabel="Add Tag"
|
||||
description="Only sync resources matching these tags."
|
||||
field="match_tags"
|
||||
values={values ?? []}
|
||||
set={set}
|
||||
disabled={disabled}
|
||||
placeholder="Input tag name"
|
||||
/>
|
||||
),
|
||||
match_tags: (values, set) => <MatchTags tags={values ?? []} set={set} />,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -507,3 +508,103 @@ export const ResourceSyncConfig = ({
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const MatchTags = ({
|
||||
tags,
|
||||
set,
|
||||
}: {
|
||||
tags: string[];
|
||||
set: (update: Partial<Types.ResourceSyncConfig>) => void;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
const all_tags = useRead("ListTags", {}).data;
|
||||
const filtered = filterBySplit(all_tags, search, (item) => item.name);
|
||||
return (
|
||||
<div className="flex gap-3 items-center">
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setSearch("");
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="flex items-center gap-2">
|
||||
<Tag className="w-3 h-3" />
|
||||
Select Tag
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[200px] max-h-[200px] p-0"
|
||||
sideOffset={12}
|
||||
align="start"
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Search Tags"
|
||||
className="h-9"
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty className="flex justify-evenly items-center pt-2">
|
||||
No Tags Found
|
||||
<SearchX className="w-3 h-3" />
|
||||
</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{filtered
|
||||
?.filter((tag) => !tags.includes(tag.name))
|
||||
.map((tag) => (
|
||||
<CommandItem
|
||||
key={tag.name}
|
||||
onSelect={() => {
|
||||
set({ match_tags: [...tags, tag.name] });
|
||||
setSearch("");
|
||||
setOpen(false);
|
||||
}}
|
||||
className="flex items-center justify-between cursor-pointer"
|
||||
>
|
||||
<div className="p-1">{tag.name}</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<MatchTagsTags
|
||||
tags={tags}
|
||||
onBadgeClick={(tag) =>
|
||||
set({ match_tags: tags.filter((name) => name !== tag) })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MatchTagsTags = ({
|
||||
tags,
|
||||
onBadgeClick,
|
||||
}: {
|
||||
tags?: string[];
|
||||
onBadgeClick: (tag: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{tags?.map((tag) => (
|
||||
<Button
|
||||
key={tag}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => onBadgeClick && onBadgeClick(tag)}
|
||||
>
|
||||
{tag}
|
||||
<MinusCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Section } from "@components/layouts";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { Card, CardContent, CardHeader } from "@ui/card";
|
||||
import { useFullResourceSync } from ".";
|
||||
import { updateLogToHtml } from "@lib/utils";
|
||||
import { cn, updateLogToHtml } from "@lib/utils";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
import { useEditPermissions } from "@pages/resource";
|
||||
import { useWrite } from "@lib/hooks";
|
||||
@@ -10,7 +10,7 @@ import { useToast } from "@ui/use-toast";
|
||||
import { Button } from "@ui/button";
|
||||
import { FilePlus, History } from "lucide-react";
|
||||
import { ConfirmUpdate } from "@components/config/util";
|
||||
import { ConfirmButton } from "@components/util";
|
||||
import { ConfirmButton, ShowHideButton } from "@components/util";
|
||||
|
||||
export const ResourceSyncInfo = ({
|
||||
id,
|
||||
@@ -20,6 +20,7 @@ export const ResourceSyncInfo = ({
|
||||
titleOther: ReactNode;
|
||||
}) => {
|
||||
const [edits, setEdits] = useState<Record<string, string | undefined>>({});
|
||||
const [show, setShow] = useState<Record<string, boolean | undefined>>({});
|
||||
const { canWrite } = useEditPermissions({ type: "ResourceSync", id });
|
||||
const { toast } = useToast();
|
||||
const { mutateAsync, isPending } = useWrite("WriteSyncFileContents", {
|
||||
@@ -34,12 +35,15 @@ export const ResourceSyncInfo = ({
|
||||
const file_on_host = sync?.config?.files_on_host ?? false;
|
||||
const git_repo = sync?.config?.repo ? true : false;
|
||||
const canEdit = canWrite && (file_on_host || git_repo);
|
||||
const editFileCallback = (path: string) => (contents: string) =>
|
||||
setEdits({ ...edits, [path]: contents });
|
||||
const editFileCallback = (keyPath: string) => (contents: string) =>
|
||||
setEdits({ ...edits, [keyPath]: contents });
|
||||
|
||||
const latest_contents = sync?.info?.remote_contents;
|
||||
const latest_errors = sync?.info?.remote_errors;
|
||||
|
||||
// Contents will be default hidden if there is more than 2 file editor to show
|
||||
const default_show_contents = !latest_contents || latest_contents.length < 3;
|
||||
|
||||
return (
|
||||
<Section titleOther={titleOther}>
|
||||
{/* Errors */}
|
||||
@@ -65,7 +69,7 @@ export const ResourceSyncInfo = ({
|
||||
</div>
|
||||
{canEdit && (
|
||||
<ConfirmButton
|
||||
title="Init File"
|
||||
title="Initialize File"
|
||||
icon={<FilePlus className="w-4 h-4" />}
|
||||
onClick={() => {
|
||||
if (sync) {
|
||||
@@ -95,69 +99,86 @@ export const ResourceSyncInfo = ({
|
||||
{/* Update latest contents */}
|
||||
{latest_contents &&
|
||||
latest_contents.length > 0 &&
|
||||
latest_contents.map((content) => (
|
||||
<Card key={content.path} className="flex flex-col gap-4">
|
||||
<CardHeader className="flex flex-row justify-between items-center pb-0">
|
||||
<div className="font-mono flex gap-4">
|
||||
{content.resource_path && (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
<div className="text-muted-foreground">Folder:</div>
|
||||
{content.resource_path}
|
||||
</div>
|
||||
<div className="text-muted-foreground">|</div>
|
||||
</>
|
||||
latest_contents.map((content) => {
|
||||
const keyPath = content.resource_path + "/" + content.path;
|
||||
const showContents = show[keyPath] ?? default_show_contents;
|
||||
return (
|
||||
<Card key={keyPath} className="flex flex-col gap-4">
|
||||
<CardHeader
|
||||
className={cn(
|
||||
"flex flex-row justify-between items-center",
|
||||
showContents && "pb-2"
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<div className="text-muted-foreground">File:</div>
|
||||
{content.path}
|
||||
>
|
||||
<div className="font-mono flex gap-4">
|
||||
{content.resource_path && (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
<div className="text-muted-foreground">Folder:</div>
|
||||
{content.resource_path}
|
||||
</div>
|
||||
<div className="text-muted-foreground">|</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<div className="text-muted-foreground">File:</div>
|
||||
{content.path}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{canEdit && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setEdits({ ...edits, [content.path]: undefined })
|
||||
}
|
||||
className="flex items-center gap-2"
|
||||
disabled={!edits[content.path]}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
Reset
|
||||
</Button>
|
||||
<ConfirmUpdate
|
||||
previous={{ contents: content.contents }}
|
||||
content={{ contents: edits[content.path] }}
|
||||
onConfirm={async () => {
|
||||
if (sync) {
|
||||
return await mutateAsync({
|
||||
sync: sync.name,
|
||||
resource_path: content.resource_path ?? "",
|
||||
file_path: content.path,
|
||||
contents: edits[content.path]!,
|
||||
}).then(() =>
|
||||
setEdits({ ...edits, [content.path]: undefined })
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={!edits[content.path]}
|
||||
language="toml"
|
||||
loading={isPending}
|
||||
<div className="flex items-center gap-3">
|
||||
{canEdit && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setEdits({ ...edits, [keyPath]: undefined })
|
||||
}
|
||||
className="flex items-center gap-2"
|
||||
disabled={!edits[keyPath]}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
Reset
|
||||
</Button>
|
||||
<ConfirmUpdate
|
||||
previous={{ contents: content.contents }}
|
||||
content={{ contents: edits[keyPath] }}
|
||||
onConfirm={async () => {
|
||||
if (sync) {
|
||||
return await mutateAsync({
|
||||
sync: sync.name,
|
||||
resource_path: content.resource_path ?? "",
|
||||
file_path: content.path,
|
||||
contents: edits[keyPath]!,
|
||||
}).then(() =>
|
||||
setEdits({ ...edits, [keyPath]: undefined })
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={!edits[keyPath]}
|
||||
language="toml"
|
||||
loading={isPending}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ShowHideButton
|
||||
show={showContents}
|
||||
setShow={(val) => setShow({ ...show, [keyPath]: val })}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
{showContents && (
|
||||
<CardContent className="pr-8">
|
||||
<MonacoEditor
|
||||
value={edits[keyPath] ?? content.contents}
|
||||
language="toml"
|
||||
readOnly={!canEdit}
|
||||
onValueChange={editFileCallback(keyPath)}
|
||||
/>
|
||||
</CardContent>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="pr-8">
|
||||
<MonacoEditor
|
||||
value={edits[content.path] ?? content.contents}
|
||||
language="toml"
|
||||
readOnly={!canEdit}
|
||||
onValueChange={editFileCallback(content.path)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Section } from "@components/layouts";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { Card, CardContent, CardHeader } from "@ui/card";
|
||||
import { useFullStack } from ".";
|
||||
import { updateLogToHtml } from "@lib/utils";
|
||||
import { cn, updateLogToHtml } from "@lib/utils";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
import { useEditPermissions } from "@pages/resource";
|
||||
import { ConfirmUpdate } from "@components/config/util";
|
||||
@@ -10,7 +10,7 @@ import { useWrite } from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import { FilePlus, History } from "lucide-react";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { ConfirmButton } from "@components/util";
|
||||
import { ConfirmButton, ShowHideButton } from "@components/util";
|
||||
import { DEFAULT_STACK_FILE_CONTENTS } from "./config";
|
||||
|
||||
export const StackInfo = ({
|
||||
@@ -21,6 +21,7 @@ export const StackInfo = ({
|
||||
titleOther: ReactNode;
|
||||
}) => {
|
||||
const [edits, setEdits] = useState<Record<string, string | undefined>>({});
|
||||
const [show, setShow] = useState<Record<string, boolean | undefined>>({});
|
||||
const { canWrite } = useEditPermissions({ type: "Stack", id });
|
||||
const { toast } = useToast();
|
||||
const { mutateAsync, isPending } = useWrite("WriteStackFileContents", {
|
||||
@@ -72,6 +73,9 @@ export const StackInfo = ({
|
||||
const latest_contents = stack?.info?.remote_contents;
|
||||
const latest_errors = stack?.info?.remote_errors;
|
||||
|
||||
// Contents will be default hidden if there is more than 2 file editor to show
|
||||
const default_show_contents = !latest_contents || latest_contents.length < 3;
|
||||
|
||||
return (
|
||||
<Section titleOther={titleOther}>
|
||||
{/* Errors */}
|
||||
@@ -86,7 +90,7 @@ export const StackInfo = ({
|
||||
</div>
|
||||
{canEdit && (
|
||||
<ConfirmButton
|
||||
title="Init File"
|
||||
title="Initialize File"
|
||||
icon={<FilePlus className="w-4 h-4" />}
|
||||
onClick={() => {
|
||||
if (stack) {
|
||||
@@ -188,57 +192,73 @@ export const StackInfo = ({
|
||||
{/* Update latest contents */}
|
||||
{latest_contents &&
|
||||
latest_contents.length > 0 &&
|
||||
latest_contents.map((content) => (
|
||||
<Card key={content.path} className="flex flex-col gap-4">
|
||||
<CardHeader className="flex flex-row justify-between items-center pb-0">
|
||||
<div className="font-mono flex gap-2">
|
||||
<div className="text-muted-foreground">File:</div>
|
||||
{content.path}
|
||||
</div>
|
||||
{canEdit && (
|
||||
latest_contents.map((content) => {
|
||||
const showContents = show[content.path] ?? default_show_contents;
|
||||
return (
|
||||
<Card key={content.path} className="flex flex-col gap-4">
|
||||
<CardHeader
|
||||
className={cn(
|
||||
"flex flex-row justify-between items-center",
|
||||
showContents && "pb-2"
|
||||
)}
|
||||
>
|
||||
<div className="font-mono flex gap-2">
|
||||
<div className="text-muted-foreground">File:</div>
|
||||
{content.path}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setEdits({ ...edits, [content.path]: undefined })
|
||||
}
|
||||
className="flex items-center gap-2"
|
||||
disabled={!edits[content.path]}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
Reset
|
||||
</Button>
|
||||
<ConfirmUpdate
|
||||
previous={{ contents: content.contents }}
|
||||
content={{ contents: edits[content.path] }}
|
||||
onConfirm={async () => {
|
||||
if (stack) {
|
||||
return await mutateAsync({
|
||||
stack: stack.name,
|
||||
file_path: content.path,
|
||||
contents: edits[content.path]!,
|
||||
}).then(() =>
|
||||
{canEdit && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setEdits({ ...edits, [content.path]: undefined })
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={!edits[content.path]}
|
||||
language="yaml"
|
||||
loading={isPending}
|
||||
}
|
||||
className="flex items-center gap-2"
|
||||
disabled={!edits[content.path]}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
Reset
|
||||
</Button>
|
||||
<ConfirmUpdate
|
||||
previous={{ contents: content.contents }}
|
||||
content={{ contents: edits[content.path] }}
|
||||
onConfirm={async () => {
|
||||
if (stack) {
|
||||
return await mutateAsync({
|
||||
stack: stack.name,
|
||||
file_path: content.path,
|
||||
contents: edits[content.path]!,
|
||||
}).then(() =>
|
||||
setEdits({ ...edits, [content.path]: undefined })
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={!edits[content.path]}
|
||||
language="yaml"
|
||||
loading={isPending}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ShowHideButton
|
||||
show={showContents}
|
||||
setShow={(val) => setShow({ ...show, [content.path]: val })}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
{showContents && (
|
||||
<CardContent className="pr-8">
|
||||
<MonacoEditor
|
||||
value={edits[content.path] ?? content.contents}
|
||||
language="yaml"
|
||||
readOnly={!canEdit}
|
||||
onValueChange={editFileCallback(content.path)}
|
||||
/>
|
||||
</CardContent>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="pr-8">
|
||||
<MonacoEditor
|
||||
value={edits[content.path] ?? content.contents}
|
||||
language="yaml"
|
||||
readOnly={!canEdit}
|
||||
onValueChange={editFileCallback(content.path)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -253,3 +253,14 @@ export const extract_registry_domain = (image_name: string) => {
|
||||
return "docker.io";
|
||||
}
|
||||
};
|
||||
|
||||
/** Checks file contents empty, not including whitespace / comments */
|
||||
export const file_contents_empty = (contents?: string) => {
|
||||
if (!contents) return true;
|
||||
return (
|
||||
contents
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length !== 0 && !line.startsWith("#")).length === 0
|
||||
);
|
||||
};
|
||||
|
||||
@@ -60,7 +60,7 @@ pub async fn commit_file_inner(
|
||||
file: &Path,
|
||||
) {
|
||||
ensure_global_git_config_set().await;
|
||||
|
||||
|
||||
let add_log = run_komodo_command(
|
||||
"add files",
|
||||
repo_dir,
|
||||
@@ -146,8 +146,7 @@ pub async fn commit_all(repo_dir: &Path, message: &str) -> GitRes {
|
||||
};
|
||||
|
||||
let push_log =
|
||||
run_komodo_command("push", repo_dir, format!("git push -f"))
|
||||
.await;
|
||||
run_komodo_command("push", repo_dir, "git push -f").await;
|
||||
res.logs.push(push_log);
|
||||
|
||||
res
|
||||
|
||||
Reference in New Issue
Block a user