Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add support to run pre/post scripts #1673

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open

Conversation

imobachgs
Copy link
Contributor

@imobachgs imobachgs commented Oct 16, 2024

This is the first implementation of pre/post-installation scripts. They are not as capable as their AutoYaST counterparts, but the idea is to evolve them in the future.

Specifying a script

Warning

JSON does not support multiline values (a.k.a. blocks) but Jsonnet does it. Perhaps we should consider using Jsonnet in the agama config edit command.

{
  scripts: {
    pre: [
      {
         name: "say-hi",
         body: |||
           #!/bin/bash
           echo "before starting the installation"
         |||
      }
    ]
  }
}

Note

What about having a plain list where each script has a type (or group) like pre, post, etc.?

When scripts get executed

  • Pre-scripts: when loading a profile.
  • Post-scripts: after the installation.

Implementation details

The scripting support is written in Rust and does not use the YaST scripts code anymore. We will rewrite the Manager in Rust (at least the Agama-specific part) in the future, and the scripting support will be part of that.

Future

Agama scripts will gain additional features soon:

  • Running in a chroot.
  • Better error handling if the shebang line is not present.
  • Running after the first boot.

Pending tasks

  • Extend the JSON schema
  • Copy the resulting files to the installed system
  • Add some logging
  • Clean-up the old scripts when reloading a profile

@coveralls
Copy link

coveralls commented Oct 16, 2024

Pull Request Test Coverage Report for Build 11368553812

Details

  • 47 of 126 (37.3%) changed or added relevant lines in 7 files are covered.
  • 4 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-0.2%) to 70.862%

Changes Missing Coverage Covered Lines Changed/Added Lines %
rust/agama-server/src/web.rs 0 1 0.0%
rust/agama-lib/src/store.rs 0 4 0.0%
rust/agama-lib/src/scripts/model.rs 32 37 86.49%
rust/agama-lib/src/scripts/client.rs 0 10 0.0%
rust/agama-server/src/scripts/web.rs 0 27 0.0%
rust/agama-lib/src/scripts/store.rs 0 32 0.0%
Files with Coverage Reduction New Missed Lines %
rust/agama-lib/src/store.rs 1 0.0%
rust/agama-server/src/web.rs 1 0.0%
service/lib/agama/storage/finisher.rb 2 98.33%
Totals Coverage Status
Change from base Build 11364775644: -0.2%
Covered Lines: 16126
Relevant Lines: 22757

💛 - Coveralls

@imobachgs imobachgs marked this pull request as ready for review October 16, 2024 15:12
Copy link
Contributor

@lslezak lslezak left a comment

Choose a reason for hiding this comment

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

Just some minor comments, looks good to me.

require "fileutils"
logs_dir = File.join(Yast::Installation.destdir, "var", "log", "agama")
FileUtils.mkdir_p(logs_dir)
FileUtils.cp_r(SCRIPTS_DIR, logs_dir)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should document that the user scripts are copied to the logs in the installed system. The user scripts might contain sensitive data (passwords, tokens, keys, certificates,...) and the users should be aware of this when attaching the logs to bugzilla or sending via email.

"properties": {
"pre": {
"title": "Pre-installation scripts",
"description": "User-defined scripts to run before the installation starts",
Copy link
Contributor

@lslezak lslezak Oct 17, 2024

Choose a reason for hiding this comment

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

Um, when exactly? Nothing done yet?

I mean later we might add a "post-partitioning" script so it should be clean whether at this point something is already done by Agama or the system has not been touched yet.


it "copies the artifacts to the installed system" do
storage.finish
expect(File).to exist(File.join(tmp_dir, "mnt", "var", "log", "agama", "scripts"))
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if the test should really touch the testing system (create and copy files). I do not see much advantage over just to expect to call FileUtils.cp_r.

self.client.post_void("/scripts/run", &group).await
}

pub async fn scripts(&self) -> Result<Vec<Script>, ServiceError> {
Copy link
Contributor

Choose a reason for hiding this comment

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

documentation missing


pub async fn scripts(&self) -> Result<Vec<Script>, ServiceError> {
let scripts = self.client.get("/scripts").await?;
Ok(scripts)
Copy link
Contributor

Choose a reason for hiding this comment

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

cannot it be simple oneliner self.client.get("/scripts").await ?

use super::ScriptError;

#[derive(Debug, Clone, Copy, PartialEq, strum::Display, Serialize, Deserialize)]
#[strum(serialize_all = "camelCase")]
Copy link
Contributor

Choose a reason for hiding this comment

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

I will need to read a bit more about strum. First time I met it.

/// Script's body. Either the body or the URL must be specified.
pub body: Option<String>,
/// URL to get the script from. Either the body or the URL must be specified.
pub url: Option<String>,
Copy link
Contributor

Choose a reason for hiding this comment

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

this looks like bad representation in rust. I kind of expect here enum with either body or url content (which are just marked string). I am just not sure how hard it will be to express it in serde.

} else if let Some(body) = &self.body {
write!(file, "{}", &body)?;
}
// FIXME: else: invalid script definition
Copy link
Contributor

Choose a reason for hiding this comment

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

as said above, should be already catched by json serializer and not here in code where you can rely on valid code.


/// Runs the scripts in the given group.
///
/// They run in the order they were added to the repository.
Copy link
Contributor

Choose a reason for hiding this comment

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

I would document here that it does not return error if script failed. and in this case just log error.

let body: Vec<u8> = std::fs::read(&path).unwrap();
let body = String::from_utf8(body).unwrap();
assert_eq!("error\n", body);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 for tests

pub body: Option<String>,
/// URL to get the script from. Either the body or the URL must be specified.
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
Copy link
Contributor

Choose a reason for hiding this comment

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

I still hope that serde can handle if we have it as enum...especially as it is really exclusive as having both body and url does not make sense.

}

pub async fn load(&self) -> Result<ScriptsConfig, ServiceError> {
// TODO: Ok(self.client.scripts().await.unwrap())
Copy link
Contributor

Choose a reason for hiding this comment

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

? I do not get this todo

}

pub fn new_with_client(client: ScriptsClient) -> Result<Self, ServiceError> {
Ok(Self { client })
Copy link
Contributor

Choose a reason for hiding this comment

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

how it can fail? this looks kind of strange.

.await?;
}

self.client.run_scripts(ScriptsGroup::Pre).await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I am not sure I like it. Separation of load and store is understandable, but why it runs pre script when store? This looks kind of unexpected.

url: script.url.clone(),
body: script.body.clone(),
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I have to say that this to_something looks kind of strange in rust. What is reason to not implement trait Into?

) -> Result<(), ScriptServiceError> {
let scripts = state.scripts.write().await;
if let Err(error) = scripts.run(group).await {
tracing::error!("Could not run user-defined scripts: {error}");
Copy link
Contributor

Choose a reason for hiding this comment

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

so it means that it will return 200 when coulld not run scripts? I do not feel it is good behavior.

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.

4 participants