Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Ask cli #1457

Merged
merged 17 commits into from
Jul 16, 2024
Merged

Ask cli #1457

merged 17 commits into from
Jul 16, 2024

Conversation

jreidinger
Copy link
Contributor

@jreidinger jreidinger commented Jul 12, 2024

Problem

There is no CLI command to ask new question. It is needed to ask questions from autoyast convert script.
Also there is a need to share functionality between various CLI calls that will use HTTP API as current one in network has limitation and is not designed as generic http agent that fits agama.

Solution

Implement needed functionality in questions http client.
Implement BaseHttpClient that shares code for doing http requests on agama http server.
Implement ask and list subcommand for questions to allow asking questions and also listing currently unanswered ones.

Copy link
Contributor

@imobachgs imobachgs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from some minor naming issues (you know, naming is hard) and some missing docs, I think you are on the right track.

async fn run_command(cli: Cli) -> anyhow::Result<()> {
match cli.command {
async fn run_command(cli: Cli) -> Result<(), ServiceError> {
Ok(match cli.command {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am unsure whether I would like to put the full match inside the Ok().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can use let result = match and then Ok(result)

rust/agama-lib/src/questions/http_client.rs Show resolved Hide resolved
impl HTTPClient {
pub async fn new() -> Result<Self, ServiceError> {
Ok(Self {
client: crate::http_client::HTTPClient::new().await?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding a good name and importing it is a better (and more readable) approach IMHO.


const API_URL: &str = "http://localhost/api";

impl HTTPClient {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, document this client with some examples.


let mut headers = header::HeaderMap::new();
let value = header::HeaderValue::from_str(format!("Bearer {}", token).as_str())
.map_err(|e| ServiceError::NetworkClientError(e.to_string()))?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NetworkClientError sounds like specific to the network scope only.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, my fault. It should be generic one. Thanks for catch ( c&p error )

}

const NO_TEXT: &'static str = "No text";
// Simple wrapper around Response to get target type.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What "target type" means?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is basically to get object you expect from backend. Something like let questions : Vec<Question> = client.get_type("/questions")

// For more advanced usage use directly get method
pub async fn get_type<T: DeserializeOwned>(&self, path: &str) -> Result<T, ServiceError> {
let response = self.get(path).await?;
if !response.status().is_success() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

np: What about returning early if is_success()? I am saying just because the error handling seems to be longer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW one more idea I had...I know that we use 400 for error and in body we use json for ServiceError...so do you think it make sense to have something like ErrorFromBackend that gets as argument ServiceError from response?

.map_err(|e| e.into())
}

fn target_path(&self, path: &str) -> String {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about using request_url? (or just url)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fine for me...url is probably better as it is internal only method..so shorter is better.

const NO_TEXT: &'static str = "No text";
// Simple wrapper around Response to get target type.
// For more advanced usage use directly get method
pub async fn get_type<T: DeserializeOwned>(&self, path: &str) -> Result<T, ServiceError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea but get_type is kind of confusing (am I getting the "type"?).

In any case, I would call this method get and rename the get method below to something like get_raw as I expect us to use this more often.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed. I am even thinking about naming this one as get_object but it is right, that plain get here and maybe get_response would be better

/// is that it contain both question and answer. It works for dbus
/// API which has both as attributes, but web API separate
/// question and its answer. So here it is split into GenericQuestion
/// and GenericAnswer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should consider refactoring the GenericQuestion from lib to be a structure composed of question and answer structs from this module. This is just an idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think whole questions DBus service needs a bit refactoring as this code does not make me proud to be author :)

Copy link
Contributor

@imobachgs imobachgs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general it is OK. Just some comments about the documentation.

let mut headers = header::HeaderMap::new();
// just use generic anyhow error here as Bearer format is constructed by us, so failures can come only from token
let value = header::HeaderValue::from_str(format!("Bearer {}", token).as_str())
.map_err(|e| anyhow::Error::new(e))?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still no convinced of using anyhow, especially in a library. But we can postpone that until we refactor the ServiceError stuff.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

me neither...I basically use it for trash error which should not happen instead of unwrap

@@ -19,6 +19,8 @@ pub enum QuestionsCommands {
/// Path to a file containing the answers in YAML format.
path: String,
},
/// prints list of questions that is waiting for answer in YAML format
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether we should use JSON for everything.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is fine for me if agreed on it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reason why I use yaml for it is that in answers file we used for automatic answers we also use yaml.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I know, we use JSON for profiles and YAML for the answers file. I am not saying that we should fix it now, but let's have into account in the future.

rust/agama-lib/src/base_http_client.rs Outdated Show resolved Hide resolved
rust/agama-lib/src/base_http_client.rs Outdated Show resolved Hide resolved
})
}

const NO_TEXT: &'static str = "No text";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about the "No text" value. Why not an empty string?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To see that error from backend does not provide any text. For me less confusing to write explicitelly that no text received from backend.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the const definition closer to the method that uses it. That also makes it more apparent what it should say

rust/agama-lib/src/base_http_client.rs Show resolved Hide resolved
rust/agama-lib/src/base_http_client.rs Show resolved Hide resolved
.map_err(|e| e.into())
}

pub async fn build_backend_error(&self, response: Response) -> ServiceError {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps you might want to implement something like From<Response> (or TryFrom).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be asynchronous? As it is not simple conversion but it also ask for text.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it is a bit against documentation of traits which mention that it should be obvious to convert...which I am not sure about this case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, async could be a problem. Let's keep it as it is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About when to implement those traits, I am usually unsure 😅

rust/agama-lib/src/error.rs Outdated Show resolved Hide resolved
rust/agama-lib/src/questions.rs Outdated Show resolved Hide resolved
@jreidinger jreidinger marked this pull request as ready for review July 16, 2024 11:59
Copy link
Contributor

@mvidner mvidner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here reviewing just the BaseHTTPClient part

rust/agama-lib/src/base_http_client.rs Outdated Show resolved Hide resolved
impl BaseHTTPClient {
// if there is need for client without authorization, create new constructor for it
pub fn new() -> Result<Self, ServiceError> {
let token = AuthToken::find().context("You are not logged in")?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message is correct (I hope) but even better would be a hint how to fix the situation: "... Try: agama auth login"?

let text = response
.text()
.await
.unwrap_or_else(|_| Self::NO_TEXT.to_string());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"No text" -> "(failed to extract error text from HTTP response)" is perhaps too verbose but I would appreciate it when debugging it as an operator

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I check quite deeply when call to text can fail and to be honest I do not find it either in documentation neither from code. So lets use your suggestion.

})
}

const NO_TEXT: &'static str = "No text";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the const definition closer to the method that uses it. That also makes it more apparent what it should say

}

/// post object to given path and report error if response is not success
pub async fn post(&self, path: &str, object: &impl Serialize) -> Result<(), ServiceError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stupid question, is it normal that a successful HTTP POST returns no useful text? At least in Agama?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, response is optional in POST. I check what we have in agama-web-server openapi and it is like half to half if we return something or not...if we need it more often we can create variant that also return object...I am just not sure what will be good name :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, a POST call does not need to return anything. I am not convinced about using "no text" string. Why not just return an empty body? In any case, we can evolve this API when we use it.

rust/agama-lib/src/error.rs Outdated Show resolved Hide resolved
rust/agama-lib/src/error.rs Outdated Show resolved Hide resolved
rust/package/agama.changes Outdated Show resolved Hide resolved
jreidinger and others added 2 commits July 16, 2024 14:51
Co-authored-by: Martin Vidner <mvidner@suse.cz>
Copy link
Contributor

@imobachgs imobachgs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just minor comments. Feel free to merge.

@@ -19,6 +19,8 @@ pub enum QuestionsCommands {
/// Path to a file containing the answers in YAML format.
path: String,
},
/// prints list of questions that is waiting for answer in YAML format
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I know, we use JSON for profiles and YAML for the answers file. I am not saying that we should fix it now, but let's have into account in the future.

async fn list_questions() -> Result<(), ServiceError> {
let client = HTTPClient::new().await?;
let questions = client.list_questions().await?;
// FIXME: that conversion to anyhow error is nasty, but we do not expect issue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could have a generic error for these cases. But let's keep it as it is now. At some point we should improve our error types.

rust/agama-lib/src/base_http_client.rs Outdated Show resolved Hide resolved
.map_err(|e| e.into())
}

pub async fn build_backend_error(&self, response: Response) -> ServiceError {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, async could be a problem. Let's keep it as it is.

jreidinger and others added 2 commits July 16, 2024 15:53
Co-authored-by: Imobach González Sosa <igonzalezsosa@suse.com>
@jreidinger jreidinger merged commit fce3d4c into master Jul 16, 2024
2 checks passed
@jreidinger jreidinger deleted the ask_cli branch July 16, 2024 19:06
@imobachgs imobachgs mentioned this pull request Sep 20, 2024
imobachgs added a commit that referenced this pull request Sep 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants