Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dre): Discourse client for automatic forum post management around proposals. #1098

Merged
merged 13 commits into from
Nov 21, 2024
Merged
6 changes: 3 additions & 3 deletions Cargo.lock

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

18 changes: 18 additions & 0 deletions rs/cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,30 @@ impl TryFrom<String> for AuthOpts {
}
}

#[derive(ClapArgs, Debug, Clone)]
pub struct DiscourseOpts {
/// Api key used to interact with the forum
#[clap(long, env = "DISCOURSE_API_KEY")]
pub(crate) discourse_api_key: Option<String>,

/// Api key used to interact with the forum
NikolaMilosa marked this conversation as resolved.
Show resolved Hide resolved
#[clap(long, env = "DISCOURSE_API_USER")]
pub(crate) discourse_api_user: Option<String>,

/// Api url used to interact with the forum
#[clap(long, env = "DISCOURSE_API_URL")]
pub(crate) discourse_api_url: Option<String>,
}

#[derive(Parser, Debug)]
#[clap(version = env!("CARGO_PKG_VERSION"), about, author)]
pub struct Args {
#[clap(flatten)]
pub(crate) auth_opts: AuthOpts,

#[clap(flatten)]
pub(crate) discourse_opts: DiscourseOpts,

/// Neuron ID
#[clap(long, global = true, env = "NEURON_ID")]
pub neuron_id: Option<u64>,
Expand Down
56 changes: 54 additions & 2 deletions rs/cli/src/commands/subnet/replace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ use clap::{error::ErrorKind, Args};

use ic_types::PrincipalId;
use itertools::Itertools;
use log::warn;

use crate::{
commands::{AuthRequirement, ExecutableCommand},
discourse_client::parse_proposal_id_from_governance_response,
ic_admin::ProposeOptions,
subnet_manager::SubnetTarget,
};

Expand Down Expand Up @@ -54,7 +57,6 @@ impl ExecutableCommand for Replace {
_ => SubnetTarget::FromNodesIds(self.nodes.clone()),
};

let runner = ctx.runner().await?;
let all_nodes = ctx.registry().await.nodes().await?.values().cloned().collect_vec();

let subnet_manager = ctx.subnet_manager().await?;
Expand All @@ -71,9 +73,59 @@ impl ExecutableCommand for Replace {
)
.await?;

let runner = ctx.runner().await?;

let subnet_id = subnet_change_response.subnet_id;
// Should be refactored to not require forum post links like this.
if let Some(runner_proposal) = runner.propose_subnet_change(subnet_change_response, ctx.forum_post_link()).await? {
let ic_admin = ctx.ic_admin().await?;
ic_admin.propose_run(runner_proposal.cmd, runner_proposal.opts).await?;
if !ic_admin
.propose_print_and_confirm(runner_proposal.cmd.clone(), runner_proposal.opts.clone())
.await?
{
return Ok(());
}

let discourse_client = ctx.discourse_client()?;
let maybe_topic = if let Some(id) = subnet_id {
let summary = match (&runner_proposal.opts.summary, &runner_proposal.opts.motivation) {
(Some(s), _) => s,
(None, Some(m)) => m,
_ => {
return Err(anyhow::anyhow!(
"Expected to have `summary` or `motivation` for proposal. Got: {:?}",
runner_proposal
))
}
};
discourse_client.create_replace_nodes_forum_post(id, summary.to_string()).await?
} else {
None
};

let proposal_response = ic_admin
.propose_submit(
runner_proposal.cmd,
ProposeOptions {
forum_post_link: match (maybe_topic.as_ref(), runner_proposal.opts.forum_post_link.as_ref()) {
(Some(discourse_response), _) => Some(discourse_response.url.clone()),
(None, Some(from_cli_or_auto_formated)) => Some(from_cli_or_auto_formated.clone()),
_ => {
warn!("Didn't find a link to forum post from discourse, cli and couldn't auto-format it.");
warn!("Will not add forum post to the proposal");
None
}
},
..runner_proposal.opts
},
)
.await?;

if let Some(topic) = maybe_topic {
discourse_client
.add_proposal_url_to_post(topic.id, parse_proposal_id_from_governance_response(proposal_response)?)
.await?
}
}
Ok(())
}
Expand Down
48 changes: 46 additions & 2 deletions rs/cli/src/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ use log::warn;
use crate::{
artifact_downloader::{ArtifactDownloader, ArtifactDownloaderImpl},
auth::Neuron,
commands::{Args, AuthOpts, AuthRequirement, ExecutableCommand, IcAdminVersion},
commands::{Args, AuthOpts, AuthRequirement, DiscourseOpts, ExecutableCommand, IcAdminVersion},
cordoned_feature_fetcher::CordonedFeatureFetcher,
discourse_client::{DiscourseClient, DiscourseClientImp},
ic_admin::{IcAdmin, IcAdminImpl},
runner::Runner,
store::Store,
Expand Down Expand Up @@ -47,6 +48,8 @@ pub struct DreContext {
cordoned_features_fetcher: Arc<dyn CordonedFeatureFetcher>,
health_client: Arc<dyn HealthStatusQuerier>,
store: Store,
discourse_opts: DiscourseOpts,
discourse_client: RefCell<Option<Arc<dyn DiscourseClient>>>,
}

#[allow(clippy::too_many_arguments)]
Expand All @@ -64,6 +67,7 @@ impl DreContext {
cordoned_features_fetcher: Arc<dyn CordonedFeatureFetcher>,
health_client: Arc<dyn HealthStatusQuerier>,
store: Store,
discourse_opts: DiscourseOpts,
) -> anyhow::Result<Self> {
Ok(Self {
proposal_agent: Arc::new(ProposalAgentImpl::new(&network.nns_urls)),
Expand All @@ -87,6 +91,8 @@ impl DreContext {
cordoned_features_fetcher,
health_client,
store,
discourse_opts,
discourse_client: RefCell::new(None),
})
}

Expand All @@ -113,6 +119,7 @@ impl DreContext {
store.cordoned_features_fetcher()?,
store.health_client(&network)?,
store,
args.discourse_opts.clone(),
)
.await
}
Expand Down Expand Up @@ -242,6 +249,35 @@ impl DreContext {
pub fn health_client(&self) -> Arc<dyn HealthStatusQuerier> {
self.health_client.clone()
}

pub fn discourse_client(&self) -> anyhow::Result<Arc<dyn DiscourseClient>> {
if let Some(client) = self.discourse_client.borrow().as_ref() {
return Ok(client.clone());
}

let (api_key, api_user, forum_url) = match (
self.discourse_opts.discourse_api_key.clone(),
self.discourse_opts.discourse_api_user.clone(),
self.discourse_opts.discourse_api_url.clone(),
) {
(Some(api_key), Some(api_user), Some(forum_url)) => (api_key, api_user, forum_url),
_ => anyhow::bail!(
"Expected to have both `api_key` and `forum_url`. Instead found: {:?}",
self.discourse_opts
),
};

let client = Arc::new(DiscourseClientImp::new(
forum_url,
api_key,
api_user,
// `offline` for discourse client means that it shouldn't try and create posts.
// It can happen because the tool runs in offline mode, or if its a dry run.
self.store.is_offline() || self.dry_run,
)?);
*self.discourse_client.borrow_mut() = Some(client.clone());
Ok(client)
}
}

#[cfg(test)]
Expand All @@ -255,8 +291,9 @@ pub mod tests {
use crate::{
artifact_downloader::ArtifactDownloader,
auth::Neuron,
commands::{AuthOpts, HsmOpts, HsmParams},
commands::{AuthOpts, DiscourseOpts, HsmOpts, HsmParams},
cordoned_feature_fetcher::CordonedFeatureFetcher,
discourse_client::DiscourseClient,
ic_admin::IcAdmin,
store::Store,
};
Expand All @@ -273,6 +310,7 @@ pub mod tests {
artifact_downloader: Arc<dyn ArtifactDownloader>,
cordoned_features_fetcher: Arc<dyn CordonedFeatureFetcher>,
health_client: Arc<dyn HealthStatusQuerier>,
discourse_client: Arc<dyn DiscourseClient>,
) -> DreContext {
DreContext {
network,
Expand Down Expand Up @@ -308,6 +346,12 @@ pub mod tests {
cordoned_features_fetcher,
health_client,
store: Store::new(false).unwrap(),
discourse_opts: DiscourseOpts {
discourse_api_key: None,
discourse_api_url: None,
discourse_api_user: None,
},
discourse_client: RefCell::new(Some(discourse_client)),
}
}
}
Loading
Loading