From 97c6442f49a6d9063646b8fbdb7451cc74660fb9 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Mon, 15 Jul 2024 14:19:14 +0200 Subject: [PATCH 01/12] feat(cli/secrets): Add `secret` subcommand to `wasmer app` --- Cargo.lock | 7 + lib/backend-api/schema.graphql | 190 ++++++++++++--- lib/backend-api/src/query.rs | 199 +++++++++++++++- lib/backend-api/src/types.rs | 125 ++++++++++ lib/cli/Cargo.toml | 1 + lib/cli/src/commands/app/mod.rs | 4 + lib/cli/src/commands/app/secrets/create.rs | 225 ++++++++++++++++++ lib/cli/src/commands/app/secrets/delete.rs | 182 ++++++++++++++ lib/cli/src/commands/app/secrets/list.rs | 87 +++++++ lib/cli/src/commands/app/secrets/mod.rs | 49 ++++ lib/cli/src/commands/app/secrets/reveal.rs | 141 +++++++++++ lib/cli/src/commands/app/secrets/update.rs | 221 +++++++++++++++++ lib/cli/src/commands/app/secrets/utils/mod.rs | 84 +++++++ .../src/commands/app/secrets/utils/render.rs | 110 +++++++++ lib/cli/src/commands/app/util.rs | 30 ++- 15 files changed, 1614 insertions(+), 41 deletions(-) create mode 100644 lib/cli/src/commands/app/secrets/create.rs create mode 100644 lib/cli/src/commands/app/secrets/delete.rs create mode 100644 lib/cli/src/commands/app/secrets/list.rs create mode 100644 lib/cli/src/commands/app/secrets/mod.rs create mode 100644 lib/cli/src/commands/app/secrets/reveal.rs create mode 100644 lib/cli/src/commands/app/secrets/update.rs create mode 100644 lib/cli/src/commands/app/secrets/utils/mod.rs create mode 100644 lib/cli/src/commands/app/secrets/utils/render.rs diff --git a/Cargo.lock b/Cargo.lock index 0263b2afeb4..77a26d62b26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1440,6 +1440,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dunce" version = "1.0.4" @@ -6531,6 +6537,7 @@ dependencies = [ "console", "dialoguer", "dirs", + "dotenvy", "edge-schema 0.1.0", "edge-util", "flate2", diff --git a/lib/backend-api/schema.graphql b/lib/backend-api/schema.graphql index ee38b06e560..6b6ee380633 100644 --- a/lib/backend-api/schema.graphql +++ b/lib/backend-api/schema.graphql @@ -16,9 +16,6 @@ interface Node { type PublicKey implements Node { """The ID of the object""" id: ID! - createdAt: DateTime! - updatedAt: DateTime! - deletedAt: DateTime owner: User! keyId: String! key: String! @@ -28,13 +25,6 @@ type PublicKey implements Node { revoked: Boolean! } -""" -The `DateTime` scalar type represents a DateTime -value as specified by -[iso8601](https://en.wikipedia.org/wiki/ISO_8601). -""" -scalar DateTime - type User implements Node & PackageOwner & Owner { firstName: String! lastName: String! @@ -94,6 +84,13 @@ interface Owner { globalId: ID! } +""" +The `DateTime` scalar type represents a DateTime +value as specified by +[iso8601](https://en.wikipedia.org/wiki/ISO_8601). +""" +scalar DateTime + type ActivityEventConnection { """Pagination data for this connection.""" pageInfo: PageInfo! @@ -193,11 +190,9 @@ type NamespaceEdge { type Namespace implements Node & PackageOwner & Owner { """The ID of the object""" id: ID! - deletedAt: DateTime name: String! displayName: String description: String! - avatar: String! avatarUpdatedAt: DateTime twitterHandle: String githubHandle: String @@ -209,6 +204,7 @@ type Namespace implements Node & PackageOwner & Owner { userSet(offset: Int, before: String, after: String, first: Int, last: Int): UserConnection! globalName: String! globalId: ID! + avatar: String! packages(offset: Int, before: String, after: String, first: Int, last: Int): PackageConnection! apps(sortBy: DeployAppsSortBy, offset: Int, before: String, after: String, first: Int, last: Int): DeployAppConnection! packageVersions(offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionConnection! @@ -253,8 +249,6 @@ type NamespaceCollaboratorInviteEdge { type NamespaceCollaboratorInvite implements Node { """The ID of the object""" id: ID! - updatedAt: DateTime! - deletedAt: DateTime requestedBy: User! user: User inviteEmail: String @@ -285,7 +279,6 @@ enum RegistryNamespaceMaintainerInviteRoleChoices { type NamespaceCollaborator implements Node { """The ID of the object""" id: ID! - deletedAt: DateTime user: User! role: RegistryNamespaceMaintainerRoleChoices! namespace: Namespace! @@ -351,7 +344,6 @@ type PackageEdge { type Package implements Likeable & Node & PackageOwner { """The ID of the object""" id: ID! - deletedAt: DateTime name: String! private: Boolean! createdAt: DateTime! @@ -949,7 +941,6 @@ type BindingsGenerator implements Node { id: ID! createdAt: DateTime! updatedAt: DateTime! - deletedAt: DateTime packageVersion: PackageVersion! active: Boolean! commandName: String! @@ -1114,7 +1105,6 @@ type PackageVersionFilesystem { type InterfaceVersion implements Node { """The ID of the object""" id: ID! - deletedAt: DateTime interface: Interface! version: String! content: String! @@ -1127,7 +1117,6 @@ type InterfaceVersion implements Node { type Interface implements Node { """The ID of the object""" id: ID! - deletedAt: DateTime name: String! displayName: String! description: String! @@ -1373,7 +1362,6 @@ type PackageCollaboratorEdge { type PackageCollaborator implements Node { """The ID of the object""" id: ID! - deletedAt: DateTime user: User! role: RegistryPackageMaintainerRoleChoices! package: Package! @@ -1399,8 +1387,6 @@ enum RegistryPackageMaintainerRoleChoices { type PackageCollaboratorInvite implements Node { """The ID of the object""" id: ID! - updatedAt: DateTime! - deletedAt: DateTime requestedBy: User! user: User inviteEmail: String @@ -1458,8 +1444,6 @@ enum GrapheneRole { type PackageTransferRequest implements Node { """The ID of the object""" id: ID! - updatedAt: DateTime! - deletedAt: DateTime requestedBy: User! previousOwnerObjectId: Int! newOwnerObjectId: Int! @@ -1898,8 +1882,6 @@ type APITokenEdge { type APIToken { id: ID! - updatedAt: DateTime! - deletedAt: DateTime user: User! identifier: String createdAt: DateTime! @@ -2016,8 +1998,6 @@ type SocialAuth implements Node { type Signature { id: ID! - updatedAt: DateTime! - deletedAt: DateTime publicKey: PublicKey! data: String! createdAt: DateTime! @@ -2303,6 +2283,10 @@ type Query { getTemplateFrameworks(offset: Int, before: String, after: String, first: Int, last: Int): TemplateFrameworkConnection getTemplateLanguages(offset: Int, before: String, after: String, first: Int, last: Int): TemplateLanguageConnection getAppTemplates(categorySlug: String, frameworkSlug: String, languageSlug: String, sortBy: AppTemplatesSortBy, offset: Int, before: String, after: String, first: Int, last: Int): AppTemplateConnection + getAppSecrets(appId: ID!, names: [String], offset: Int, before: String, after: String, first: Int, last: Int): SecretConnection + getAppSecret(appId: ID!, secretName: String!): Secret + getAppSecretLog(appId: ID!, offset: Int, before: String, after: String, first: Int, last: Int): SecretLogConnection + getSecretValue(id: ID!): String getAppTemplate(slug: String!): AppTemplate getAppTemplateCategories(offset: Int, before: String, after: String, first: Int, last: Int): AppTemplateCategoryConnection viewer: User @@ -2447,6 +2431,78 @@ enum AppTemplatesSortBy { POPULAR } +type SecretConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [SecretEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `Secret` and its cursor.""" +type SecretEdge { + """The item at the end of the edge""" + node: Secret + + """A cursor for use in pagination""" + cursor: String! +} + +type Secret implements Node { + createdAt: DateTime! + updatedAt: DateTime! + name: String! + + """The ID of the object""" + id: ID! +} + +type SecretLogConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [SecretLogEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `SecretLog` and its cursor.""" +type SecretLogEdge { + """The item at the end of the edge""" + node: SecretLog + + """A cursor for use in pagination""" + cursor: String! +} + +type SecretLog implements Node { + createdAt: DateTime! + action: DeploySecretLogActionChoices! + + """The ID of the object""" + id: ID! + secretName: String! +} + +enum DeploySecretLogActionChoices { + """Access""" + ACCESS + + """Modification""" + MODIFICATION + + """Create""" + CREATE + + """DELETE""" + DELETE +} + type AppTemplateCategoryConnection { """Pagination data for this connection.""" pageInfo: PageInfo! @@ -2899,6 +2955,15 @@ type Mutation { deleteDNSRecord(input: DeleteDNSRecordInput!): DeleteDNSRecordPayload upsertDomainFromZoneFile(input: UpsertDomainFromZoneFileInput!): UpsertDomainFromZoneFilePayload deleteDomain(input: DeleteDomainInput!): DeleteDomainPayload + + """Create or update an app secret on an app with given ID""" + upsertAppSecret(input: UpsertAppSecretInput!): UpsertAppSecretPayload + + """Create or update app secrets on an app with given ID""" + upsertAppSecrets(input: UpsertAppSecretsInput!): UpsertAppSecretsPayload + + """Delete secret with given ID""" + deleteAppSecret(input: DeleteAppSecretInput!): DeleteAppSecretPayload tokenAuth(input: ObtainJSONWebTokenInput!): ObtainJSONWebTokenPayload generateDeployToken(input: GenerateDeployTokenInput!): GenerateDeployTokenPayload verifyAccessToken(token: String): Verify @@ -2960,6 +3025,7 @@ type Mutation { removePackageTransferRequest(input: RemovePackageTransferRequestInput!): RemovePackageTransferRequestPayload generateBindingsForAllPackages(input: GenerateBindingsForAllPackagesInput!): GenerateBindingsForAllPackagesPayload makePackagePublic(input: MakePackagePublicInput!): MakePackagePublicPayload + generateUploadUrl(input: GenerateUploadUrlInput!): GenerateUploadUrlPayload } """Viewer accepts the latest ToS.""" @@ -3150,8 +3216,6 @@ type RequestAppTransferPayload { type AppTransferRequest implements Node { """The ID of the object""" id: ID! - updatedAt: DateTime! - deletedAt: DateTime requestedBy: User! previousOwnerObjectId: Int! newOwnerObjectId: Int! @@ -3288,6 +3352,59 @@ input DeleteDomainInput { clientMutationId: String } +"""Create or update an app secret on an app with given ID""" +type UpsertAppSecretPayload { + secret: Secret! + success: Boolean! + clientMutationId: String +} + +input UpsertAppSecretInput { + """ID of the app onto which to add secrets.""" + appId: ID! + + """Name of the secret.""" + name: String! + + """Value of the secret.""" + value: String! + clientMutationId: String +} + +"""Create or update app secrets on an app with given ID""" +type UpsertAppSecretsPayload { + success: Boolean! + secrets: [Secret]! + clientMutationId: String +} + +input UpsertAppSecretsInput { + """ID of the app onto which to add secrets.""" + appId: ID! + secrets: [SecretInput] + clientMutationId: String +} + +input SecretInput { + """Name of the secret.""" + name: String! + + """Value of the secret.""" + value: String! +} + +"""Delete secret with given ID""" +type DeleteAppSecretPayload { + success: Boolean! + clientMutationId: String +} + +input DeleteAppSecretInput { + """ID of the secret to delete.""" + id: ID! + clientMutationId: String +} + type ObtainJSONWebTokenPayload { payload: GenericScalar! refreshExpiresIn: Int! @@ -4080,6 +4197,19 @@ input MakePackagePublicInput { clientMutationId: String } +type GenerateUploadUrlPayload { + signedUrl: SignedUrl! + clientMutationId: String +} + +input GenerateUploadUrlInput { + name: String + version: String = "latest" + filename: String + expiresAfterSeconds: Int = 60 + clientMutationId: String +} + type Subscription { streamLogs( appVersionId: ID! diff --git a/lib/backend-api/src/query.rs b/lib/backend-api/src/query.rs index ba7f9189c7f..40ae033339d 100644 --- a/lib/backend-api/src/query.rs +++ b/lib/backend-api/src/query.rs @@ -10,19 +10,198 @@ use url::Url; use wasmer_config::package::PackageIdent; use crate::{ - types::{ - self, CreateNamespaceVars, DeployApp, DeployAppConnection, DeployAppVersion, - DeployAppVersionConnection, DnsDomain, GetAppTemplateFromSlugVariables, - GetAppTemplatesFromFrameworkVars, GetAppTemplatesFromLanguageVars, GetAppTemplatesVars, - GetCurrentUserWithAppsVars, GetDeployAppAndVersion, GetDeployAppVersionsVars, - GetNamespaceAppsVars, GetSignedUrlForPackageUploadVariables, GetTemplateFrameworksVars, - GetTemplateLanguagesVars, Log, LogStream, PackageVersionConnection, PublishDeployAppVars, - PushPackageReleasePayload, SignedUrl, TagPackageReleasePayload, - UpsertDomainFromZoneFileVars, - }, + types::{self, *}, GraphQLApiFailure, WasmerClient, }; +pub async fn get_app_secret_value_by_id( + client: &WasmerClient, + secret_id: impl Into, +) -> Result, anyhow::Error> { + client + .run_graphql_strict(types::GetAppSecretValue::build( + GetAppSecretValueVariables { + id: types::Id::from(secret_id), + }, + )) + .await + .map(|v| v.get_secret_value) +} + +pub async fn get_app_secret_by_name( + client: &WasmerClient, + app_id: impl Into, + name: impl Into, +) -> Result, anyhow::Error> { + client + .run_graphql_strict(types::GetAppSecret::build(GetAppSecretVariables { + app_id: types::Id::from(app_id), + secret_name: name.into(), + })) + .await + .map(|v| v.get_app_secret) +} + +/// Update or create an app secret. +pub async fn upsert_app_secret( + client: &WasmerClient, + app_id: impl Into, + name: impl Into, + value: impl Into, +) -> Result, anyhow::Error> { + client + .run_graphql_strict(types::UpsertAppSecret::build(UpsertAppSecretVariables { + app_id: cynic::Id::from(app_id.into()), + name: name.into().as_str(), + value: value.into().as_str(), + })) + .await + .map(|v| v.upsert_app_secret) +} + +/// Update or create app secrets in bulk. +pub async fn upsert_app_secrets( + client: &WasmerClient, + app_id: impl Into, + secrets: impl IntoIterator, impl Into)>, +) -> Result, anyhow::Error> { + client + .run_graphql_strict(types::UpsertAppSecrets::build(UpsertAppSecretsVariables { + app_id: cynic::Id::from(app_id.into()), + secrets: Some( + secrets + .into_iter() + .map(|(name, value)| SecretInput { + name: name.into(), + value: value.into(), + }) + .collect(), + ), + })) + .await + .map(|v| v.upsert_app_secrets) +} + +/// Load all secrets of an app. +/// +/// Will paginate through all versions and return them in a single list. +pub async fn get_all_app_secrets_filtered( + client: &WasmerClient, + app_id: impl Into, + names: impl IntoIterator>, +) -> Result, anyhow::Error> { + let mut vars = GetAllAppSecretsVariables { + after: None, + app_id: types::Id::from(app_id), + before: None, + first: None, + last: None, + offset: None, + names: Some(names.into_iter().map(|s| s.into()).collect()), + }; + + let mut all_versions = Vec::::new(); + + loop { + let page = get_app_secrets(client, vars.clone()).await?; + if page.edges.is_empty() { + break; + } + + for edge in page.edges { + let edge = match edge { + Some(edge) => edge, + None => continue, + }; + let version = match edge.node { + Some(item) => item, + None => continue, + }; + + // Sanity check to avoid duplication. + if all_versions.iter().any(|v| v.id == version.id) == false { + all_versions.push(version); + } + + // Update pagination. + vars.after = Some(edge.cursor); + } + } + + Ok(all_versions) +} + +/// Load all secrets of an app. +/// +/// Will paginate through all versions and return them in a single list. +pub async fn get_all_app_secrets( + client: &WasmerClient, + app_id: impl Into, +) -> Result, anyhow::Error> { + let mut vars = GetAllAppSecretsVariables { + after: None, + app_id: types::Id::from(app_id), + before: None, + first: None, + last: None, + offset: None, + names: None, + }; + + let mut all_versions = Vec::::new(); + + loop { + let page = get_app_secrets(client, vars.clone()).await?; + if page.edges.is_empty() { + break; + } + + for edge in page.edges { + let edge = match edge { + Some(edge) => edge, + None => continue, + }; + let version = match edge.node { + Some(item) => item, + None => continue, + }; + + // Sanity check to avoid duplication. + if all_versions.iter().any(|v| v.id == version.id) == false { + all_versions.push(version); + } + + // Update pagination. + vars.after = Some(edge.cursor); + } + } + + Ok(all_versions) +} + +/// Retrieve secrets for an app. +pub async fn get_app_secrets( + client: &WasmerClient, + vars: GetAllAppSecretsVariables, +) -> Result { + let res = client + .run_graphql_strict(types::GetAllAppSecrets::build(vars)) + .await?; + res.get_app_secrets.context("app not found") +} + +pub async fn delete_app_secret( + client: &WasmerClient, + secret_id: impl Into, +) -> Result, anyhow::Error> { + client + .run_graphql_strict(types::DeleteAppSecret::build(DeleteAppSecretVariables { + id: types::Id::from(secret_id.into()), + })) + .await + .map(|v| v.delete_app_secret) +} + /// Load a webc package from the registry. /// /// NOTE: this uses the public URL instead of the download URL available through diff --git a/lib/backend-api/src/types.rs b/lib/backend-api/src/types.rs index 4646dff33a0..d2b9919406a 100644 --- a/lib/backend-api/src/types.rs +++ b/lib/backend-api/src/types.rs @@ -1186,6 +1186,131 @@ mod queries { pub version: Option, } + #[derive(cynic::QueryVariables, Debug)] + pub struct DeleteAppSecretVariables { + pub id: cynic::Id, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Mutation", variables = "DeleteAppSecretVariables")] + pub struct DeleteAppSecret { + #[arguments(input: { id: $id })] + pub delete_app_secret: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + pub struct DeleteAppSecretPayload { + pub success: bool, + } + #[derive(cynic::QueryVariables, Debug, Clone)] + pub struct GetAllAppSecretsVariables { + pub after: Option, + pub app_id: cynic::Id, + pub before: Option, + pub first: Option, + pub last: Option, + pub offset: Option, + pub names: Option>, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Query", variables = "GetAllAppSecretsVariables")] + pub struct GetAllAppSecrets { + #[arguments(appId: $app_id, after: $after, before: $before, first: $first, last: $last, offset: $offset, names: $names)] + pub get_app_secrets: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + pub struct SecretConnection { + pub edges: Vec>, + pub page_info: PageInfo, + pub total_count: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + pub struct SecretEdge { + pub cursor: String, + pub node: Option, + } + + #[derive(cynic::QueryVariables, Debug)] + pub struct GetAppSecretVariables { + pub app_id: cynic::Id, + pub secret_name: String, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Query", variables = "GetAppSecretVariables")] + pub struct GetAppSecret { + #[arguments(appId: $app_id, secretName: $secret_name)] + pub get_app_secret: Option, + } + + #[derive(cynic::QueryVariables, Debug)] + pub struct GetAppSecretValueVariables { + pub id: cynic::Id, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Query", variables = "GetAppSecretValueVariables")] + pub struct GetAppSecretValue { + #[arguments(id: $id)] + pub get_secret_value: Option, + } + + #[derive(cynic::QueryVariables, Debug)] + pub struct UpsertAppSecretVariables<'a> { + pub app_id: cynic::Id, + pub name: &'a str, + pub value: &'a str, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Mutation", variables = "UpsertAppSecretVariables")] + pub struct UpsertAppSecret { + #[arguments(input: { appId: $app_id, name: $name, value: $value })] + pub upsert_app_secret: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + pub struct UpsertAppSecretPayload { + pub secret: Secret, + pub success: bool, + } + + #[derive(cynic::QueryVariables, Debug)] + pub struct UpsertAppSecretsVariables { + pub app_id: cynic::Id, + pub secrets: Option>, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Mutation", variables = "UpsertAppSecretsVariables")] + pub struct UpsertAppSecrets { + #[arguments(input: { appId: $app_id, secrets: $secrets })] + pub upsert_app_secrets: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + pub struct UpsertAppSecretsPayload { + pub secrets: Vec>, + pub success: bool, + } + + #[derive(cynic::InputObject, Debug)] + pub struct SecretInput { + pub name: String, + pub value: String, + } + #[derive(cynic::QueryFragment, Debug, Serialize)] + pub struct Secret { + #[serde(skip_serializing)] + pub id: cynic::Id, + pub name: String, + pub created_at: DateTime, + pub updated_at: DateTime, + } + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "TXTRecord")] pub struct TxtRecord { diff --git a/lib/cli/Cargo.toml b/lib/cli/Cargo.toml index 291fcd783ee..568b487f682 100644 --- a/lib/cli/Cargo.toml +++ b/lib/cli/Cargo.toml @@ -231,6 +231,7 @@ clap_complete = "4.5.2" clap_mangen = "0.2.20" zip = { version = "2.1.3", default-features = false, features = ["deflate"] } console = "0.15.8" +dotenvy = "0.15.7" # NOTE: Must use different features for clap because the "color" feature does not # work on wasi due to the anstream dependency not compiling. diff --git a/lib/cli/src/commands/app/mod.rs b/lib/cli/src/commands/app/mod.rs index 35c8336b4e2..3da02d42eb8 100644 --- a/lib/cli/src/commands/app/mod.rs +++ b/lib/cli/src/commands/app/mod.rs @@ -8,6 +8,7 @@ pub mod info; pub mod list; pub mod logs; pub mod purge_cache; +pub mod secrets; pub mod version; mod util; @@ -27,6 +28,8 @@ pub enum CmdApp { Delete(delete::CmdAppDelete), #[clap(subcommand)] Version(version::CmdAppVersion), + #[clap(subcommand, alias = "secrets")] + Secret(secrets::CmdAppSecrets), } #[async_trait::async_trait] @@ -53,6 +56,7 @@ impl AsyncCliCommand for CmdApp { Self::Version(cmd) => cmd.run_async().await, Self::Deploy(cmd) => cmd.run_async().await, Self::PurgeCache(cmd) => cmd.run_async().await, + Self::Secret(cmd) => cmd.run_async().await, } } } diff --git a/lib/cli/src/commands/app/secrets/create.rs b/lib/cli/src/commands/app/secrets/create.rs new file mode 100644 index 00000000000..214a58b78c0 --- /dev/null +++ b/lib/cli/src/commands/app/secrets/create.rs @@ -0,0 +1,225 @@ +use super::utils::Secret; +use crate::{ + commands::{ + app::util::{get_app_id_from_config, prompt_app_ident, AppIdent}, + AsyncCliCommand, + }, + opts::{ApiOpts, WasmerEnv}, +}; +use anyhow::Context; +use colored::Colorize; +use dialoguer::theme::ColorfulTheme; +use is_terminal::IsTerminal; +use std::{ + collections::{HashMap, HashSet}, + env::current_dir, + path::{Path, PathBuf}, +}; +use wasmer_api::WasmerClient; + +/// Create a new secret related to an Edge app. +#[derive(clap::Parser, Debug)] +pub struct CmdAppSecretsCreate { + /// The identifier of the app the secret is related to. + pub app_id: Option, + + /// The path to the directory where the config file for the application will be written to. + #[clap(long = "app-dir", conflicts_with = "app-id")] + pub app_dir_path: Option, + + /// The name of the secret to create. + #[clap(name = "name")] + pub secret_name: Option, + + /// The value of the secret to create. + #[clap(name = "value")] + pub secret_value: Option, + + /// Path to a file with secrets stored in JSON format to create secrets from. + #[clap( + long, + name = "from-file", + conflicts_with = "secret_name", + conflicts_with = "all" + )] + pub from_file: Option, + + /* --- Common args --- */ + #[clap(flatten)] + #[allow(missing_docs)] + pub api: ApiOpts, + + #[clap(flatten)] + pub env: WasmerEnv, + + /// Don't print any message. + #[clap(long)] + pub quiet: bool, + + /// Do not prompt for user input. + #[clap(long, default_value_t = !std::io::stdin().is_terminal())] + pub non_interactive: bool, +} + +impl CmdAppSecretsCreate { + fn get_secret_name(&self) -> anyhow::Result { + if let Some(name) = &self.secret_name { + return Ok(name.clone()); + } + + if self.non_interactive { + anyhow::bail!("No secret name given. Use the `--name` flag to specify one.") + } else { + let theme = ColorfulTheme::default(); + Ok(dialoguer::Input::with_theme(&theme) + .with_prompt("Enter the name of the secret") + .interact_text()?) + } + } + + fn get_secret_value(&self) -> anyhow::Result { + if let Some(value) = &self.secret_value { + return Ok(value.clone()); + } + + if self.non_interactive { + anyhow::bail!("No secret value given. Use the `--value` flag to specify one.") + } else { + let theme = ColorfulTheme::default(); + Ok(dialoguer::Input::with_theme(&theme) + .with_prompt("Enter the value of the secret") + .interact_text()?) + } + } + + async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { + if let Some(app_id) = &self.app_id { + let app = app_id.resolve(client).await?; + return Ok(app.id.into_inner()); + } + + let app_dir_path = if let Some(app_dir_path) = &self.app_dir_path { + app_dir_path.clone() + } else { + current_dir()? + }; + + if let Ok(Some(app_id)) = get_app_id_from_config(&app_dir_path).await { + return Ok(app_id.clone()); + } + + if self.non_interactive { + anyhow::bail!("No app id given. Use the `--app_id` flag to specify one.") + } else { + let id = prompt_app_ident("Enter the name of the app")?; + let app = id.resolve(client).await?; + return Ok(app.id.into_inner()); + } + } + + /// Given a list of secrets, checks if the given secrets already exist for the given app and + /// returns a list of secrets that must be upserted. + async fn filter_secrets( + &self, + client: &WasmerClient, + app_id: &str, + secrets: Vec, + ) -> anyhow::Result> { + let names = secrets.iter().map(|s| &s.name); + let app_secrets = + wasmer_api::query::get_all_app_secrets_filtered(client, app_id, names).await?; + let mut sset = HashSet::::from_iter(app_secrets.iter().map(|s| s.name.clone())); + let mut ret = HashMap::new(); + + for secret in secrets { + if sset.contains(&secret.name) { + if self.non_interactive { + anyhow::bail!("Cannot create secret '{}' in app {app_id} as it already exists. Use the `update` command instead.", secret.name.bold()); + } else { + eprintln!( + "Secret '{}' already exists for the selected app.", + secret.name.bold() + ); + let theme = ColorfulTheme::default(); + let res = dialoguer::Confirm::with_theme(&theme) + .with_prompt("Do you want to update it?") + .interact()?; + + if !res { + eprintln!("Cannot create secret '{}' in app {app_id} as it already exists. Use the `update` command instead.", secret.name.bold()); + } + } + } + + sset.insert(secret.name.clone()); + ret.insert(secret.name, secret.value); + } + + Ok(ret + .into_iter() + .map(|(name, value)| Secret { name, value }) + .collect()) + } + + async fn create( + &self, + client: &WasmerClient, + app_id: &str, + secrets: Vec, + ) -> Result<(), anyhow::Error> { + let res = wasmer_api::query::upsert_app_secrets( + client, + app_id, + secrets.iter().map(|s| (s.name.as_str(), s.value.as_str())), + ) + .await?; + let res = res.context( + "Backend did not return any payload to confirm the successful creation of the secret!", + )?; + + if !res.success { + anyhow::bail!("Secret creation failed!") + } else { + if !self.quiet { + eprintln!("Succesfully created secret(s):"); + for secret in secrets { + eprintln!("{}", secret.name.bold()); + } + } + + Ok(()) + } + } + + async fn create_from_file( + &self, + path: &Path, + app_id: &str, + ) -> anyhow::Result<(), anyhow::Error> { + let secrets = super::utils::read_secrets_from_file(path).await?; + let client = self.api.client()?; + + let secrets = self.filter_secrets(&client, app_id, secrets).await?; + self.create(&client, app_id, secrets).await?; + + Ok(()) + } +} + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdAppSecretsCreate { + type Output = (); + + async fn run_async(self) -> Result { + let client = self.api.client()?; + let app_id = self.get_app_id(&client).await?; + if let Some(file) = &self.from_file { + self.create_from_file(file, &app_id).await + } else { + let name = self.get_secret_name()?; + let value = self.get_secret_value()?; + let secret = Secret { name, value }; + self.create(&client, &app_id, vec![secret]).await + } + } +} diff --git a/lib/cli/src/commands/app/secrets/delete.rs b/lib/cli/src/commands/app/secrets/delete.rs new file mode 100644 index 00000000000..c9c6fe868dc --- /dev/null +++ b/lib/cli/src/commands/app/secrets/delete.rs @@ -0,0 +1,182 @@ +use crate::{ + commands::{app::util::{get_app_id_from_config, AppIdent, prompt_app_ident}, AsyncCliCommand}, + opts::{ApiOpts, WasmerEnv}, +}; +use colored::Colorize; +use dialoguer::theme::ColorfulTheme; +use is_terminal::IsTerminal; +use std::{ + env::current_dir, + path::{Path, PathBuf}, +}; +use wasmer_api::WasmerClient; + +use super::utils::{self, get_secrets}; + +/// Delete an existing secret related to an Edge app. +#[derive(clap::Parser, Debug)] +pub struct CmdAppSecretsDelete { + /// The id of the app the secret is related to. + pub app_id: Option, + + /// The path to the directory where the config file for the application will be written to. + #[clap(long = "app-dir", conflicts_with = "app-id")] + pub app_dir_path: Option, + + /// The name of the secret to delete. + #[clap(name = "name")] + pub secret_name: Option, + + /// Path to a file with secrets stored in JSON format to delete secrets from. + #[clap( + long, + name = "from-file", + conflicts_with = "secret_value", + conflicts_with = "secret_name" + )] + pub from_file: Option, + + /// Delete all the secrets related to an app. + #[clap(long, conflicts_with = "name")] + pub all: bool, + + /// Delete the secret(s) without asking for confirmation. + #[clap(long)] + pub force: bool, + + /* --- Common args --- */ + #[clap(flatten)] + #[allow(missing_docs)] + pub api: ApiOpts, + + #[clap(flatten)] + pub env: WasmerEnv, + + /// Don't print any message. + #[clap(long)] + pub quiet: bool, + + /// Do not prompt for user input. + #[clap(long, default_value_t = !std::io::stdin().is_terminal())] + pub non_interactive: bool, +} + +impl CmdAppSecretsDelete { + fn get_secret_name(&self) -> anyhow::Result { + if let Some(name) = &self.secret_name { + return Ok(name.clone()); + } + + if self.non_interactive { + anyhow::bail!("No secret name given. Use the `--name` flag to specify one.") + } else { + let theme = ColorfulTheme::default(); + Ok(dialoguer::Input::with_theme(&theme) + .with_prompt("Enter the name of the secret") + .interact_text()?) + } + } + + async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { + if let Some(app_id) = &self.app_id { + let app = app_id.resolve(client).await?; + return Ok(app.id.into_inner()); + } + + let app_dir_path = if let Some(app_dir_path) = &self.app_dir_path { + app_dir_path.clone() + } else { + current_dir()? + }; + + if let Ok(Some(app_id)) = get_app_id_from_config(&app_dir_path).await { + return Ok(app_id.clone()); + } + + if self.non_interactive { + anyhow::bail!("No app id given. Use the `--app_id` flag to specify one.") + } else { + let id = prompt_app_ident("Enter the name of the app")?; + let app = id.resolve(client).await?; + return Ok(app.id.into_inner()); + } + } + + async fn delete( + &self, + client: &WasmerClient, + app_id: &str, + secret_name: &str, + ) -> anyhow::Result<()> { + let secret = utils::get_secret_by_name(client, app_id, secret_name).await?; + + if let Some(secret) = secret { + if !self.non_interactive && !self.force { + let theme = ColorfulTheme::default(); + let res = dialoguer::Confirm::with_theme(&theme) + .with_prompt(format!("Delete secret '{}'?", secret_name.bold())) + .interact()?; + if !res { + return Ok(()); + } + } + + let res = wasmer_api::query::delete_app_secret(client, secret.id.into_inner()).await?; + + match res { + Some(res) if !res.success => { + anyhow::bail!("Error deleting secret '{}'", secret.name.bold()) + } + Some(_) => { + if !self.quiet { + eprintln!("Correctly deleted secret '{}'", secret.name.bold()); + } + Ok(()) + } + None => { + anyhow::bail!("Error deleting secret '{}'", secret.name.bold()) + } + } + } else { + if !self.quiet { + eprintln!("No secret with name '{}' found", secret_name.bold()); + } + Ok(()) + } + } + + async fn delete_from_file(&self, path: &Path, app_id: String) -> anyhow::Result<()> { + let secrets = super::utils::read_secrets_from_file(path).await?; + let client = self.api.client()?; + + for secret in secrets { + self.delete(&client, &app_id, &secret.name).await?; + } + + Ok(()) + } +} + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdAppSecretsDelete { + type Output = (); + + async fn run_async(self) -> Result { + let client = self.api.client()?; + let app_id = self.get_app_id(&client).await?; + + if let Some(file) = &self.from_file { + self.delete_from_file(file, app_id).await + } else if self.all { + let secrets = get_secrets(&client, &app_id).await?; + for secret in secrets { + self.delete(&client, &app_id, &secret.name).await?; + } + + Ok(()) + } else { + let name = self.get_secret_name()?; + self.delete(&client, &app_id, &name).await + } + } +} diff --git a/lib/cli/src/commands/app/secrets/list.rs b/lib/cli/src/commands/app/secrets/list.rs new file mode 100644 index 00000000000..baef9424a1f --- /dev/null +++ b/lib/cli/src/commands/app/secrets/list.rs @@ -0,0 +1,87 @@ +use super::utils::{get_secrets, BackendSecretWrapper}; +use crate::{ + commands::{ + app::util::{get_app_id_from_config, AppIdent, prompt_app_ident}, + AsyncCliCommand, + }, + opts::{ApiOpts, ListFormatOpts, WasmerEnv}, +}; +use is_terminal::IsTerminal; +use wasmer_api::WasmerClient; +use std::{env::current_dir, path::PathBuf}; + +/// Retrieve the value of an existing secret related to an Edge app. +#[derive(clap::Parser, Debug)] +pub struct CmdAppSecretsList { + /// The identifier of the app to list secrets of. + pub app_id: Option, + + /// The path to the directory where the config file for the application will be written to. + #[clap(long = "app-dir", conflicts_with = "app_id")] + pub app_dir_path: Option, + + /* --- Common args --- */ + #[clap(flatten)] + #[allow(missing_docs)] + pub api: ApiOpts, + + #[clap(flatten)] + pub env: WasmerEnv, + + /// Don't print any message. + #[clap(long)] + pub quiet: bool, + + /// Do not prompt for user input. + #[clap(long, default_value_t = !std::io::stdin().is_terminal())] + pub non_interactive: bool, + + #[clap(flatten)] + pub fmt: ListFormatOpts, +} + +impl CmdAppSecretsList { + async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { + if let Some(app_id) = &self.app_id { + let app = app_id.resolve(client).await?; + return Ok(app.id.into_inner()); + } + + let app_dir_path = if let Some(app_dir_path) = &self.app_dir_path { + app_dir_path.clone() + } else { + current_dir()? + }; + + if let Ok(Some(app_id)) = get_app_id_from_config(&app_dir_path).await { + return Ok(app_id.clone()); + } + + if self.non_interactive { + anyhow::bail!("No app id given. Use the `--app_id` flag to specify one.") + } else { + let id = prompt_app_ident("Enter the name of the app")?; + let app = id.resolve(client).await?; + return Ok(app.id.into_inner()); + } + } +} + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdAppSecretsList { + type Output = (); + + async fn run_async(self) -> Result { + let client = self.api.client()?; + let app_id = self.get_app_id(&client).await?; + let secrets: Vec = get_secrets(&client, &app_id) + .await? + .into_iter() + .map(|s| s.into()) + .collect(); + + println!("{}", self.fmt.format.render(secrets.as_slice())); + + Ok(()) + } +} diff --git a/lib/cli/src/commands/app/secrets/mod.rs b/lib/cli/src/commands/app/secrets/mod.rs new file mode 100644 index 00000000000..1328a2f5e69 --- /dev/null +++ b/lib/cli/src/commands/app/secrets/mod.rs @@ -0,0 +1,49 @@ +use crate::commands::AsyncCliCommand; + +pub mod create; +pub mod delete; +pub mod reveal; +pub mod list; +pub mod update; +mod utils; + +/// Manage and reveal secrets related to Edge apps. +#[derive(Debug, clap::Parser)] +pub enum CmdAppSecrets { + Create(create::CmdAppSecretsCreate), + Delete(delete::CmdAppSecretsDelete), + Reveal(reveal::CmdAppSecretsReveal), + List(list::CmdAppSecretsList), + Update(update::CmdAppSecretsUpdate), +} + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdAppSecrets { + type Output = (); + + async fn run_async(self) -> Result { + match self { + CmdAppSecrets::Create(c) => { + c.run_async().await?; + Ok(()) + } + CmdAppSecrets::Delete(c) => { + c.run_async().await?; + Ok(()) + } + CmdAppSecrets::Reveal(c) => { + c.run_async().await?; + Ok(()) + } + CmdAppSecrets::List(c) => { + c.run_async().await?; + Ok(()) + } + + CmdAppSecrets::Update(c) => { + c.run_async().await?; + Ok(()) + } + } + } +} diff --git a/lib/cli/src/commands/app/secrets/reveal.rs b/lib/cli/src/commands/app/secrets/reveal.rs new file mode 100644 index 00000000000..15db496151f --- /dev/null +++ b/lib/cli/src/commands/app/secrets/reveal.rs @@ -0,0 +1,141 @@ +use super::utils; +use crate::{ + commands::{ + app::util::{get_app_id_from_config, AppIdent, prompt_app_ident}, + AsyncCliCommand, + }, + opts::{ApiOpts, ListFormatOpts, WasmerEnv}, + utils::render::{ItemFormat, ListFormat}, +}; +use dialoguer::theme::ColorfulTheme; +use is_terminal::IsTerminal; +use wasmer_api::WasmerClient; +use std::{env::current_dir, path::PathBuf}; + +/// Reveal the value of an existing secret related to an Edge app. +#[derive(clap::Parser, Debug)] +pub struct CmdAppSecretsReveal { + /// The name of the secret to get the value of. + #[clap(name = "name")] + pub secret_name: Option, + + /// The id of the app the secret is related to. + pub app_id: Option, + + /// The path to the directory where the config file for the application will be written to. + #[clap(long = "app-dir", conflicts_with = "app_id")] + pub app_dir_path: Option, + + /// Reveal all the secrets related to an app. + #[clap(long, conflicts_with = "name")] + pub all: bool, + + /* --- Common args --- */ + #[clap(flatten)] + #[allow(missing_docs)] + pub api: ApiOpts, + + #[clap(flatten)] + pub env: WasmerEnv, + + /// Don't print any message. + #[clap(long)] + pub quiet: bool, + + /// Do not prompt for user input. + #[clap(long, default_value_t = !std::io::stdin().is_terminal())] + pub non_interactive: bool, + + #[clap(flatten)] + pub fmt: Option, +} + +impl CmdAppSecretsReveal { + async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { + if let Some(app_id) = &self.app_id { + let app = app_id.resolve(client).await?; + return Ok(app.id.into_inner()); + } + + let app_dir_path = if let Some(app_dir_path) = &self.app_dir_path { + app_dir_path.clone() + } else { + current_dir()? + }; + + if let Ok(Some(app_id)) = get_app_id_from_config(&app_dir_path).await { + return Ok(app_id.clone()); + } + + if self.non_interactive { + anyhow::bail!("No app id given. Use the `--app_id` flag to specify one.") + } else { + let id = prompt_app_ident("Enter the name of the app")?; + let app = id.resolve(client).await?; + return Ok(app.id.into_inner()); + } + } + + fn get_secret_name(&self) -> anyhow::Result { + if let Some(name) = &self.secret_name { + return Ok(name.clone()); + } + + if self.non_interactive { + anyhow::bail!("No secret name given. Use the `--name` flag to specify one.") + } else { + let theme = ColorfulTheme::default(); + Ok(dialoguer::Input::with_theme(&theme) + .with_prompt("Enter the name of the secret:") + .interact_text()?) + } + } +} + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdAppSecretsReveal { + type Output = (); + + async fn run_async(self) -> Result { + let client = self.api.client()?; + let app_id = self.get_app_id(&client).await?; + + if !self.all { + let name = self.get_secret_name()?; + + let value = utils::get_secret_value_by_name(&client, &app_id, &name).await?; + + let secret = utils::Secret { name, value }; + + if let Some(fmt) = &self.fmt { + let fmt = match fmt.format { + ListFormat::Json => ItemFormat::Json, + ListFormat::Yaml => ItemFormat::Yaml, + ListFormat::Table => ItemFormat::Table, + ListFormat::ItemTable => { + anyhow::bail!("The 'item-table' format is not available for single values.") + } + }; + println!("{}", fmt.render(&secret)); + } else { + print!("{}", secret.value); + } + } else { + let secrets: Vec = utils::reveal_secrets(&client, &app_id).await?; + + if let Some(fmt) = &self.fmt { + println!("{}", fmt.format.render(secrets.as_slice())); + } else { + for secret in secrets { + println!( + "{}=\"{}\"", + secret.name, + utils::render::sanitize_value(&secret.value) + ); + } + } + } + + Ok(()) + } +} diff --git a/lib/cli/src/commands/app/secrets/update.rs b/lib/cli/src/commands/app/secrets/update.rs new file mode 100644 index 00000000000..38fc386e6c2 --- /dev/null +++ b/lib/cli/src/commands/app/secrets/update.rs @@ -0,0 +1,221 @@ +use super::utils::Secret; +use crate::{ + commands::{ + app::util::{get_app_id_from_config, AppIdent, prompt_app_ident}, + AsyncCliCommand, + }, + opts::{ApiOpts, WasmerEnv}, +}; +use anyhow::Context; +use colored::Colorize; +use dialoguer::theme::ColorfulTheme; +use is_terminal::IsTerminal; +use std::{ + collections::HashSet, + env::current_dir, + path::{Path, PathBuf}, +}; +use wasmer_api::WasmerClient; + +/// Update an existing secret related to an Edge app. +#[derive(clap::Parser, Debug)] +pub struct CmdAppSecretsUpdate { + /// The id of the app the secret is related to. + #[clap(long = "app-id")] + pub app_id: Option, + + /// The path to the directory where the config file for the application will be written to. + #[clap(long = "app-dir", conflicts_with = "app-id")] + pub app_dir_path: Option, + + /// The name of the secret to update. + #[clap(name = "name")] + pub secret_name: Option, + + /// The value of the secret to update. + #[clap(name = "value")] + pub secret_value: Option, + + /// Path to a file with secrets stored in JSON format to update secrets from. + #[clap( + long, + name = "from-file", + conflicts_with = "secret_value", + conflicts_with = "secret_name" + )] + pub from_file: Option, + + /* --- Common args --- */ + #[clap(flatten)] + #[allow(missing_docs)] + pub api: ApiOpts, + + #[clap(flatten)] + pub env: WasmerEnv, + + /// Don't print any message. + #[clap(long)] + pub quiet: bool, + + /// Do not prompt for user input. + #[clap(long, default_value_t = !std::io::stdin().is_terminal())] + pub non_interactive: bool, +} + +impl CmdAppSecretsUpdate { + fn get_secret_name(&self) -> anyhow::Result { + if let Some(name) = &self.secret_name { + return Ok(name.clone()); + } + + if self.non_interactive { + anyhow::bail!("No secret name given. Use the `--name` flag to specify one.") + } else { + let theme = ColorfulTheme::default(); + Ok(dialoguer::Input::with_theme(&theme) + .with_prompt("Enter the name of the secret:") + .interact_text()?) + } + } + + fn get_secret_value(&self) -> anyhow::Result { + if let Some(value) = &self.secret_value { + return Ok(value.clone()); + } + + if self.non_interactive { + anyhow::bail!("No secret value given. Use the `--value` flag to specify one.") + } else { + let theme = ColorfulTheme::default(); + Ok(dialoguer::Input::with_theme(&theme) + .with_prompt("Enter the value of the secret:") + .interact_text()?) + } + } + + async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { + if let Some(app_id) = &self.app_id { + let app = app_id.resolve(client).await?; + return Ok(app.id.into_inner()); + } + + let app_dir_path = if let Some(app_dir_path) = &self.app_dir_path { + app_dir_path.clone() + } else { + current_dir()? + }; + + if let Ok(Some(app_id)) = get_app_id_from_config(&app_dir_path).await { + return Ok(app_id.clone()); + } + + if self.non_interactive { + anyhow::bail!("No app id given. Use the `--app_id` flag to specify one.") + } else { + let id = prompt_app_ident("Enter the name of the app")?; + let app = id.resolve(client).await?; + return Ok(app.id.into_inner()); + } + } + + /// Given a list of secrets, checks if the given secrets already exist for the given app and + /// returns a list of secrets that must be upserted. + async fn filter_secrets( + &self, + client: &WasmerClient, + app_id: &str, + secrets: Vec, + ) -> anyhow::Result> { + let names = secrets.iter().map(|s| &s.name); + let app_secrets = + wasmer_api::query::get_all_app_secrets_filtered(client, app_id, names).await?; + let sset = HashSet::<&str>::from_iter(app_secrets.iter().map(|s| s.name.as_str())); + + let mut ret = vec![]; + + for secret in secrets { + if !sset.contains(secret.name.as_str()) { + if self.non_interactive { + anyhow::bail!("Cannot update secret '{}' in app {app_id} as it does not exist yet. Use the `create` command instead.", secret.name.bold()); + } else { + eprintln!( + "Secret '{}' does not exist for the selected app.", + secret.name.bold() + ); + let theme = ColorfulTheme::default(); + let res = dialoguer::Confirm::with_theme(&theme) + .with_prompt("Do you want to create it?") + .interact()?; + + if !res { + eprintln!("Cannot update secret '{}' as it does not exist yet. Use the `create` command instead.", secret.name.bold()); + } + } + } + + ret.push(secret); + } + + Ok(ret) + } + async fn update( + &self, + client: &WasmerClient, + app_id: &str, + secrets: Vec, + ) -> Result<(), anyhow::Error> { + let res = wasmer_api::query::upsert_app_secrets( + client, + app_id, + secrets.iter().map(|s| (s.name.as_str(), s.value.as_str())), + ) + .await?; + let res = res.context( + "Backend did not return any payload to confirm the successful update of the secret!", + )?; + + if !res.success { + anyhow::bail!("Secret creation failed!") + } else { + if !self.quiet { + for secret in secrets { + eprintln!("Succesfully updated secret '{}'", secret.name.bold()); + } + } + + Ok(()) + } + } + + async fn update_from_file( + &self, + path: &Path, + app_id: &str, + ) -> anyhow::Result<(), anyhow::Error> { + let secrets = super::utils::read_secrets_from_file(path).await?; + let client = self.api.client()?; + + let secrets = self.filter_secrets(&client, app_id, secrets).await?; + self.update(&client, app_id, secrets).await?; + + Ok(()) + } +} + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdAppSecretsUpdate { + type Output = (); + + async fn run_async(self) -> Result { + let client = self.api.client()?; + let app_id = self.get_app_id(&client).await?; + if let Some(file) = &self.from_file { + self.update_from_file(file, &app_id).await + } else { + let name = self.get_secret_name()?; + let value = self.get_secret_value()?; + let secret = Secret { name, value }; + self.update(&client, &app_id, vec![secret]).await + } + } +} diff --git a/lib/cli/src/commands/app/secrets/utils/mod.rs b/lib/cli/src/commands/app/secrets/utils/mod.rs new file mode 100644 index 00000000000..9a7440f935c --- /dev/null +++ b/lib/cli/src/commands/app/secrets/utils/mod.rs @@ -0,0 +1,84 @@ +pub(crate) mod render; + +use colored::Colorize; +use std::path::Path; +use wasmer_api::{types::Secret as BackendSecret, WasmerClient}; + +#[derive(serde::Serialize, serde::Deserialize)] +pub(super) struct Secret { + pub name: String, + pub value: String, +} + +pub(super) async fn read_secrets_from_file(path: &Path) -> anyhow::Result> { + let mut ret = vec![]; + for item in dotenvy::from_path_iter(path)? { + let (name, value) = item?; + ret.push(Secret { name, value }) + } + Ok(ret) +} + +pub(super) async fn get_secret_by_name( + client: &WasmerClient, + app_id: &str, + secret_name: &str, +) -> anyhow::Result> { + Ok(wasmer_api::query::get_app_secret_by_name(client, app_id, secret_name).await?) +} +pub(crate) async fn get_secrets( + client: &WasmerClient, + app_id: &str, +) -> anyhow::Result> { + wasmer_api::query::get_all_app_secrets(client, app_id).await +} + +pub(crate) async fn get_secret_value( + client: &WasmerClient, + secret: &wasmer_api::types::Secret, +) -> anyhow::Result { + wasmer_api::query::get_app_secret_value_by_id(client, secret.id.clone().into_inner()) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "No value found for secret with name '{}'", + secret.name.bold() + ) + }) +} + +pub(crate) async fn get_secret_value_by_name( + client: &WasmerClient, + app_id: &str, + secret_name: &str, +) -> anyhow::Result { + match get_secret_by_name(client, app_id, secret_name).await? { + Some(secret) => get_secret_value(client, &secret).await, + None => anyhow::bail!("No secret found with name {secret_name} for app {app_id}"), + } +} + +pub(crate) async fn reveal_secrets( + client: &WasmerClient, + app_id: &str, +) -> anyhow::Result> { + let secrets = wasmer_api::query::get_all_app_secrets(client, app_id).await?; + let mut ret = vec![]; + for secret in secrets { + let name = secret.name.clone(); + let value = get_secret_value(client, &secret).await?; + ret.push(Secret { name, value }); + } + + Ok(ret) +} + +/// Utility struct used just to implement [`CliRender`]. +#[derive(Debug, serde::Serialize)] +pub(super) struct BackendSecretWrapper(pub BackendSecret); + +impl From for BackendSecretWrapper { + fn from(value: BackendSecret) -> Self { + Self(value) + } +} diff --git a/lib/cli/src/commands/app/secrets/utils/render.rs b/lib/cli/src/commands/app/secrets/utils/render.rs new file mode 100644 index 00000000000..66389f4de6c --- /dev/null +++ b/lib/cli/src/commands/app/secrets/utils/render.rs @@ -0,0 +1,110 @@ +use super::{BackendSecretWrapper, Secret}; +use crate::utils::render::CliRender; +use comfy_table::{Cell, Table}; +use time::OffsetDateTime; +use wasmer_api::types::{DateTime, Secret as BackendSecret}; + +impl CliRender for Secret { + fn render_item_table(&self) -> String { + let mut table = Table::new(); + let Secret { name, value }: &Secret = self; + + table.load_preset(comfy_table::presets::NOTHING); + table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); + + let value = sanitize_value(value); + table.add_rows([ + vec![ + Cell::new("Name".to_string()).add_attribute(comfy_table::Attribute::Bold), + Cell::new("Value".to_string()).add_attribute(comfy_table::Attribute::Bold), + ], + vec![Cell::new(name.to_string()), Cell::new(format!("'{value}'"))], + ]); + table.to_string() + } + + fn render_list_table(items: &[Self]) -> String { + if items.is_empty() { + return String::new(); + } + let mut table = Table::new(); + table.load_preset(comfy_table::presets::NOTHING); + table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); + + table.set_header(vec![ + Cell::new("Name".to_string()).add_attribute(comfy_table::Attribute::Bold), + Cell::new("Value".to_string()).add_attribute(comfy_table::Attribute::Bold), + ]); + table.add_rows(items.iter().map(|s| { + vec![ + Cell::new(s.name.clone()), + Cell::new(format!("'{}'", sanitize_value(&s.value))), + ] + })); + table.to_string() + } +} + +impl CliRender for BackendSecretWrapper { + fn render_item_table(&self) -> String { + let mut table = Table::new(); + let BackendSecret { + name, updated_at, .. + }: &BackendSecret = &self.0; + let last_updated = last_updated_to_human(updated_at.clone()) + .unwrap() + .to_string(); + table.add_rows([ + vec!["Name".to_string(), name.to_string()], + vec!["Last updated".to_string(), format!("{last_updated} ago")], + ]); + table.to_string() + } + + fn render_list_table(items: &[Self]) -> String { + if items.is_empty() { + return String::new(); + } + let mut table = Table::new(); + table.load_preset(comfy_table::presets::NOTHING); + table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); + + table.set_header(vec![ + Cell::new("Name".to_string()).add_attribute(comfy_table::Attribute::Bold), + Cell::new("Last updated".to_string()).add_attribute(comfy_table::Attribute::Bold), + ]); + table.add_rows(items.iter().map(|s| { + let last_updated = last_updated_to_human(s.0.updated_at.clone()) + .unwrap() + .to_string(); + vec![ + Cell::new(s.0.name.clone()), + Cell::new(format!("{last_updated} ago")), + ] + })); + table.to_string() + } +} + +fn last_updated_to_human(last_update: DateTime) -> anyhow::Result { + let last_update: OffsetDateTime = last_update.try_into()?; + let elapsed: std::time::Duration = (OffsetDateTime::now_utc() - last_update).try_into()?; + Ok(humantime::Duration::from(std::time::Duration::from_secs( + elapsed.as_secs(), + ))) +} + +pub(crate) fn sanitize_value(value: &str) -> String { + value + .chars() + .map(|c| { + if c.is_ascii() { + let c = c as u8; + std::ascii::escape_default(c).to_string() + } else { + c.to_string() + } + }) + .collect::>() + .join("") +} diff --git a/lib/cli/src/commands/app/util.rs b/lib/cli/src/commands/app/util.rs index c4f93c94b28..51c1b946f38 100644 --- a/lib/cli/src/commands/app/util.rs +++ b/lib/cli/src/commands/app/util.rs @@ -1,6 +1,8 @@ +use std::str::FromStr; + use anyhow::{bail, Context}; use colored::Colorize; -use dialoguer::Confirm; +use dialoguer::{Confirm, theme::ColorfulTheme}; use wasmer_api::{ global_id::{GlobalId, NodeKind}, types::DeployApp, @@ -270,3 +272,29 @@ pub(super) async fn login_user( api.client() } + +pub(super) async fn get_app_id_from_config( + app_dir_path: &std::path::Path, +) -> anyhow::Result> { + Ok(AppConfigV1::parse_yaml( + &tokio::fs::read_to_string(app_dir_path.join(AppConfigV1::CANONICAL_FILE_NAME)).await?, + )? + .app_id) +} + +/// Prompt for an app ident. +#[allow(dead_code)] +pub(crate) fn prompt_app_ident( + message: &str, +) -> Result { + let theme = ColorfulTheme::default(); + loop { + let ident: String = dialoguer::Input::with_theme(&theme) + .with_prompt(message) + .interact_text()?; + match AppIdent::from_str(&ident) { + Ok(id) => break Ok(id), + Err(e) => eprintln!("{e}"), + } + } +} From a88855f0589112b9bcc5f864baf5e28b168f2c14 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Mon, 15 Jul 2024 15:36:16 +0200 Subject: [PATCH 02/12] feat(cli/secrets): Various QoL fixes --- lib/cli/src/commands/app/secrets/create.rs | 47 ++++++++++---- lib/cli/src/commands/app/secrets/delete.rs | 37 ++++++++--- lib/cli/src/commands/app/secrets/list.rs | 26 ++++++-- lib/cli/src/commands/app/secrets/mod.rs | 2 +- lib/cli/src/commands/app/secrets/reveal.rs | 25 ++++++-- lib/cli/src/commands/app/secrets/update.rs | 33 +++++++--- .../src/commands/app/secrets/utils/render.rs | 5 +- lib/cli/src/commands/app/util.rs | 64 +++++++++---------- 8 files changed, 161 insertions(+), 78 deletions(-) diff --git a/lib/cli/src/commands/app/secrets/create.rs b/lib/cli/src/commands/app/secrets/create.rs index 214a58b78c0..0c5a839228c 100644 --- a/lib/cli/src/commands/app/secrets/create.rs +++ b/lib/cli/src/commands/app/secrets/create.rs @@ -1,7 +1,7 @@ use super::utils::Secret; use crate::{ commands::{ - app::util::{get_app_id_from_config, prompt_app_ident, AppIdent}, + app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent}, AsyncCliCommand, }, opts::{ApiOpts, WasmerEnv}, @@ -20,9 +20,6 @@ use wasmer_api::WasmerClient; /// Create a new secret related to an Edge app. #[derive(clap::Parser, Debug)] pub struct CmdAppSecretsCreate { - /// The identifier of the app the secret is related to. - pub app_id: Option, - /// The path to the directory where the config file for the application will be written to. #[clap(long = "app-dir", conflicts_with = "app-id")] pub app_dir_path: Option, @@ -35,6 +32,9 @@ pub struct CmdAppSecretsCreate { #[clap(name = "value")] pub secret_value: Option, + /// The identifier of the app the secret is related to. + pub app_id: Option, + /// Path to a file with secrets stored in JSON format to create secrets from. #[clap( long, @@ -104,8 +104,22 @@ impl CmdAppSecretsCreate { current_dir()? }; - if let Ok(Some(app_id)) = get_app_id_from_config(&app_dir_path).await { - return Ok(app_id.clone()); + if let Ok(r) = get_app_config_from_dir(&app_dir_path) { + let (app, _) = r; + + if let Some(id) = &app.app_id { + if !self.quiet { + if let Some(owner) = &app.owner { + eprintln!( + "Managing secrets related to app {} ({owner}).", + app.name.bold() + ); + } else { + eprintln!("Managing secrets related to app {}.", app.name.bold()); + } + } + return Ok(id.clone()); + } } if self.non_interactive { @@ -113,7 +127,7 @@ impl CmdAppSecretsCreate { } else { let id = prompt_app_ident("Enter the name of the app")?; let app = id.resolve(client).await?; - return Ok(app.id.into_inner()); + Ok(app.id.into_inner()) } } @@ -134,19 +148,26 @@ impl CmdAppSecretsCreate { for secret in secrets { if sset.contains(&secret.name) { if self.non_interactive { - anyhow::bail!("Cannot create secret '{}' in app {app_id} as it already exists. Use the `update` command instead.", secret.name.bold()); + anyhow::bail!("Cannot create secret '{}' as it already exists. Use the `update` command instead.", secret.name.bold()); } else { - eprintln!( - "Secret '{}' already exists for the selected app.", - secret.name.bold() - ); + if ret.contains_key(&secret.name) { + eprintln!( + "Secret '{}' appears twice in the input file.", + secret.name.bold() + ); + } else { + eprintln!( + "Secret '{}' already exists for the selected app.", + secret.name.bold() + ); + } let theme = ColorfulTheme::default(); let res = dialoguer::Confirm::with_theme(&theme) .with_prompt("Do you want to update it?") .interact()?; if !res { - eprintln!("Cannot create secret '{}' in app {app_id} as it already exists. Use the `update` command instead.", secret.name.bold()); + eprintln!("Cannot create secret '{}' as it already exists. Use the `update` command instead.", secret.name.bold()); } } } diff --git a/lib/cli/src/commands/app/secrets/delete.rs b/lib/cli/src/commands/app/secrets/delete.rs index c9c6fe868dc..3da462c4f0a 100644 --- a/lib/cli/src/commands/app/secrets/delete.rs +++ b/lib/cli/src/commands/app/secrets/delete.rs @@ -1,5 +1,8 @@ use crate::{ - commands::{app::util::{get_app_id_from_config, AppIdent, prompt_app_ident}, AsyncCliCommand}, + commands::{ + app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent}, + AsyncCliCommand, + }, opts::{ApiOpts, WasmerEnv}, }; use colored::Colorize; @@ -16,6 +19,10 @@ use super::utils::{self, get_secrets}; /// Delete an existing secret related to an Edge app. #[derive(clap::Parser, Debug)] pub struct CmdAppSecretsDelete { + /// The name of the secret to delete. + #[clap(name = "name")] + pub secret_name: Option, + /// The id of the app the secret is related to. pub app_id: Option, @@ -23,10 +30,6 @@ pub struct CmdAppSecretsDelete { #[clap(long = "app-dir", conflicts_with = "app-id")] pub app_dir_path: Option, - /// The name of the secret to delete. - #[clap(name = "name")] - pub secret_name: Option, - /// Path to a file with secrets stored in JSON format to delete secrets from. #[clap( long, @@ -89,8 +92,23 @@ impl CmdAppSecretsDelete { current_dir()? }; - if let Ok(Some(app_id)) = get_app_id_from_config(&app_dir_path).await { - return Ok(app_id.clone()); + if let Ok(r) = get_app_config_from_dir(&app_dir_path) { + let (app, _) = r; + + if let Some(id) = &app.app_id { + if !self.quiet { + if let Some(owner) = &app.owner { + eprintln!( + "Managing secrets related to app {} ({owner}).", + app.name.bold() + ); + } else { + eprintln!("Managing secrets related to app {}.", app.name.bold()); + } + } + + return Ok(id.clone()); + } } if self.non_interactive { @@ -98,7 +116,7 @@ impl CmdAppSecretsDelete { } else { let id = prompt_app_ident("Enter the name of the app")?; let app = id.resolve(client).await?; - return Ok(app.id.into_inner()); + Ok(app.id.into_inner()) } } @@ -168,6 +186,9 @@ impl AsyncCliCommand for CmdAppSecretsDelete { if let Some(file) = &self.from_file { self.delete_from_file(file, app_id).await } else if self.all { + if self.non_interactive && !self.force { + anyhow::bail!("Refusing to delete all secrets in non-interactive mode without the `--force` flag.") + } let secrets = get_secrets(&client, &app_id).await?; for secret in secrets { self.delete(&client, &app_id, &secret.name).await?; diff --git a/lib/cli/src/commands/app/secrets/list.rs b/lib/cli/src/commands/app/secrets/list.rs index baef9424a1f..862a2007f83 100644 --- a/lib/cli/src/commands/app/secrets/list.rs +++ b/lib/cli/src/commands/app/secrets/list.rs @@ -1,14 +1,15 @@ use super::utils::{get_secrets, BackendSecretWrapper}; use crate::{ commands::{ - app::util::{get_app_id_from_config, AppIdent, prompt_app_ident}, + app::util::{prompt_app_ident, AppIdent, get_app_config_from_dir}, AsyncCliCommand, }, opts::{ApiOpts, ListFormatOpts, WasmerEnv}, }; +use colored::Colorize; use is_terminal::IsTerminal; -use wasmer_api::WasmerClient; use std::{env::current_dir, path::PathBuf}; +use wasmer_api::WasmerClient; /// Retrieve the value of an existing secret related to an Edge app. #[derive(clap::Parser, Debug)] @@ -53,8 +54,23 @@ impl CmdAppSecretsList { current_dir()? }; - if let Ok(Some(app_id)) = get_app_id_from_config(&app_dir_path).await { - return Ok(app_id.clone()); + if let Ok(r) = get_app_config_from_dir(&app_dir_path) { + let (app, _) = r; + + if let Some(id) = &app.app_id { + if !self.quiet { +if let Some(owner) = &app.owner { + eprintln!( + "Managing secrets related to app {} ({owner}).", + app.name.bold() + ); + } else { + eprintln!("Managing secrets related to app {}.", app.name.bold()); + } + + } + return Ok(id.clone()); + } } if self.non_interactive { @@ -62,7 +78,7 @@ impl CmdAppSecretsList { } else { let id = prompt_app_ident("Enter the name of the app")?; let app = id.resolve(client).await?; - return Ok(app.id.into_inner()); + Ok(app.id.into_inner()) } } } diff --git a/lib/cli/src/commands/app/secrets/mod.rs b/lib/cli/src/commands/app/secrets/mod.rs index 1328a2f5e69..84b340a43bf 100644 --- a/lib/cli/src/commands/app/secrets/mod.rs +++ b/lib/cli/src/commands/app/secrets/mod.rs @@ -2,8 +2,8 @@ use crate::commands::AsyncCliCommand; pub mod create; pub mod delete; -pub mod reveal; pub mod list; +pub mod reveal; pub mod update; mod utils; diff --git a/lib/cli/src/commands/app/secrets/reveal.rs b/lib/cli/src/commands/app/secrets/reveal.rs index 15db496151f..d1233a59f5a 100644 --- a/lib/cli/src/commands/app/secrets/reveal.rs +++ b/lib/cli/src/commands/app/secrets/reveal.rs @@ -1,16 +1,17 @@ use super::utils; use crate::{ commands::{ - app::util::{get_app_id_from_config, AppIdent, prompt_app_ident}, + app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent}, AsyncCliCommand, }, opts::{ApiOpts, ListFormatOpts, WasmerEnv}, utils::render::{ItemFormat, ListFormat}, }; +use colored::Colorize; use dialoguer::theme::ColorfulTheme; use is_terminal::IsTerminal; -use wasmer_api::WasmerClient; use std::{env::current_dir, path::PathBuf}; +use wasmer_api::WasmerClient; /// Reveal the value of an existing secret related to an Edge app. #[derive(clap::Parser, Debug)] @@ -63,8 +64,22 @@ impl CmdAppSecretsReveal { current_dir()? }; - if let Ok(Some(app_id)) = get_app_id_from_config(&app_dir_path).await { - return Ok(app_id.clone()); + if let Ok(r) = get_app_config_from_dir(&app_dir_path) { + let (app, _) = r; + + if let Some(id) = &app.app_id { + if !self.quiet { + if let Some(owner) = &app.owner { + eprintln!( + "Managing secrets related to app {} ({owner}).", + app.name.bold() + ); + } else { + eprintln!("Managing secrets related to app {}.", app.name.bold()); + } + } + return Ok(id.clone()); + } } if self.non_interactive { @@ -72,7 +87,7 @@ impl CmdAppSecretsReveal { } else { let id = prompt_app_ident("Enter the name of the app")?; let app = id.resolve(client).await?; - return Ok(app.id.into_inner()); + Ok(app.id.into_inner()) } } diff --git a/lib/cli/src/commands/app/secrets/update.rs b/lib/cli/src/commands/app/secrets/update.rs index 38fc386e6c2..7502523a057 100644 --- a/lib/cli/src/commands/app/secrets/update.rs +++ b/lib/cli/src/commands/app/secrets/update.rs @@ -1,7 +1,7 @@ use super::utils::Secret; use crate::{ commands::{ - app::util::{get_app_id_from_config, AppIdent, prompt_app_ident}, + app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent}, AsyncCliCommand, }, opts::{ApiOpts, WasmerEnv}, @@ -20,10 +20,6 @@ use wasmer_api::WasmerClient; /// Update an existing secret related to an Edge app. #[derive(clap::Parser, Debug)] pub struct CmdAppSecretsUpdate { - /// The id of the app the secret is related to. - #[clap(long = "app-id")] - pub app_id: Option, - /// The path to the directory where the config file for the application will be written to. #[clap(long = "app-dir", conflicts_with = "app-id")] pub app_dir_path: Option, @@ -36,6 +32,10 @@ pub struct CmdAppSecretsUpdate { #[clap(name = "value")] pub secret_value: Option, + /// The id of the app the secret is related to. + #[clap(long = "app-id")] + pub app_id: Option, + /// Path to a file with secrets stored in JSON format to update secrets from. #[clap( long, @@ -105,8 +105,22 @@ impl CmdAppSecretsUpdate { current_dir()? }; - if let Ok(Some(app_id)) = get_app_id_from_config(&app_dir_path).await { - return Ok(app_id.clone()); + if let Ok(r) = get_app_config_from_dir(&app_dir_path) { + let (app, _) = r; + + if let Some(id) = &app.app_id { + if !self.quiet { + if let Some(owner) = &app.owner { + eprintln!( + "Managing secrets related to app {} ({owner}).", + app.name.bold() + ); + } else { + eprintln!("Managing secrets related to app {}.", app.name.bold()); + } + } + return Ok(id.clone()); + } } if self.non_interactive { @@ -114,7 +128,7 @@ impl CmdAppSecretsUpdate { } else { let id = prompt_app_ident("Enter the name of the app")?; let app = id.resolve(client).await?; - return Ok(app.id.into_inner()); + Ok(app.id.into_inner()) } } @@ -178,8 +192,9 @@ impl CmdAppSecretsUpdate { anyhow::bail!("Secret creation failed!") } else { if !self.quiet { + eprintln!("Succesfully updated secret(s):"); for secret in secrets { - eprintln!("Succesfully updated secret '{}'", secret.name.bold()); + eprintln!("{}", secret.name.bold()); } } diff --git a/lib/cli/src/commands/app/secrets/utils/render.rs b/lib/cli/src/commands/app/secrets/utils/render.rs index 66389f4de6c..c3de77e014a 100644 --- a/lib/cli/src/commands/app/secrets/utils/render.rs +++ b/lib/cli/src/commands/app/secrets/utils/render.rs @@ -1,5 +1,6 @@ use super::{BackendSecretWrapper, Secret}; use crate::utils::render::CliRender; +use colored::Colorize; use comfy_table::{Cell, Table}; use time::OffsetDateTime; use wasmer_api::types::{DateTime, Secret as BackendSecret}; @@ -56,7 +57,7 @@ impl CliRender for BackendSecretWrapper { .to_string(); table.add_rows([ vec!["Name".to_string(), name.to_string()], - vec!["Last updated".to_string(), format!("{last_updated} ago")], + vec!["Last updated".to_string(), format!("{last_updated} ago").dimmed().to_string()], ]); table.to_string() } @@ -79,7 +80,7 @@ impl CliRender for BackendSecretWrapper { .to_string(); vec![ Cell::new(s.0.name.clone()), - Cell::new(format!("{last_updated} ago")), + Cell::new(format!("{last_updated} ago").dimmed().to_string()), ] })); table.to_string() diff --git a/lib/cli/src/commands/app/util.rs b/lib/cli/src/commands/app/util.rs index 51c1b946f38..90ccc4d21ec 100644 --- a/lib/cli/src/commands/app/util.rs +++ b/lib/cli/src/commands/app/util.rs @@ -1,8 +1,8 @@ -use std::str::FromStr; +use std::{path::Path, str::FromStr}; use anyhow::{bail, Context}; use colored::Colorize; -use dialoguer::{Confirm, theme::ColorfulTheme}; +use dialoguer::{theme::ColorfulTheme, Confirm}; use wasmer_api::{ global_id::{GlobalId, NodeKind}, types::DeployApp, @@ -96,29 +96,6 @@ impl std::str::FromStr for AppIdent { } } -pub fn get_app_config_from_current_dir() -> Result<(AppConfigV1, std::path::PathBuf), anyhow::Error> -{ - // read the information from local `app.yaml - let current_dir = std::env::current_dir()?; - let app_config_path = current_dir.join(AppConfigV1::CANONICAL_FILE_NAME); - - if !app_config_path.exists() || !app_config_path.is_file() { - bail!( - "Could not find app.yaml at path: '{}'.\nPlease specify an app like 'wasmer app get /' or 'wasmer app get `'", - app_config_path.display() - ); - } - // read the app.yaml - let raw_app_config = std::fs::read_to_string(&app_config_path) - .with_context(|| format!("Could not read file '{}'", app_config_path.display()))?; - - // parse the app.yaml - let config = AppConfigV1::parse_yaml(&raw_app_config) - .map_err(|err| anyhow::anyhow!("Could not parse app.yaml: {err:?}"))?; - - Ok((config, app_config_path)) -} - /// Options for identifying an app. /// /// Provides convenience methods for resolving an app identifier or loading it @@ -273,20 +250,37 @@ pub(super) async fn login_user( api.client() } -pub(super) async fn get_app_id_from_config( - app_dir_path: &std::path::Path, -) -> anyhow::Result> { - Ok(AppConfigV1::parse_yaml( - &tokio::fs::read_to_string(app_dir_path.join(AppConfigV1::CANONICAL_FILE_NAME)).await?, - )? - .app_id) +pub fn get_app_config_from_dir( + path: &Path, +) -> Result<(AppConfigV1, std::path::PathBuf), anyhow::Error> { + let app_config_path = path.join(AppConfigV1::CANONICAL_FILE_NAME); + + if !app_config_path.exists() || !app_config_path.is_file() { + bail!( + "Could not find app.yaml at path: '{}'.\nPlease specify an app like 'wasmer app get /' or 'wasmer app get `'", + app_config_path.display() + ); + } + // read the app.yaml + let raw_app_config = std::fs::read_to_string(&app_config_path) + .with_context(|| format!("Could not read file '{}'", app_config_path.display()))?; + + // parse the app.yaml + let config = AppConfigV1::parse_yaml(&raw_app_config) + .map_err(|err| anyhow::anyhow!("Could not parse app.yaml: {err:?}"))?; + + Ok((config, app_config_path)) +} + +pub fn get_app_config_from_current_dir() -> Result<(AppConfigV1, std::path::PathBuf), anyhow::Error> +{ + let current_dir = std::env::current_dir()?; + get_app_config_from_dir(¤t_dir) } /// Prompt for an app ident. #[allow(dead_code)] -pub(crate) fn prompt_app_ident( - message: &str, -) -> Result { +pub(crate) fn prompt_app_ident(message: &str) -> Result { let theme = ColorfulTheme::default(); loop { let ident: String = dialoguer::Input::with_theme(&theme) From 4aded578e0c1e114ae3f31a075fa6143e3a9a9a2 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Mon, 15 Jul 2024 17:30:26 +0200 Subject: [PATCH 03/12] chore(cli/secrets): Make linter happy --- lib/cli/src/commands/app/secrets/list.rs | 5 ++--- lib/cli/src/commands/app/secrets/utils/mod.rs | 2 +- lib/cli/src/commands/app/secrets/utils/render.rs | 5 ++++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/cli/src/commands/app/secrets/list.rs b/lib/cli/src/commands/app/secrets/list.rs index 862a2007f83..c6c91e48036 100644 --- a/lib/cli/src/commands/app/secrets/list.rs +++ b/lib/cli/src/commands/app/secrets/list.rs @@ -1,7 +1,7 @@ use super::utils::{get_secrets, BackendSecretWrapper}; use crate::{ commands::{ - app::util::{prompt_app_ident, AppIdent, get_app_config_from_dir}, + app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent}, AsyncCliCommand, }, opts::{ApiOpts, ListFormatOpts, WasmerEnv}, @@ -59,7 +59,7 @@ impl CmdAppSecretsList { if let Some(id) = &app.app_id { if !self.quiet { -if let Some(owner) = &app.owner { + if let Some(owner) = &app.owner { eprintln!( "Managing secrets related to app {} ({owner}).", app.name.bold() @@ -67,7 +67,6 @@ if let Some(owner) = &app.owner { } else { eprintln!("Managing secrets related to app {}.", app.name.bold()); } - } return Ok(id.clone()); } diff --git a/lib/cli/src/commands/app/secrets/utils/mod.rs b/lib/cli/src/commands/app/secrets/utils/mod.rs index 9a7440f935c..e9d386857f2 100644 --- a/lib/cli/src/commands/app/secrets/utils/mod.rs +++ b/lib/cli/src/commands/app/secrets/utils/mod.rs @@ -24,7 +24,7 @@ pub(super) async fn get_secret_by_name( app_id: &str, secret_name: &str, ) -> anyhow::Result> { - Ok(wasmer_api::query::get_app_secret_by_name(client, app_id, secret_name).await?) + wasmer_api::query::get_app_secret_by_name(client, app_id, secret_name).await } pub(crate) async fn get_secrets( client: &WasmerClient, diff --git a/lib/cli/src/commands/app/secrets/utils/render.rs b/lib/cli/src/commands/app/secrets/utils/render.rs index c3de77e014a..0b679e10b52 100644 --- a/lib/cli/src/commands/app/secrets/utils/render.rs +++ b/lib/cli/src/commands/app/secrets/utils/render.rs @@ -57,7 +57,10 @@ impl CliRender for BackendSecretWrapper { .to_string(); table.add_rows([ vec!["Name".to_string(), name.to_string()], - vec!["Last updated".to_string(), format!("{last_updated} ago").dimmed().to_string()], + vec![ + "Last updated".to_string(), + format!("{last_updated} ago").dimmed().to_string(), + ], ]); table.to_string() } From c3fae859e8cb530aa813b57b1c45ca1e7e554305 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Mon, 15 Jul 2024 17:36:37 +0200 Subject: [PATCH 04/12] chore: Update yanked `bytes` version --- Cargo.lock | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9427857f16..c9a35d46a36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -435,9 +435,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" dependencies = [ "serde", ] @@ -2086,7 +2086,7 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ - "bytes 1.6.0", + "bytes 1.6.1", "fnv", "futures-core", "futures-sink", @@ -2236,7 +2236,7 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.6.0", + "bytes 1.6.1", "fnv", "itoa", ] @@ -2247,7 +2247,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "bytes 1.6.0", + "bytes 1.6.1", "http", "pin-project-lite", ] @@ -2292,7 +2292,7 @@ version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ - "bytes 1.6.0", + "bytes 1.6.1", "futures-channel", "futures-core", "futures-util", @@ -2330,7 +2330,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.6.0", + "bytes 1.6.1", "hyper", "native-tls", "tokio 1.37.0", @@ -3975,7 +3975,7 @@ checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "async-compression", "base64 0.21.7", - "bytes 1.6.0", + "bytes 1.6.1", "encoding_rs", "futures-core", "futures-util", @@ -4039,7 +4039,7 @@ checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" dependencies = [ "bitvec", "bytecheck", - "bytes 1.6.0", + "bytes 1.6.1", "hashbrown 0.12.3", "indexmap 1.9.3", "ptr_meta", @@ -4681,7 +4681,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6c99835bad52957e7aa241d3975ed17c1e5f8c92026377d117a606f36b84b16" dependencies = [ - "bytes 1.6.0", + "bytes 1.6.1", "memmap2 0.6.2", ] @@ -5171,7 +5171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", - "bytes 1.6.0", + "bytes 1.6.1", "libc", "mio 0.8.11", "num_cpus", @@ -5311,7 +5311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "911a61637386b789af998ee23f50aa30d5fd7edcec8d6d3dedae5e5815205466" dependencies = [ "bincode", - "bytes 1.6.0", + "bytes 1.6.1", "educe", "futures-core", "futures-sink", @@ -5454,7 +5454,7 @@ version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ - "bytes 1.6.0", + "bytes 1.6.1", "futures-core", "futures-sink", "pin-project-lite", @@ -5553,7 +5553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ "bitflags 2.5.0", - "bytes 1.6.0", + "bytes 1.6.1", "futures-core", "futures-util", "http", @@ -5727,7 +5727,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ "byteorder", - "bytes 1.6.0", + "bytes 1.6.1", "data-encoding", "http", "httparse", @@ -5942,7 +5942,7 @@ version = "0.14.0" dependencies = [ "anyhow", "async-trait", - "bytes 1.6.0", + "bytes 1.6.1", "derivative", "dunce", "filetime", @@ -5972,7 +5972,7 @@ name = "virtual-mio" version = "0.3.1" dependencies = [ "async-trait", - "bytes 1.6.0", + "bytes 1.6.1", "derivative", "futures 0.3.30", "mio 0.8.11", @@ -5991,7 +5991,7 @@ dependencies = [ "base64 0.21.7", "bincode", "bytecheck", - "bytes 1.6.0", + "bytes 1.6.1", "derivative", "futures-util", "hyper", @@ -6362,7 +6362,7 @@ name = "wasmer" version = "4.3.4" dependencies = [ "anyhow", - "bytes 1.6.0", + "bytes 1.6.1", "cfg-if 1.0.0", "derivative", "hashbrown 0.11.2", @@ -6536,7 +6536,7 @@ dependencies = [ "anyhow", "assert_cmd 2.0.14", "async-trait", - "bytes 1.6.0", + "bytes 1.6.1", "bytesize", "cargo_metadata", "cfg-if 1.0.0", @@ -6630,7 +6630,7 @@ name = "wasmer-compiler" version = "4.3.4" dependencies = [ "backtrace", - "bytes 1.6.0", + "bytes 1.6.1", "cfg-if 1.0.0", "enum-iterator", "enumset", @@ -6877,7 +6877,7 @@ dependencies = [ "base64 0.21.7", "bincode", "bytecheck", - "bytes 1.6.0", + "bytes 1.6.1", "derivative", "lz4_flex", "num_enum", @@ -7036,7 +7036,7 @@ dependencies = [ "bincode", "blake3", "bytecheck", - "bytes 1.6.0", + "bytes 1.6.1", "cfg-if 1.0.0", "chrono", "cooked-waker", @@ -7314,7 +7314,7 @@ checksum = "c1fc686c7b43c9bc630a499f6ae1f0a4c4bd656576a53ae8a147b0cc9bc983ad" dependencies = [ "anyhow", "base64 0.21.7", - "bytes 1.6.0", + "bytes 1.6.1", "cfg-if 1.0.0", "document-features", "flate2", From e93a13b560d7b969038e9b5356510d3062e6ac35 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Mon, 15 Jul 2024 20:31:47 +0200 Subject: [PATCH 05/12] fix(cli/secrets): Fix arg groups for `secrets` subcommands --- lib/cli/src/commands/app/secrets/create.rs | 4 ++-- lib/cli/src/commands/app/secrets/delete.rs | 5 +++-- lib/cli/src/commands/app/secrets/list.rs | 3 ++- lib/cli/src/commands/app/secrets/reveal.rs | 3 ++- lib/cli/src/commands/app/secrets/update.rs | 6 +++--- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/cli/src/commands/app/secrets/create.rs b/lib/cli/src/commands/app/secrets/create.rs index 0c5a839228c..10853dc7c6e 100644 --- a/lib/cli/src/commands/app/secrets/create.rs +++ b/lib/cli/src/commands/app/secrets/create.rs @@ -33,14 +33,14 @@ pub struct CmdAppSecretsCreate { pub secret_value: Option, /// The identifier of the app the secret is related to. + #[clap(name = "app-id")] pub app_id: Option, /// Path to a file with secrets stored in JSON format to create secrets from. #[clap( long, name = "from-file", - conflicts_with = "secret_name", - conflicts_with = "all" + conflicts_with = "name", )] pub from_file: Option, diff --git a/lib/cli/src/commands/app/secrets/delete.rs b/lib/cli/src/commands/app/secrets/delete.rs index 3da462c4f0a..e9c9a80cd7c 100644 --- a/lib/cli/src/commands/app/secrets/delete.rs +++ b/lib/cli/src/commands/app/secrets/delete.rs @@ -24,6 +24,7 @@ pub struct CmdAppSecretsDelete { pub secret_name: Option, /// The id of the app the secret is related to. + #[clap(name = "app-id")] pub app_id: Option, /// The path to the directory where the config file for the application will be written to. @@ -34,8 +35,8 @@ pub struct CmdAppSecretsDelete { #[clap( long, name = "from-file", - conflicts_with = "secret_value", - conflicts_with = "secret_name" + conflicts_with = "name", + conflicts_with = "all" )] pub from_file: Option, diff --git a/lib/cli/src/commands/app/secrets/list.rs b/lib/cli/src/commands/app/secrets/list.rs index c6c91e48036..74dbffcda59 100644 --- a/lib/cli/src/commands/app/secrets/list.rs +++ b/lib/cli/src/commands/app/secrets/list.rs @@ -15,10 +15,11 @@ use wasmer_api::WasmerClient; #[derive(clap::Parser, Debug)] pub struct CmdAppSecretsList { /// The identifier of the app to list secrets of. + #[clap(name = "app-id")] pub app_id: Option, /// The path to the directory where the config file for the application will be written to. - #[clap(long = "app-dir", conflicts_with = "app_id")] + #[clap(long = "app-dir", conflicts_with = "app-id")] pub app_dir_path: Option, /* --- Common args --- */ diff --git a/lib/cli/src/commands/app/secrets/reveal.rs b/lib/cli/src/commands/app/secrets/reveal.rs index d1233a59f5a..fe45a0fd620 100644 --- a/lib/cli/src/commands/app/secrets/reveal.rs +++ b/lib/cli/src/commands/app/secrets/reveal.rs @@ -21,10 +21,11 @@ pub struct CmdAppSecretsReveal { pub secret_name: Option, /// The id of the app the secret is related to. + #[clap(name = "app-id")] pub app_id: Option, /// The path to the directory where the config file for the application will be written to. - #[clap(long = "app-dir", conflicts_with = "app_id")] + #[clap(long = "app-dir", conflicts_with = "app-id")] pub app_dir_path: Option, /// Reveal all the secrets related to an app. diff --git a/lib/cli/src/commands/app/secrets/update.rs b/lib/cli/src/commands/app/secrets/update.rs index 7502523a057..0ab7d4d7cc2 100644 --- a/lib/cli/src/commands/app/secrets/update.rs +++ b/lib/cli/src/commands/app/secrets/update.rs @@ -33,15 +33,15 @@ pub struct CmdAppSecretsUpdate { pub secret_value: Option, /// The id of the app the secret is related to. - #[clap(long = "app-id")] + #[clap(name = "app-id")] pub app_id: Option, /// Path to a file with secrets stored in JSON format to update secrets from. #[clap( long, name = "from-file", - conflicts_with = "secret_value", - conflicts_with = "secret_name" + conflicts_with = "value", + conflicts_with = "name" )] pub from_file: Option, From a995e83b0c540ea904bbc5f86fa429d0a4175ace Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Mon, 15 Jul 2024 21:01:46 +0200 Subject: [PATCH 06/12] chore: Make linter happy --- lib/cli/src/commands/app/secrets/create.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/cli/src/commands/app/secrets/create.rs b/lib/cli/src/commands/app/secrets/create.rs index 10853dc7c6e..3bb3f0ba2f1 100644 --- a/lib/cli/src/commands/app/secrets/create.rs +++ b/lib/cli/src/commands/app/secrets/create.rs @@ -37,11 +37,7 @@ pub struct CmdAppSecretsCreate { pub app_id: Option, /// Path to a file with secrets stored in JSON format to create secrets from. - #[clap( - long, - name = "from-file", - conflicts_with = "name", - )] + #[clap(long, name = "from-file", conflicts_with = "name")] pub from_file: Option, /* --- Common args --- */ From e4df13536ed8728ff4020eee203d395c00ad0e1c Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Tue, 16 Jul 2024 08:48:07 +0200 Subject: [PATCH 07/12] feat(cli/secrets): Resolve app id from app name in config file --- lib/cli/src/commands/app/secrets/create.rs | 25 +++++++++++++++++----- lib/cli/src/commands/app/secrets/delete.rs | 24 ++++++++++++++++----- lib/cli/src/commands/app/secrets/list.rs | 22 +++++++++++++++---- lib/cli/src/commands/app/secrets/reveal.rs | 24 ++++++++++++++++----- lib/cli/src/commands/app/secrets/update.rs | 25 +++++++++++++++++----- 5 files changed, 96 insertions(+), 24 deletions(-) diff --git a/lib/cli/src/commands/app/secrets/create.rs b/lib/cli/src/commands/app/secrets/create.rs index 3bb3f0ba2f1..97e435be58d 100644 --- a/lib/cli/src/commands/app/secrets/create.rs +++ b/lib/cli/src/commands/app/secrets/create.rs @@ -14,6 +14,7 @@ use std::{ collections::{HashMap, HashSet}, env::current_dir, path::{Path, PathBuf}, + str::FromStr, }; use wasmer_api::WasmerClient; @@ -64,7 +65,7 @@ impl CmdAppSecretsCreate { } if self.non_interactive { - anyhow::bail!("No secret name given. Use the `--name` flag to specify one.") + anyhow::bail!("No secret name given. Provide one as a positional argument.") } else { let theme = ColorfulTheme::default(); Ok(dialoguer::Input::with_theme(&theme) @@ -79,7 +80,7 @@ impl CmdAppSecretsCreate { } if self.non_interactive { - anyhow::bail!("No secret value given. Use the `--value` flag to specify one.") + anyhow::bail!("No secret value given. Provide one as a positional argument.") } else { let theme = ColorfulTheme::default(); Ok(dialoguer::Input::with_theme(&theme) @@ -103,7 +104,16 @@ impl CmdAppSecretsCreate { if let Ok(r) = get_app_config_from_dir(&app_dir_path) { let (app, _) = r; - if let Some(id) = &app.app_id { + let id = if let Some(id) = &app.app_id { + Some(id.clone()) + } else if let Ok(app_ident) = AppIdent::from_str(&app.name) { + let app = app_ident.resolve(client).await?; + Some(app.id.into_inner()) + } else { + None + }; + + if let Some(id) = id { if !self.quiet { if let Some(owner) = &app.owner { eprintln!( @@ -114,12 +124,17 @@ impl CmdAppSecretsCreate { eprintln!("Managing secrets related to app {}.", app.name.bold()); } } - return Ok(id.clone()); + return Ok(id); } + } else if let Some(path) = &self.app_dir_path { + anyhow::bail!( + "No app configuration file found in path {}.", + path.display() + ) } if self.non_interactive { - anyhow::bail!("No app id given. Use the `--app_id` flag to specify one.") + anyhow::bail!("No app id given. Provide one as a positional argument.") } else { let id = prompt_app_ident("Enter the name of the app")?; let app = id.resolve(client).await?; diff --git a/lib/cli/src/commands/app/secrets/delete.rs b/lib/cli/src/commands/app/secrets/delete.rs index e9c9a80cd7c..0815d5b561e 100644 --- a/lib/cli/src/commands/app/secrets/delete.rs +++ b/lib/cli/src/commands/app/secrets/delete.rs @@ -11,6 +11,7 @@ use is_terminal::IsTerminal; use std::{ env::current_dir, path::{Path, PathBuf}, + str::FromStr, }; use wasmer_api::WasmerClient; @@ -72,7 +73,7 @@ impl CmdAppSecretsDelete { } if self.non_interactive { - anyhow::bail!("No secret name given. Use the `--name` flag to specify one.") + anyhow::bail!("No secret name given. Provide one as a positional argument.") } else { let theme = ColorfulTheme::default(); Ok(dialoguer::Input::with_theme(&theme) @@ -96,7 +97,16 @@ impl CmdAppSecretsDelete { if let Ok(r) = get_app_config_from_dir(&app_dir_path) { let (app, _) = r; - if let Some(id) = &app.app_id { + let id = if let Some(id) = &app.app_id { + Some(id.clone()) + } else if let Ok(app_ident) = AppIdent::from_str(&app.name) { + let app = app_ident.resolve(client).await?; + Some(app.id.into_inner()) + } else { + None + }; + + if let Some(id) = id { if !self.quiet { if let Some(owner) = &app.owner { eprintln!( @@ -107,13 +117,17 @@ impl CmdAppSecretsDelete { eprintln!("Managing secrets related to app {}.", app.name.bold()); } } - - return Ok(id.clone()); + return Ok(id); } + } else if let Some(path) = &self.app_dir_path { + anyhow::bail!( + "No app configuration file found in path {}.", + path.display() + ) } if self.non_interactive { - anyhow::bail!("No app id given. Use the `--app_id` flag to specify one.") + anyhow::bail!("No app id given. Provide one as a positional argument.") } else { let id = prompt_app_ident("Enter the name of the app")?; let app = id.resolve(client).await?; diff --git a/lib/cli/src/commands/app/secrets/list.rs b/lib/cli/src/commands/app/secrets/list.rs index 74dbffcda59..d0d57322ea6 100644 --- a/lib/cli/src/commands/app/secrets/list.rs +++ b/lib/cli/src/commands/app/secrets/list.rs @@ -8,7 +8,7 @@ use crate::{ }; use colored::Colorize; use is_terminal::IsTerminal; -use std::{env::current_dir, path::PathBuf}; +use std::{env::current_dir, path::PathBuf, str::FromStr}; use wasmer_api::WasmerClient; /// Retrieve the value of an existing secret related to an Edge app. @@ -58,7 +58,16 @@ impl CmdAppSecretsList { if let Ok(r) = get_app_config_from_dir(&app_dir_path) { let (app, _) = r; - if let Some(id) = &app.app_id { + let id = if let Some(id) = &app.app_id { + Some(id.clone()) + } else if let Ok(app_ident) = AppIdent::from_str(&app.name) { + let app = app_ident.resolve(client).await?; + Some(app.id.into_inner()) + } else { + None + }; + + if let Some(id) = id { if !self.quiet { if let Some(owner) = &app.owner { eprintln!( @@ -69,12 +78,17 @@ impl CmdAppSecretsList { eprintln!("Managing secrets related to app {}.", app.name.bold()); } } - return Ok(id.clone()); + return Ok(id); } + } else if let Some(path) = &self.app_dir_path { + anyhow::bail!( + "No app configuration file found in path {}.", + path.display() + ) } if self.non_interactive { - anyhow::bail!("No app id given. Use the `--app_id` flag to specify one.") + anyhow::bail!("No app id given. Provide one as a positional argument.") } else { let id = prompt_app_ident("Enter the name of the app")?; let app = id.resolve(client).await?; diff --git a/lib/cli/src/commands/app/secrets/reveal.rs b/lib/cli/src/commands/app/secrets/reveal.rs index fe45a0fd620..e96c40bc966 100644 --- a/lib/cli/src/commands/app/secrets/reveal.rs +++ b/lib/cli/src/commands/app/secrets/reveal.rs @@ -10,7 +10,7 @@ use crate::{ use colored::Colorize; use dialoguer::theme::ColorfulTheme; use is_terminal::IsTerminal; -use std::{env::current_dir, path::PathBuf}; +use std::{env::current_dir, path::PathBuf, str::FromStr}; use wasmer_api::WasmerClient; /// Reveal the value of an existing secret related to an Edge app. @@ -68,7 +68,16 @@ impl CmdAppSecretsReveal { if let Ok(r) = get_app_config_from_dir(&app_dir_path) { let (app, _) = r; - if let Some(id) = &app.app_id { + let id = if let Some(id) = &app.app_id { + Some(id.clone()) + } else if let Ok(app_ident) = AppIdent::from_str(&app.name) { + let app = app_ident.resolve(client).await?; + Some(app.id.into_inner()) + } else { + None + }; + + if let Some(id) = id { if !self.quiet { if let Some(owner) = &app.owner { eprintln!( @@ -79,12 +88,17 @@ impl CmdAppSecretsReveal { eprintln!("Managing secrets related to app {}.", app.name.bold()); } } - return Ok(id.clone()); + return Ok(id); } + } else if let Some(path) = &self.app_dir_path { + anyhow::bail!( + "No app configuration file found in path {}.", + path.display() + ) } if self.non_interactive { - anyhow::bail!("No app id given. Use the `--app_id` flag to specify one.") + anyhow::bail!("No app id given. Provide one as a positional argument.") } else { let id = prompt_app_ident("Enter the name of the app")?; let app = id.resolve(client).await?; @@ -98,7 +112,7 @@ impl CmdAppSecretsReveal { } if self.non_interactive { - anyhow::bail!("No secret name given. Use the `--name` flag to specify one.") + anyhow::bail!("No secret name given. Provide one as a positional argument.") } else { let theme = ColorfulTheme::default(); Ok(dialoguer::Input::with_theme(&theme) diff --git a/lib/cli/src/commands/app/secrets/update.rs b/lib/cli/src/commands/app/secrets/update.rs index 0ab7d4d7cc2..21d8b617bb8 100644 --- a/lib/cli/src/commands/app/secrets/update.rs +++ b/lib/cli/src/commands/app/secrets/update.rs @@ -14,6 +14,7 @@ use std::{ collections::HashSet, env::current_dir, path::{Path, PathBuf}, + str::FromStr, }; use wasmer_api::WasmerClient; @@ -69,7 +70,7 @@ impl CmdAppSecretsUpdate { } if self.non_interactive { - anyhow::bail!("No secret name given. Use the `--name` flag to specify one.") + anyhow::bail!("No secret name given. Provide one as a positional argument.") } else { let theme = ColorfulTheme::default(); Ok(dialoguer::Input::with_theme(&theme) @@ -84,7 +85,7 @@ impl CmdAppSecretsUpdate { } if self.non_interactive { - anyhow::bail!("No secret value given. Use the `--value` flag to specify one.") + anyhow::bail!("No secret value given. Provide one as a positional argument.") } else { let theme = ColorfulTheme::default(); Ok(dialoguer::Input::with_theme(&theme) @@ -108,7 +109,16 @@ impl CmdAppSecretsUpdate { if let Ok(r) = get_app_config_from_dir(&app_dir_path) { let (app, _) = r; - if let Some(id) = &app.app_id { + let id = if let Some(id) = &app.app_id { + Some(id.clone()) + } else if let Ok(app_ident) = AppIdent::from_str(&app.name) { + let app = app_ident.resolve(client).await?; + Some(app.id.into_inner()) + } else { + None + }; + + if let Some(id) = id { if !self.quiet { if let Some(owner) = &app.owner { eprintln!( @@ -119,12 +129,17 @@ impl CmdAppSecretsUpdate { eprintln!("Managing secrets related to app {}.", app.name.bold()); } } - return Ok(id.clone()); + return Ok(id); } + } else if let Some(path) = &self.app_dir_path { + anyhow::bail!( + "No app configuration file found in path {}.", + path.display() + ) } if self.non_interactive { - anyhow::bail!("No app id given. Use the `--app_id` flag to specify one.") + anyhow::bail!("No app id given. Provide one as a positional argument.") } else { let id = prompt_app_ident("Enter the name of the app")?; let app = id.resolve(client).await?; From dca2d520fa18d3ba1122abb88f7cc6cff6f2f2cf Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Tue, 16 Jul 2024 12:16:03 +0200 Subject: [PATCH 08/12] chore: Fix Cargo.lock --- Cargo.lock | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fcef340514d..ec445fd59f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2075,25 +2075,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes 1.6.1", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap 2.2.6", - "slab", - "tokio 1.37.0", - "tokio-util", - "tracing", -] - [[package]] name = "half" version = "1.8.3" @@ -2254,7 +2235,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes 1.6.1", - "pin-project-lite", "http 1.1.0", ] @@ -4022,7 +4002,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ "async-compression", - "encoding_rs", "base64 0.22.1", "bytes 1.6.1", "futures-channel", @@ -5555,7 +5534,6 @@ checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "bitflags 2.5.0", "bytes 1.6.1", - "futures-core", "futures-util", "http 1.1.0", "http-body", From 113559744e57179676fbc3cb044e5581892fdc76 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Tue, 16 Jul 2024 14:00:39 +0200 Subject: [PATCH 09/12] feat(cli/secrets): `app_id` to flag, various QoL improvements --- lib/cli/src/commands/app/secrets/create.rs | 34 +++++++++++++++------- lib/cli/src/commands/app/secrets/delete.rs | 34 +++++++++++++++------- lib/cli/src/commands/app/secrets/list.rs | 31 +++++++++++++++----- lib/cli/src/commands/app/secrets/reveal.rs | 32 ++++++++++++++------ lib/cli/src/commands/app/secrets/update.rs | 31 +++++++++++++++----- lib/cli/src/commands/app/util.rs | 13 +++++++++ 6 files changed, 130 insertions(+), 45 deletions(-) diff --git a/lib/cli/src/commands/app/secrets/create.rs b/lib/cli/src/commands/app/secrets/create.rs index 97e435be58d..f61e82f95cf 100644 --- a/lib/cli/src/commands/app/secrets/create.rs +++ b/lib/cli/src/commands/app/secrets/create.rs @@ -1,7 +1,7 @@ use super::utils::Secret; use crate::{ commands::{ - app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent}, + app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent, AppIdentFlag}, AsyncCliCommand, }, opts::{ApiOpts, WasmerEnv}, @@ -22,7 +22,7 @@ use wasmer_api::WasmerClient; #[derive(clap::Parser, Debug)] pub struct CmdAppSecretsCreate { /// The path to the directory where the config file for the application will be written to. - #[clap(long = "app-dir", conflicts_with = "app-id")] + #[clap(long = "app-dir", conflicts_with = "app")] pub app_dir_path: Option, /// The name of the secret to create. @@ -33,9 +33,8 @@ pub struct CmdAppSecretsCreate { #[clap(name = "value")] pub secret_value: Option, - /// The identifier of the app the secret is related to. - #[clap(name = "app-id")] - pub app_id: Option, + #[clap(flatten)] + pub app_id: AppIdentFlag, /// Path to a file with secrets stored in JSON format to create secrets from. #[clap(long, name = "from-file", conflicts_with = "name")] @@ -90,7 +89,7 @@ impl CmdAppSecretsCreate { } async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { - if let Some(app_id) = &self.app_id { + if let Some(app_id) = &self.app_id.app { let app = app_id.resolve(client).await?; return Ok(app.id.into_inner()); } @@ -104,11 +103,26 @@ impl CmdAppSecretsCreate { if let Ok(r) = get_app_config_from_dir(&app_dir_path) { let (app, _) = r; + let app_name = if let Some(owner) = &app.owner { + format!("{owner}/{}", app.name) + } else { + app.name.to_string() + }; + let id = if let Some(id) = &app.app_id { Some(id.clone()) - } else if let Ok(app_ident) = AppIdent::from_str(&app.name) { - let app = app_ident.resolve(client).await?; - Some(app.id.into_inner()) + } else if let Ok(app_ident) = AppIdent::from_str(&app_name) { + if let Ok(app) = app_ident.resolve(client).await { + Some(app.id.into_inner()) + } else { + if !self.quiet { + eprintln!("{}: the app found in {} does not exist.\n{}: maybe it was not deployed yet?", + "Warning".bold().yellow(), + format!("'{}'", app_dir_path.display()).dimmed(), + "Hint".bold()); + } + None + } } else { None }; @@ -134,7 +148,7 @@ impl CmdAppSecretsCreate { } if self.non_interactive { - anyhow::bail!("No app id given. Provide one as a positional argument.") + anyhow::bail!("No app id given. Provide one using the `--app` flag.") } else { let id = prompt_app_ident("Enter the name of the app")?; let app = id.resolve(client).await?; diff --git a/lib/cli/src/commands/app/secrets/delete.rs b/lib/cli/src/commands/app/secrets/delete.rs index 0815d5b561e..bd4a8e58e0d 100644 --- a/lib/cli/src/commands/app/secrets/delete.rs +++ b/lib/cli/src/commands/app/secrets/delete.rs @@ -1,6 +1,6 @@ use crate::{ commands::{ - app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent}, + app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent, AppIdentFlag}, AsyncCliCommand, }, opts::{ApiOpts, WasmerEnv}, @@ -24,12 +24,11 @@ pub struct CmdAppSecretsDelete { #[clap(name = "name")] pub secret_name: Option, - /// The id of the app the secret is related to. - #[clap(name = "app-id")] - pub app_id: Option, + #[clap(flatten)] + pub app_id: AppIdentFlag, /// The path to the directory where the config file for the application will be written to. - #[clap(long = "app-dir", conflicts_with = "app-id")] + #[clap(long = "app-dir", conflicts_with = "app")] pub app_dir_path: Option, /// Path to a file with secrets stored in JSON format to delete secrets from. @@ -83,7 +82,7 @@ impl CmdAppSecretsDelete { } async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { - if let Some(app_id) = &self.app_id { + if let Some(app_id) = &self.app_id.app { let app = app_id.resolve(client).await?; return Ok(app.id.into_inner()); } @@ -97,11 +96,26 @@ impl CmdAppSecretsDelete { if let Ok(r) = get_app_config_from_dir(&app_dir_path) { let (app, _) = r; + let app_name = if let Some(owner) = &app.owner { + format!("{owner}/{}", app.name) + } else { + app.name.to_string() + }; + let id = if let Some(id) = &app.app_id { Some(id.clone()) - } else if let Ok(app_ident) = AppIdent::from_str(&app.name) { - let app = app_ident.resolve(client).await?; - Some(app.id.into_inner()) + } else if let Ok(app_ident) = AppIdent::from_str(&app_name) { + if let Ok(app) = app_ident.resolve(client).await { + Some(app.id.into_inner()) + } else { + if !self.quiet { + eprintln!("{}: the app found in {} does not exist.\n{}: maybe it was not deployed yet?", + "Warning".bold().yellow(), + format!("'{}'", app_dir_path.display()).dimmed(), + "Hint".bold()); + } + None + } } else { None }; @@ -127,7 +141,7 @@ impl CmdAppSecretsDelete { } if self.non_interactive { - anyhow::bail!("No app id given. Provide one as a positional argument.") + anyhow::bail!("No app id given. Provide one using the `--app` flag.") } else { let id = prompt_app_ident("Enter the name of the app")?; let app = id.resolve(client).await?; diff --git a/lib/cli/src/commands/app/secrets/list.rs b/lib/cli/src/commands/app/secrets/list.rs index d0d57322ea6..4fb7ea3b0b7 100644 --- a/lib/cli/src/commands/app/secrets/list.rs +++ b/lib/cli/src/commands/app/secrets/list.rs @@ -1,7 +1,7 @@ use super::utils::{get_secrets, BackendSecretWrapper}; use crate::{ commands::{ - app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent}, + app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent, AppIdentFlag}, AsyncCliCommand, }, opts::{ApiOpts, ListFormatOpts, WasmerEnv}, @@ -15,11 +15,11 @@ use wasmer_api::WasmerClient; #[derive(clap::Parser, Debug)] pub struct CmdAppSecretsList { /// The identifier of the app to list secrets of. - #[clap(name = "app-id")] - pub app_id: Option, + #[clap(flatten)] + pub app_id: AppIdentFlag, /// The path to the directory where the config file for the application will be written to. - #[clap(long = "app-dir", conflicts_with = "app-id")] + #[clap(long = "app-dir", conflicts_with = "app")] pub app_dir_path: Option, /* --- Common args --- */ @@ -44,7 +44,7 @@ pub struct CmdAppSecretsList { impl CmdAppSecretsList { async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { - if let Some(app_id) = &self.app_id { + if let Some(app_id) = &self.app_id.app { let app = app_id.resolve(client).await?; return Ok(app.id.into_inner()); } @@ -58,11 +58,26 @@ impl CmdAppSecretsList { if let Ok(r) = get_app_config_from_dir(&app_dir_path) { let (app, _) = r; + let app_name = if let Some(owner) = &app.owner { + format!("{owner}/{}", app.name) + } else { + app.name.to_string() + }; + let id = if let Some(id) = &app.app_id { Some(id.clone()) - } else if let Ok(app_ident) = AppIdent::from_str(&app.name) { - let app = app_ident.resolve(client).await?; - Some(app.id.into_inner()) + } else if let Ok(app_ident) = AppIdent::from_str(&app_name) { + if let Ok(app) = app_ident.resolve(client).await { + Some(app.id.into_inner()) + } else { + if !self.quiet { + eprintln!("{}: the app found in {} does not exist.\n{}: maybe it was not deployed yet?", + "Warning".bold().yellow(), + format!("'{}'", app_dir_path.display()).dimmed(), + "Hint".bold()); + } + None + } } else { None }; diff --git a/lib/cli/src/commands/app/secrets/reveal.rs b/lib/cli/src/commands/app/secrets/reveal.rs index e96c40bc966..73acbc7897b 100644 --- a/lib/cli/src/commands/app/secrets/reveal.rs +++ b/lib/cli/src/commands/app/secrets/reveal.rs @@ -1,7 +1,7 @@ use super::utils; use crate::{ commands::{ - app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent}, + app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent, AppIdentFlag}, AsyncCliCommand, }, opts::{ApiOpts, ListFormatOpts, WasmerEnv}, @@ -20,12 +20,11 @@ pub struct CmdAppSecretsReveal { #[clap(name = "name")] pub secret_name: Option, - /// The id of the app the secret is related to. - #[clap(name = "app-id")] - pub app_id: Option, + #[clap(flatten)] + pub app_id: AppIdentFlag, /// The path to the directory where the config file for the application will be written to. - #[clap(long = "app-dir", conflicts_with = "app-id")] + #[clap(long = "app-dir", conflicts_with = "app")] pub app_dir_path: Option, /// Reveal all the secrets related to an app. @@ -54,7 +53,7 @@ pub struct CmdAppSecretsReveal { impl CmdAppSecretsReveal { async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { - if let Some(app_id) = &self.app_id { + if let Some(app_id) = &self.app_id.app { let app = app_id.resolve(client).await?; return Ok(app.id.into_inner()); } @@ -68,11 +67,26 @@ impl CmdAppSecretsReveal { if let Ok(r) = get_app_config_from_dir(&app_dir_path) { let (app, _) = r; + let app_name = if let Some(owner) = &app.owner { + format!("{owner}/{}", app.name) + } else { + app.name.to_string() + }; + let id = if let Some(id) = &app.app_id { Some(id.clone()) - } else if let Ok(app_ident) = AppIdent::from_str(&app.name) { - let app = app_ident.resolve(client).await?; - Some(app.id.into_inner()) + } else if let Ok(app_ident) = AppIdent::from_str(&app_name) { + if let Ok(app) = app_ident.resolve(client).await { + Some(app.id.into_inner()) + } else { + if !self.quiet { + eprintln!("{}: the app found in {} does not exist.\n{}: maybe it was not deployed yet?", + "Warning".bold().yellow(), + format!("'{}'", app_dir_path.display()).dimmed(), + "Hint".bold()); + } + None + } } else { None }; diff --git a/lib/cli/src/commands/app/secrets/update.rs b/lib/cli/src/commands/app/secrets/update.rs index 21d8b617bb8..c707e9fce40 100644 --- a/lib/cli/src/commands/app/secrets/update.rs +++ b/lib/cli/src/commands/app/secrets/update.rs @@ -1,7 +1,7 @@ use super::utils::Secret; use crate::{ commands::{ - app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent}, + app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent, AppIdentFlag}, AsyncCliCommand, }, opts::{ApiOpts, WasmerEnv}, @@ -33,9 +33,8 @@ pub struct CmdAppSecretsUpdate { #[clap(name = "value")] pub secret_value: Option, - /// The id of the app the secret is related to. - #[clap(name = "app-id")] - pub app_id: Option, + #[clap(flatten)] + pub app_id: AppIdentFlag, /// Path to a file with secrets stored in JSON format to update secrets from. #[clap( @@ -95,7 +94,7 @@ impl CmdAppSecretsUpdate { } async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { - if let Some(app_id) = &self.app_id { + if let Some(app_id) = &self.app_id.app { let app = app_id.resolve(client).await?; return Ok(app.id.into_inner()); } @@ -109,11 +108,27 @@ impl CmdAppSecretsUpdate { if let Ok(r) = get_app_config_from_dir(&app_dir_path) { let (app, _) = r; + let app_name = if let Some(owner) = &app.owner { + format!("{owner}/{}", app.name) + } else { + app.name.to_string() + }; + let id = if let Some(id) = &app.app_id { Some(id.clone()) - } else if let Ok(app_ident) = AppIdent::from_str(&app.name) { - let app = app_ident.resolve(client).await?; - Some(app.id.into_inner()) + } else if let Ok(app_ident) = AppIdent::from_str(&app_name) { + if let Ok(app) = app_ident.resolve(client).await { + Some(app.id.into_inner()) + } else { + if !self.quiet { + eprintln!("{}: the app found in {} does not exist.\n{}: maybe it was not deployed yet?", + "Warning".bold().yellow(), + format!("'{}'", app_dir_path.display()).dimmed(), + "Hint".bold()); + } + + None + } } else { None }; diff --git a/lib/cli/src/commands/app/util.rs b/lib/cli/src/commands/app/util.rs index 90ccc4d21ec..b9692b72b8e 100644 --- a/lib/cli/src/commands/app/util.rs +++ b/lib/cli/src/commands/app/util.rs @@ -117,6 +117,19 @@ pub struct AppIdentOpts { pub app: Option, } +/// A utility struct used by commands that need the [`AppIdent`] as a flag. +#[derive(clap::Parser, Debug)] +pub struct AppIdentFlag { + /// Identifier of the application. + /// + /// Valid input: + /// - namespace/app-name + /// - app-alias + /// - App ID + #[clap(long)] + pub app: Option, +} + // Allowing because this is not performance-critical at all. #[allow(clippy::large_enum_variant)] pub enum ResolvedAppIdent { From e1b8201def29099b5a48d08eaab49d7e3c3bfa20 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Tue, 16 Jul 2024 17:03:35 +0200 Subject: [PATCH 10/12] fix(backend-api): Remove useless sanity checks --- lib/backend-api/src/query.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/backend-api/src/query.rs b/lib/backend-api/src/query.rs index d4839cdfc38..0fae804ef5f 100644 --- a/lib/backend-api/src/query.rs +++ b/lib/backend-api/src/query.rs @@ -101,7 +101,7 @@ pub async fn get_all_app_secrets_filtered( names: Some(names.into_iter().map(|s| s.into()).collect()), }; - let mut all_versions = Vec::::new(); + let mut all_secrets = Vec::::new(); loop { let page = get_app_secrets(client, vars.clone()).await?; @@ -119,17 +119,14 @@ pub async fn get_all_app_secrets_filtered( None => continue, }; - // Sanity check to avoid duplication. - if all_versions.iter().any(|v| v.id == version.id) == false { - all_versions.push(version); - } + all_secrets.push(version); // Update pagination. vars.after = Some(edge.cursor); } } - Ok(all_versions) + Ok(all_secrets) } /// Load all secrets of an app. @@ -149,7 +146,7 @@ pub async fn get_all_app_secrets( names: None, }; - let mut all_versions = Vec::::new(); + let mut all_secrets = Vec::::new(); loop { let page = get_app_secrets(client, vars.clone()).await?; @@ -167,17 +164,14 @@ pub async fn get_all_app_secrets( None => continue, }; - // Sanity check to avoid duplication. - if all_versions.iter().any(|v| v.id == version.id) == false { - all_versions.push(version); - } + all_secrets.push(version); // Update pagination. vars.after = Some(edge.cursor); } } - Ok(all_versions) + Ok(all_secrets) } /// Retrieve secrets for an app. From 846514378414fa35b51476777faa9714cf0f6b81 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Tue, 16 Jul 2024 17:04:26 +0200 Subject: [PATCH 11/12] fix(cli/secrets): Change order of fields in structs and move `get_app_id` to `utils` --- lib/cli/src/commands/app/secrets/create.rs | 130 +++++----------- lib/cli/src/commands/app/secrets/delete.rs | 127 ++++------------ lib/cli/src/commands/app/secrets/list.rs | 104 +++---------- lib/cli/src/commands/app/secrets/reveal.rs | 116 ++++----------- lib/cli/src/commands/app/secrets/update.rs | 139 +++++------------- lib/cli/src/commands/app/secrets/utils/mod.rs | 83 ++++++++++- 6 files changed, 232 insertions(+), 467 deletions(-) diff --git a/lib/cli/src/commands/app/secrets/create.rs b/lib/cli/src/commands/app/secrets/create.rs index f61e82f95cf..da2ee56cada 100644 --- a/lib/cli/src/commands/app/secrets/create.rs +++ b/lib/cli/src/commands/app/secrets/create.rs @@ -1,9 +1,6 @@ use super::utils::Secret; use crate::{ - commands::{ - app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent, AppIdentFlag}, - AsyncCliCommand, - }, + commands::{app::util::AppIdentFlag, AsyncCliCommand}, opts::{ApiOpts, WasmerEnv}, }; use anyhow::Context; @@ -12,35 +9,14 @@ use dialoguer::theme::ColorfulTheme; use is_terminal::IsTerminal; use std::{ collections::{HashMap, HashSet}, - env::current_dir, path::{Path, PathBuf}, - str::FromStr, }; use wasmer_api::WasmerClient; /// Create a new secret related to an Edge app. #[derive(clap::Parser, Debug)] pub struct CmdAppSecretsCreate { - /// The path to the directory where the config file for the application will be written to. - #[clap(long = "app-dir", conflicts_with = "app")] - pub app_dir_path: Option, - - /// The name of the secret to create. - #[clap(name = "name")] - pub secret_name: Option, - - /// The value of the secret to create. - #[clap(name = "value")] - pub secret_value: Option, - - #[clap(flatten)] - pub app_id: AppIdentFlag, - - /// Path to a file with secrets stored in JSON format to create secrets from. - #[clap(long, name = "from-file", conflicts_with = "name")] - pub from_file: Option, - - /* --- Common args --- */ + /* --- Common flags --- */ #[clap(flatten)] #[allow(missing_docs)] pub api: ApiOpts, @@ -55,6 +31,27 @@ pub struct CmdAppSecretsCreate { /// Do not prompt for user input. #[clap(long, default_value_t = !std::io::stdin().is_terminal())] pub non_interactive: bool, + + /* --- Flags --- */ + /// The path to the directory where the config file for the application will be written to. + #[clap(long = "app-dir", conflicts_with = "app")] + pub app_dir_path: Option, + + #[clap(flatten)] + pub app_id: AppIdentFlag, + + /// Path to a file with secrets stored in JSON format to create secrets from. + #[clap(long, name = "from-file", conflicts_with = "name")] + pub from_file: Option, + + /* --- Parameters --- */ + /// The name of the secret to create. + #[clap(name = "name")] + pub secret_name: Option, + + /// The value of the secret to create. + #[clap(name = "value")] + pub secret_value: Option, } impl CmdAppSecretsCreate { @@ -88,74 +85,6 @@ impl CmdAppSecretsCreate { } } - async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { - if let Some(app_id) = &self.app_id.app { - let app = app_id.resolve(client).await?; - return Ok(app.id.into_inner()); - } - - let app_dir_path = if let Some(app_dir_path) = &self.app_dir_path { - app_dir_path.clone() - } else { - current_dir()? - }; - - if let Ok(r) = get_app_config_from_dir(&app_dir_path) { - let (app, _) = r; - - let app_name = if let Some(owner) = &app.owner { - format!("{owner}/{}", app.name) - } else { - app.name.to_string() - }; - - let id = if let Some(id) = &app.app_id { - Some(id.clone()) - } else if let Ok(app_ident) = AppIdent::from_str(&app_name) { - if let Ok(app) = app_ident.resolve(client).await { - Some(app.id.into_inner()) - } else { - if !self.quiet { - eprintln!("{}: the app found in {} does not exist.\n{}: maybe it was not deployed yet?", - "Warning".bold().yellow(), - format!("'{}'", app_dir_path.display()).dimmed(), - "Hint".bold()); - } - None - } - } else { - None - }; - - if let Some(id) = id { - if !self.quiet { - if let Some(owner) = &app.owner { - eprintln!( - "Managing secrets related to app {} ({owner}).", - app.name.bold() - ); - } else { - eprintln!("Managing secrets related to app {}.", app.name.bold()); - } - } - return Ok(id); - } - } else if let Some(path) = &self.app_dir_path { - anyhow::bail!( - "No app configuration file found in path {}.", - path.display() - ) - } - - if self.non_interactive { - anyhow::bail!("No app id given. Provide one using the `--app` flag.") - } else { - let id = prompt_app_ident("Enter the name of the app")?; - let app = id.resolve(client).await?; - Ok(app.id.into_inner()) - } - } - /// Given a list of secrets, checks if the given secrets already exist for the given app and /// returns a list of secrets that must be upserted. async fn filter_secrets( @@ -231,6 +160,10 @@ impl CmdAppSecretsCreate { for secret in secrets { eprintln!("{}", secret.name.bold()); } + eprintln!( + "{}: In order for secrets to appear in your app, re-deploy it.", + "Info".bold() + ); } Ok(()) @@ -258,7 +191,14 @@ impl AsyncCliCommand for CmdAppSecretsCreate { async fn run_async(self) -> Result { let client = self.api.client()?; - let app_id = self.get_app_id(&client).await?; + let app_id = super::utils::get_app_id( + &client, + self.app_id.app.as_ref(), + self.app_dir_path.as_ref(), + self.quiet, + self.non_interactive, + ) + .await?; if let Some(file) = &self.from_file { self.create_from_file(file, &app_id).await } else { diff --git a/lib/cli/src/commands/app/secrets/delete.rs b/lib/cli/src/commands/app/secrets/delete.rs index bd4a8e58e0d..857c75e72fc 100644 --- a/lib/cli/src/commands/app/secrets/delete.rs +++ b/lib/cli/src/commands/app/secrets/delete.rs @@ -1,18 +1,11 @@ use crate::{ - commands::{ - app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent, AppIdentFlag}, - AsyncCliCommand, - }, + commands::{app::util::AppIdentFlag, AsyncCliCommand}, opts::{ApiOpts, WasmerEnv}, }; use colored::Colorize; use dialoguer::theme::ColorfulTheme; use is_terminal::IsTerminal; -use std::{ - env::current_dir, - path::{Path, PathBuf}, - str::FromStr, -}; +use std::path::{Path, PathBuf}; use wasmer_api::WasmerClient; use super::utils::{self, get_secrets}; @@ -20,10 +13,23 @@ use super::utils::{self, get_secrets}; /// Delete an existing secret related to an Edge app. #[derive(clap::Parser, Debug)] pub struct CmdAppSecretsDelete { - /// The name of the secret to delete. - #[clap(name = "name")] - pub secret_name: Option, + /* --- Common flags --- */ + #[clap(flatten)] + #[allow(missing_docs)] + pub api: ApiOpts, + + #[clap(flatten)] + pub env: WasmerEnv, + + /// Don't print any message. + #[clap(long)] + pub quiet: bool, + /// Do not prompt for user input. + #[clap(long, default_value_t = !std::io::stdin().is_terminal())] + pub non_interactive: bool, + + /* --- Flags --- */ #[clap(flatten)] pub app_id: AppIdentFlag, @@ -48,21 +54,10 @@ pub struct CmdAppSecretsDelete { #[clap(long)] pub force: bool, - /* --- Common args --- */ - #[clap(flatten)] - #[allow(missing_docs)] - pub api: ApiOpts, - - #[clap(flatten)] - pub env: WasmerEnv, - - /// Don't print any message. - #[clap(long)] - pub quiet: bool, - - /// Do not prompt for user input. - #[clap(long, default_value_t = !std::io::stdin().is_terminal())] - pub non_interactive: bool, + /* --- Parameters --- */ + /// The name of the secret to delete. + #[clap(name = "name")] + pub secret_name: Option, } impl CmdAppSecretsDelete { @@ -81,74 +76,6 @@ impl CmdAppSecretsDelete { } } - async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { - if let Some(app_id) = &self.app_id.app { - let app = app_id.resolve(client).await?; - return Ok(app.id.into_inner()); - } - - let app_dir_path = if let Some(app_dir_path) = &self.app_dir_path { - app_dir_path.clone() - } else { - current_dir()? - }; - - if let Ok(r) = get_app_config_from_dir(&app_dir_path) { - let (app, _) = r; - - let app_name = if let Some(owner) = &app.owner { - format!("{owner}/{}", app.name) - } else { - app.name.to_string() - }; - - let id = if let Some(id) = &app.app_id { - Some(id.clone()) - } else if let Ok(app_ident) = AppIdent::from_str(&app_name) { - if let Ok(app) = app_ident.resolve(client).await { - Some(app.id.into_inner()) - } else { - if !self.quiet { - eprintln!("{}: the app found in {} does not exist.\n{}: maybe it was not deployed yet?", - "Warning".bold().yellow(), - format!("'{}'", app_dir_path.display()).dimmed(), - "Hint".bold()); - } - None - } - } else { - None - }; - - if let Some(id) = id { - if !self.quiet { - if let Some(owner) = &app.owner { - eprintln!( - "Managing secrets related to app {} ({owner}).", - app.name.bold() - ); - } else { - eprintln!("Managing secrets related to app {}.", app.name.bold()); - } - } - return Ok(id); - } - } else if let Some(path) = &self.app_dir_path { - anyhow::bail!( - "No app configuration file found in path {}.", - path.display() - ) - } - - if self.non_interactive { - anyhow::bail!("No app id given. Provide one using the `--app` flag.") - } else { - let id = prompt_app_ident("Enter the name of the app")?; - let app = id.resolve(client).await?; - Ok(app.id.into_inner()) - } - } - async fn delete( &self, client: &WasmerClient, @@ -210,8 +137,14 @@ impl AsyncCliCommand for CmdAppSecretsDelete { async fn run_async(self) -> Result { let client = self.api.client()?; - let app_id = self.get_app_id(&client).await?; - + let app_id = super::utils::get_app_id( + &client, + self.app_id.app.as_ref(), + self.app_dir_path.as_ref(), + self.quiet, + self.non_interactive, + ) + .await?; if let Some(file) = &self.from_file { self.delete_from_file(file, app_id).await } else if self.all { diff --git a/lib/cli/src/commands/app/secrets/list.rs b/lib/cli/src/commands/app/secrets/list.rs index 4fb7ea3b0b7..2d57c1d47dd 100644 --- a/lib/cli/src/commands/app/secrets/list.rs +++ b/lib/cli/src/commands/app/secrets/list.rs @@ -1,28 +1,15 @@ use super::utils::{get_secrets, BackendSecretWrapper}; use crate::{ - commands::{ - app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent, AppIdentFlag}, - AsyncCliCommand, - }, + commands::{app::util::AppIdentFlag, AsyncCliCommand}, opts::{ApiOpts, ListFormatOpts, WasmerEnv}, }; -use colored::Colorize; use is_terminal::IsTerminal; -use std::{env::current_dir, path::PathBuf, str::FromStr}; -use wasmer_api::WasmerClient; +use std::path::PathBuf; /// Retrieve the value of an existing secret related to an Edge app. #[derive(clap::Parser, Debug)] pub struct CmdAppSecretsList { - /// The identifier of the app to list secrets of. - #[clap(flatten)] - pub app_id: AppIdentFlag, - - /// The path to the directory where the config file for the application will be written to. - #[clap(long = "app-dir", conflicts_with = "app")] - pub app_dir_path: Option, - - /* --- Common args --- */ + /* --- Common flags --- */ #[clap(flatten)] #[allow(missing_docs)] pub api: ApiOpts, @@ -40,76 +27,15 @@ pub struct CmdAppSecretsList { #[clap(flatten)] pub fmt: ListFormatOpts, -} - -impl CmdAppSecretsList { - async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { - if let Some(app_id) = &self.app_id.app { - let app = app_id.resolve(client).await?; - return Ok(app.id.into_inner()); - } - - let app_dir_path = if let Some(app_dir_path) = &self.app_dir_path { - app_dir_path.clone() - } else { - current_dir()? - }; - if let Ok(r) = get_app_config_from_dir(&app_dir_path) { - let (app, _) = r; - - let app_name = if let Some(owner) = &app.owner { - format!("{owner}/{}", app.name) - } else { - app.name.to_string() - }; - - let id = if let Some(id) = &app.app_id { - Some(id.clone()) - } else if let Ok(app_ident) = AppIdent::from_str(&app_name) { - if let Ok(app) = app_ident.resolve(client).await { - Some(app.id.into_inner()) - } else { - if !self.quiet { - eprintln!("{}: the app found in {} does not exist.\n{}: maybe it was not deployed yet?", - "Warning".bold().yellow(), - format!("'{}'", app_dir_path.display()).dimmed(), - "Hint".bold()); - } - None - } - } else { - None - }; - - if let Some(id) = id { - if !self.quiet { - if let Some(owner) = &app.owner { - eprintln!( - "Managing secrets related to app {} ({owner}).", - app.name.bold() - ); - } else { - eprintln!("Managing secrets related to app {}.", app.name.bold()); - } - } - return Ok(id); - } - } else if let Some(path) = &self.app_dir_path { - anyhow::bail!( - "No app configuration file found in path {}.", - path.display() - ) - } + /* --- Flags --- */ + /// The identifier of the app to list secrets of. + #[clap(flatten)] + pub app_id: AppIdentFlag, - if self.non_interactive { - anyhow::bail!("No app id given. Provide one as a positional argument.") - } else { - let id = prompt_app_ident("Enter the name of the app")?; - let app = id.resolve(client).await?; - Ok(app.id.into_inner()) - } - } + /// The path to the directory where the config file for the application will be written to. + #[clap(long = "app-dir", conflicts_with = "app")] + pub app_dir_path: Option, } #[async_trait::async_trait] @@ -118,7 +44,15 @@ impl AsyncCliCommand for CmdAppSecretsList { async fn run_async(self) -> Result { let client = self.api.client()?; - let app_id = self.get_app_id(&client).await?; + let app_id = super::utils::get_app_id( + &client, + self.app_id.app.as_ref(), + self.app_dir_path.as_ref(), + self.quiet, + self.non_interactive, + ) + .await?; + let secrets: Vec = get_secrets(&client, &app_id) .await? .into_iter() diff --git a/lib/cli/src/commands/app/secrets/reveal.rs b/lib/cli/src/commands/app/secrets/reveal.rs index 73acbc7897b..224e6184997 100644 --- a/lib/cli/src/commands/app/secrets/reveal.rs +++ b/lib/cli/src/commands/app/secrets/reveal.rs @@ -1,37 +1,17 @@ use super::utils; use crate::{ - commands::{ - app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent, AppIdentFlag}, - AsyncCliCommand, - }, + commands::{app::util::AppIdentFlag, AsyncCliCommand}, opts::{ApiOpts, ListFormatOpts, WasmerEnv}, utils::render::{ItemFormat, ListFormat}, }; -use colored::Colorize; use dialoguer::theme::ColorfulTheme; use is_terminal::IsTerminal; -use std::{env::current_dir, path::PathBuf, str::FromStr}; -use wasmer_api::WasmerClient; +use std::path::PathBuf; /// Reveal the value of an existing secret related to an Edge app. #[derive(clap::Parser, Debug)] pub struct CmdAppSecretsReveal { - /// The name of the secret to get the value of. - #[clap(name = "name")] - pub secret_name: Option, - - #[clap(flatten)] - pub app_id: AppIdentFlag, - - /// The path to the directory where the config file for the application will be written to. - #[clap(long = "app-dir", conflicts_with = "app")] - pub app_dir_path: Option, - - /// Reveal all the secrets related to an app. - #[clap(long, conflicts_with = "name")] - pub all: bool, - - /* --- Common args --- */ + /* --- Common flags --- */ #[clap(flatten)] #[allow(missing_docs)] pub api: ApiOpts, @@ -49,77 +29,26 @@ pub struct CmdAppSecretsReveal { #[clap(flatten)] pub fmt: Option, -} -impl CmdAppSecretsReveal { - async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { - if let Some(app_id) = &self.app_id.app { - let app = app_id.resolve(client).await?; - return Ok(app.id.into_inner()); - } - - let app_dir_path = if let Some(app_dir_path) = &self.app_dir_path { - app_dir_path.clone() - } else { - current_dir()? - }; + /* --- Flags --- */ + #[clap(flatten)] + pub app_id: AppIdentFlag, - if let Ok(r) = get_app_config_from_dir(&app_dir_path) { - let (app, _) = r; + /// The path to the directory where the config file for the application will be written to. + #[clap(long = "app-dir", conflicts_with = "app")] + pub app_dir_path: Option, - let app_name = if let Some(owner) = &app.owner { - format!("{owner}/{}", app.name) - } else { - app.name.to_string() - }; - - let id = if let Some(id) = &app.app_id { - Some(id.clone()) - } else if let Ok(app_ident) = AppIdent::from_str(&app_name) { - if let Ok(app) = app_ident.resolve(client).await { - Some(app.id.into_inner()) - } else { - if !self.quiet { - eprintln!("{}: the app found in {} does not exist.\n{}: maybe it was not deployed yet?", - "Warning".bold().yellow(), - format!("'{}'", app_dir_path.display()).dimmed(), - "Hint".bold()); - } - None - } - } else { - None - }; - - if let Some(id) = id { - if !self.quiet { - if let Some(owner) = &app.owner { - eprintln!( - "Managing secrets related to app {} ({owner}).", - app.name.bold() - ); - } else { - eprintln!("Managing secrets related to app {}.", app.name.bold()); - } - } - return Ok(id); - } - } else if let Some(path) = &self.app_dir_path { - anyhow::bail!( - "No app configuration file found in path {}.", - path.display() - ) - } + /// Reveal all the secrets related to an app. + #[clap(long, conflicts_with = "name")] + pub all: bool, - if self.non_interactive { - anyhow::bail!("No app id given. Provide one as a positional argument.") - } else { - let id = prompt_app_ident("Enter the name of the app")?; - let app = id.resolve(client).await?; - Ok(app.id.into_inner()) - } - } + /* --- Parameters --- */ + /// The name of the secret to get the value of. + #[clap(name = "name")] + pub secret_name: Option, +} +impl CmdAppSecretsReveal { fn get_secret_name(&self) -> anyhow::Result { if let Some(name) = &self.secret_name { return Ok(name.clone()); @@ -142,7 +71,14 @@ impl AsyncCliCommand for CmdAppSecretsReveal { async fn run_async(self) -> Result { let client = self.api.client()?; - let app_id = self.get_app_id(&client).await?; + let app_id = super::utils::get_app_id( + &client, + self.app_id.app.as_ref(), + self.app_dir_path.as_ref(), + self.quiet, + self.non_interactive, + ) + .await?; if !self.all { let name = self.get_secret_name()?; diff --git a/lib/cli/src/commands/app/secrets/update.rs b/lib/cli/src/commands/app/secrets/update.rs index c707e9fce40..45f84677743 100644 --- a/lib/cli/src/commands/app/secrets/update.rs +++ b/lib/cli/src/commands/app/secrets/update.rs @@ -1,9 +1,6 @@ use super::utils::Secret; use crate::{ - commands::{ - app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent, AppIdentFlag}, - AsyncCliCommand, - }, + commands::{app::util::AppIdentFlag, AsyncCliCommand}, opts::{ApiOpts, WasmerEnv}, }; use anyhow::Context; @@ -12,26 +9,33 @@ use dialoguer::theme::ColorfulTheme; use is_terminal::IsTerminal; use std::{ collections::HashSet, - env::current_dir, path::{Path, PathBuf}, - str::FromStr, }; use wasmer_api::WasmerClient; /// Update an existing secret related to an Edge app. #[derive(clap::Parser, Debug)] pub struct CmdAppSecretsUpdate { - /// The path to the directory where the config file for the application will be written to. - #[clap(long = "app-dir", conflicts_with = "app-id")] - pub app_dir_path: Option, + /* --- Common args --- */ + #[clap(flatten)] + #[allow(missing_docs)] + pub api: ApiOpts, - /// The name of the secret to update. - #[clap(name = "name")] - pub secret_name: Option, + #[clap(flatten)] + pub env: WasmerEnv, - /// The value of the secret to update. - #[clap(name = "value")] - pub secret_value: Option, + /// Don't print any message. + #[clap(long)] + pub quiet: bool, + + /// Do not prompt for user input. + #[clap(long, default_value_t = !std::io::stdin().is_terminal())] + pub non_interactive: bool, + + /* --- Flags --- */ + /// The path to the directory where the config file for the application will be written to. + #[clap(long = "app-dir", conflicts_with = "app")] + pub app_dir_path: Option, #[clap(flatten)] pub app_id: AppIdentFlag, @@ -45,21 +49,14 @@ pub struct CmdAppSecretsUpdate { )] pub from_file: Option, - /* --- Common args --- */ - #[clap(flatten)] - #[allow(missing_docs)] - pub api: ApiOpts, - - #[clap(flatten)] - pub env: WasmerEnv, - - /// Don't print any message. - #[clap(long)] - pub quiet: bool, + /* --- Parameters --- */ + /// The name of the secret to update. + #[clap(name = "name")] + pub secret_name: Option, - /// Do not prompt for user input. - #[clap(long, default_value_t = !std::io::stdin().is_terminal())] - pub non_interactive: bool, + /// The value of the secret to update. + #[clap(name = "value")] + pub secret_value: Option, } impl CmdAppSecretsUpdate { @@ -93,75 +90,6 @@ impl CmdAppSecretsUpdate { } } - async fn get_app_id(&self, client: &WasmerClient) -> anyhow::Result { - if let Some(app_id) = &self.app_id.app { - let app = app_id.resolve(client).await?; - return Ok(app.id.into_inner()); - } - - let app_dir_path = if let Some(app_dir_path) = &self.app_dir_path { - app_dir_path.clone() - } else { - current_dir()? - }; - - if let Ok(r) = get_app_config_from_dir(&app_dir_path) { - let (app, _) = r; - - let app_name = if let Some(owner) = &app.owner { - format!("{owner}/{}", app.name) - } else { - app.name.to_string() - }; - - let id = if let Some(id) = &app.app_id { - Some(id.clone()) - } else if let Ok(app_ident) = AppIdent::from_str(&app_name) { - if let Ok(app) = app_ident.resolve(client).await { - Some(app.id.into_inner()) - } else { - if !self.quiet { - eprintln!("{}: the app found in {} does not exist.\n{}: maybe it was not deployed yet?", - "Warning".bold().yellow(), - format!("'{}'", app_dir_path.display()).dimmed(), - "Hint".bold()); - } - - None - } - } else { - None - }; - - if let Some(id) = id { - if !self.quiet { - if let Some(owner) = &app.owner { - eprintln!( - "Managing secrets related to app {} ({owner}).", - app.name.bold() - ); - } else { - eprintln!("Managing secrets related to app {}.", app.name.bold()); - } - } - return Ok(id); - } - } else if let Some(path) = &self.app_dir_path { - anyhow::bail!( - "No app configuration file found in path {}.", - path.display() - ) - } - - if self.non_interactive { - anyhow::bail!("No app id given. Provide one as a positional argument.") - } else { - let id = prompt_app_ident("Enter the name of the app")?; - let app = id.resolve(client).await?; - Ok(app.id.into_inner()) - } - } - /// Given a list of secrets, checks if the given secrets already exist for the given app and /// returns a list of secrets that must be upserted. async fn filter_secrets( @@ -226,6 +154,11 @@ impl CmdAppSecretsUpdate { for secret in secrets { eprintln!("{}", secret.name.bold()); } + + eprintln!( + "{}: In order for secrets to appear in your app, re-deploy it.", + "Info".bold() + ); } Ok(()) @@ -253,7 +186,15 @@ impl AsyncCliCommand for CmdAppSecretsUpdate { async fn run_async(self) -> Result { let client = self.api.client()?; - let app_id = self.get_app_id(&client).await?; + let app_id = super::utils::get_app_id( + &client, + self.app_id.app.as_ref(), + self.app_dir_path.as_ref(), + self.quiet, + self.non_interactive, + ) + .await?; + if let Some(file) = &self.from_file { self.update_from_file(file, &app_id).await } else { diff --git a/lib/cli/src/commands/app/secrets/utils/mod.rs b/lib/cli/src/commands/app/secrets/utils/mod.rs index e9d386857f2..745a716dd9a 100644 --- a/lib/cli/src/commands/app/secrets/utils/mod.rs +++ b/lib/cli/src/commands/app/secrets/utils/mod.rs @@ -1,9 +1,15 @@ pub(crate) mod render; use colored::Colorize; -use std::path::Path; +use std::{ + env::current_dir, + path::{Path, PathBuf}, + str::FromStr, +}; use wasmer_api::{types::Secret as BackendSecret, WasmerClient}; +use crate::commands::app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent}; + #[derive(serde::Serialize, serde::Deserialize)] pub(super) struct Secret { pub name: String, @@ -82,3 +88,78 @@ impl From for BackendSecretWrapper { Self(value) } } + +/// A secrets-specific app to retrieve an app identifier. +pub(super) async fn get_app_id( + client: &WasmerClient, + app: Option<&AppIdent>, + app_dir_path: Option<&PathBuf>, + quiet: bool, + non_interactive: bool, +) -> anyhow::Result { + if let Some(app_id) = app { + let app = app_id.resolve(client).await?; + return Ok(app.id.into_inner()); + } + + let path = if let Some(path) = app_dir_path { + path.clone() + } else { + current_dir()? + }; + + if let Ok(r) = get_app_config_from_dir(&path) { + let (app, _) = r; + + let app_name = if let Some(owner) = &app.owner { + format!("{owner}/{}", app.name) + } else { + app.name.to_string() + }; + + let id = if let Some(id) = &app.app_id { + Some(id.clone()) + } else if let Ok(app_ident) = AppIdent::from_str(&app_name) { + if let Ok(app) = app_ident.resolve(client).await { + Some(app.id.into_inner()) + } else { + if !quiet { + eprintln!("{}: the app found in {} does not exist.\n{}: maybe it was not deployed yet?", + "Warning".bold().yellow(), + format!("'{}'", path.display()).dimmed(), + "Hint".bold()); + } + None + } + } else { + None + }; + + if let Some(id) = id { + if !quiet { + if let Some(owner) = &app.owner { + eprintln!( + "Managing secrets related to app {} ({owner}).", + app.name.bold() + ); + } else { + eprintln!("Managing secrets related to app {}.", app.name.bold()); + } + } + return Ok(id); + } + } else if let Some(path) = app_dir_path { + anyhow::bail!( + "No app configuration file found in path {}.", + path.display() + ) + } + + if non_interactive { + anyhow::bail!("No app id given. Provide one using the `--app` flag.") + } else { + let id = prompt_app_ident("Enter the name of the app")?; + let app = id.resolve(client).await?; + Ok(app.id.into_inner()) + } +} From 381731542fd6e3d90ccab8ef3fd60d8285be4080 Mon Sep 17 00:00:00 2001 From: Edoardo Marangoni Date: Tue, 16 Jul 2024 17:04:38 +0200 Subject: [PATCH 12/12] fix(cli/secrets): Minor changes --- lib/cli/src/commands/app/util.rs | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/cli/src/commands/app/util.rs b/lib/cli/src/commands/app/util.rs index b9692b72b8e..362d30a4989 100644 --- a/lib/cli/src/commands/app/util.rs +++ b/lib/cli/src/commands/app/util.rs @@ -117,19 +117,6 @@ pub struct AppIdentOpts { pub app: Option, } -/// A utility struct used by commands that need the [`AppIdent`] as a flag. -#[derive(clap::Parser, Debug)] -pub struct AppIdentFlag { - /// Identifier of the application. - /// - /// Valid input: - /// - namespace/app-name - /// - app-alias - /// - App ID - #[clap(long)] - pub app: Option, -} - // Allowing because this is not performance-critical at all. #[allow(clippy::large_enum_variant)] pub enum ResolvedAppIdent { @@ -210,6 +197,22 @@ mod tests { } } +/// A utility struct used by commands that need the [`AppIdent`] as a flag. +/// +/// NOTE: Differently from [`AppIdentOpts`], the use of this struct does not entail searching the +/// current directory for an `app.yaml` if not specified. +#[derive(clap::Parser, Debug)] +pub struct AppIdentFlag { + /// Identifier of the application. + /// + /// Valid input: + /// - namespace/app-name + /// - app-alias + /// - App ID + #[clap(long)] + pub app: Option, +} + pub(super) async fn login_user( api: &ApiOpts, env: &WasmerEnv,