* 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:
Maxwell Becker
2024-10-13 18:03:16 -04:00
committed by GitHub
parent 581d7e0b2c
commit 5088dc5c3c
37 changed files with 717 additions and 279 deletions

26
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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")?;

View File

@@ -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
}
}

View File

@@ -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();

View File

@@ -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
}
}

View File

@@ -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();

View File

@@ -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,
&[]
),
)?;

View File

@@ -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")?;

View File

@@ -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();

View File

@@ -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();

View File

@@ -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
}
}

View File

@@ -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")?;

View File

@@ -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")?;

View File

@@ -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()

View File

@@ -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)
}
}

View File

@@ -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?;

View File

@@ -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>> {

View File

@@ -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)")?;

View File

@@ -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]

View File

@@ -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>(
&params.sync,
user,
PermissionLevel::Write,
)
.await?;
params.sync = sync.id;
}
Execution::DeployStack(params) => {
let stack = super::get_check_permissions::<Stack>(
&params.stack,

View File

@@ -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:?}")
})?;

View File

@@ -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

View File

@@ -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

View File

@@ -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:?}"

View File

@@ -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),

View File

@@ -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")]

View File

@@ -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 {

View File

@@ -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`.
*/

View File

@@ -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 },

View File

@@ -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 }) => {
/>
);
}
};

View File

@@ -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>
))}
</>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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
);
};

View File

@@ -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