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

Allow accessing the web UI from webpack #1074

Merged
merged 4 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions rust/Cargo.lock

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

2 changes: 1 addition & 1 deletion rust/agama-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ macaddr = "1.0"
async-trait = "0.1.75"
axum = { version = "0.7.4", features = ["ws"] }
serde_json = "1.0.113"
tower-http = { version = "0.5.1", features = ["compression-br", "trace"] }
tower-http = { version = "0.5.1", features = ["compression-br", "fs", "trace"] }
tracing-subscriber = "0.3.18"
tracing-journald = "0.3.0"
tracing = "0.1.40"
Expand Down
24 changes: 22 additions & 2 deletions rust/agama-server/src/agama-web-server.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::process::{ExitCode, Termination};
use std::{
path::{Path, PathBuf},
process::{ExitCode, Termination},
};

use agama_lib::connection_to;
use agama_server::{
Expand All @@ -10,6 +13,8 @@ use tokio::sync::broadcast::channel;
use tracing_subscriber::prelude::*;
use utoipa::OpenApi;

const DEFAULT_WEB_UI_DIR: &'static str = "/usr/share/agama/web_ui";

#[derive(Subcommand, Debug)]
enum Commands {
/// Start the API server.
Expand All @@ -27,6 +32,9 @@ pub struct ServeArgs {
// Agama D-Bus address
#[arg(long, default_value = "unix:path=/run/agama/bus")]
dbus_address: String,
// Directory containing the web UI code.
#[arg(long)]
web_ui_dir: Option<PathBuf>,
}

#[derive(Parser, Debug)]
Expand All @@ -39,6 +47,17 @@ struct Cli {
pub command: Commands,
}

fn find_web_ui_dir() -> PathBuf {
if let Ok(home) = std::env::var("HOME") {
let path = Path::new(&home).join(".local/share/agama");
if path.exists() {
return path;
}
}

Path::new(DEFAULT_WEB_UI_DIR).into()
}

/// Start serving the API.
///
/// `args`: command-line arguments.
Expand All @@ -55,7 +74,8 @@ async fn serve_command(args: ServeArgs) -> anyhow::Result<()> {

let config = web::ServiceConfig::load()?;
let dbus = connection_to(&args.dbus_address).await?;
let service = web::service(config, tx, dbus).await?;
let web_ui_dir = args.web_ui_dir.unwrap_or(find_web_ui_dir());
let service = web::service(config, tx, dbus, web_ui_dir).await?;
axum::serve(listener, service)
.await
.expect("could not mount app on listener");
Expand Down
15 changes: 11 additions & 4 deletions rust/agama-server/src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,25 @@ pub use config::ServiceConfig;
pub use docs::ApiDoc;
pub use event::{Event, EventsReceiver, EventsSender};
pub use service::MainServiceBuilder;
use std::path::Path;
use tokio_stream::StreamExt;

/// Returns a service that implements the web-based Agama API.
///
/// * `config`: service configuration.
/// * `events`: D-Bus connection.
pub async fn service(
/// * `events`: channel to send the events through the WebSocket.
/// * `dbus`: D-Bus connection.
/// * `web_ui_dir`: public directory containing the web UI.
pub async fn service<P>(
config: ServiceConfig,
events: EventsSender,
dbus: zbus::Connection,
) -> Result<Router, ServiceError> {
let router = MainServiceBuilder::new(events.clone())
web_ui_dir: P,
) -> Result<Router, ServiceError>
where
P: AsRef<Path>,
{
let router = MainServiceBuilder::new(events.clone(), web_ui_dir)
.add_service("/l10n", l10n_service(events.clone()))
.add_service("/software", software_service(dbus).await?)
.with_config(config)
Expand Down
50 changes: 41 additions & 9 deletions rust/agama-server/src/web/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,65 @@ use axum::{
routing::{get, post},
Router,
};
use std::convert::Infallible;
use std::{
convert::Infallible,
path::{Path, PathBuf},
};
use tower::Service;
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
use tower_http::{compression::CompressionLayer, services::ServeDir, trace::TraceLayer};

/// Builder for Agama main service.
///
/// It is responsible for building an axum service which includes:
///
/// * A static assets directory (`public_dir`).
/// * A websocket at the `/ws` path.
/// * An authentication endpointg at `/authenticate`.
imobachgs marked this conversation as resolved.
Show resolved Hide resolved
/// * A 'ping' endpoint at '/ping'.
/// * A number of authenticated services that are added using the `add_service` function.
pub struct MainServiceBuilder {
config: ServiceConfig,
events: EventsSender,
router: Router<ServiceState>,
api_router: Router<ServiceState>,
public_dir: PathBuf,
}

impl MainServiceBuilder {
pub fn new(events: EventsSender) -> Self {
let router = Router::new().route("/ws", get(super::ws::ws_handler));
/// Returns a new service builder.
///
/// * `events`: channel to send events through the WebSocket.
/// * `public_dir`: path to the public directory.
pub fn new<P>(events: EventsSender, public_dir: P) -> Self
where
P: AsRef<Path>,
{
let api_router = Router::new().route("/ws", get(super::ws::ws_handler));
let config = ServiceConfig::default();

Self {
events,
router,
api_router,
config,
public_dir: PathBuf::from(public_dir.as_ref()),
}
}

pub fn with_config(self, config: ServiceConfig) -> Self {
Self { config, ..self }
}

/// Add an authenticated service.
///
/// * `path`: Path to mount the service under `/api`.
/// * `service`: Service to mount on the given `path`.
pub fn add_service<T>(self, path: &str, service: T) -> Self
where
T: Service<Request, Error = Infallible> + Clone + Send + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,
{
Self {
router: self.router.nest_service(path, service),
api_router: self.api_router.nest_service(path, service),
..self
}
}
Expand All @@ -49,12 +74,19 @@ impl MainServiceBuilder {
config: self.config,
events: self.events,
};
self.router

let api_router = self
.api_router
.route_layer(middleware::from_extractor_with_state::<TokenClaims, _>(
state.clone(),
))
.route("/ping", get(super::http::ping))
.route("/authenticate", post(super::http::authenticate))
.route("/authenticate", post(super::http::authenticate));

let serve = ServeDir::new(self.public_dir);
Router::new()
.nest_service("/", serve)
.nest("/api", api_router)
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new().br(true))
.with_state(state)
Expand Down
26 changes: 19 additions & 7 deletions rust/agama-server/tests/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,34 @@ use axum::{
Router,
};
use common::{body_to_string, DBusServer};
use std::error::Error;
use std::{error::Error, path::PathBuf};
use tokio::{sync::broadcast::channel, test};
use tower::ServiceExt;

async fn build_service() -> Router {
let (tx, _) = channel(16);
let server = DBusServer::new().start().await.unwrap();
service(ServiceConfig::default(), tx, server.connection())
.await
.unwrap()
service(
ServiceConfig::default(),
tx,
server.connection(),
public_dir(),
)
.await
.unwrap()
}

fn public_dir() -> PathBuf {
std::env::current_dir().unwrap().join("public")
}

#[test]
async fn test_ping() -> Result<(), Box<dyn Error>> {
let web_service = build_service().await;
let request = Request::builder().uri("/ping").body(Body::empty()).unwrap();
let request = Request::builder()
.uri("/api/ping")
.body(Body::empty())
.unwrap();

let response = web_service.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
Expand All @@ -46,13 +58,13 @@ async fn access_protected_route(token: &str, jwt_secret: &str) -> Response {
jwt_secret: jwt_secret.to_string(),
};
let (tx, _) = channel(16);
let web_service = MainServiceBuilder::new(tx)
let web_service = MainServiceBuilder::new(tx, public_dir())
.add_service("/protected", get(protected))
.with_config(config)
.build();

let request = Request::builder()
.uri("/protected")
.uri("/api/protected")
.method(Method::GET)
.header("Authorization", format!("Bearer {}", token))
.body(Body::empty())
Expand Down
45 changes: 17 additions & 28 deletions web/README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
# Agama Web-Based UI
# Agama Web UI

This Cockpit modules offers a UI to the [Agama service](file:../service). The code is based on
[Cockpit's Starter Kit
(b2379f7)](https://github.com/cockpit-project/starter-kit/tree/b2379f78e203aab0028d8548b39f5f0bd2b27d2a).
The Agama web user interface is a React-based application that offers a user
interface to the [Agama service](file:../service).

## Development

TODO: update when new way is clear how to do
There are basically two ways how to develop the Agama fronted. You can
override the original Cockpit plugins with your own code in your `$HOME` directory
or you can run a development server which works as a proxy and sends the Cockpit
requests to a real Cockpit server.

The advantage of using the development server is that you can use the
[Hot Module Replacement](https://webpack.js.org/concepts/hot-module-replacement/)
feature for automatically updating the code and stylesheet in the browser
without reloading the page.
The easiest way to work on the Agama Web UI is to the development server to
serve the code. The advantage is that you can use the [Hot Module Replacement]
imobachgs marked this conversation as resolved.
Show resolved Hide resolved
(https://webpack.js.org/concepts/hot-module-replacement/) feature for
automatically updating the code and stylesheet in the browser without reloading
the page.

### Using a development server

TODO: update when new way is clear how to do
To start the [webpack-dev-server](https://github.com/webpack/webpack-dev-server)
use this command:

Expand All @@ -29,28 +22,25 @@ use this command:

The extra `--open` option automatically opens the server page in your default
web browser. In this case the server will use the `https://localhost:8080` URL
and expects a running Cockpit instance at `https://localhost:9090`.

At the first start the development server generates a self-signed SSL
certificate, you have to accept it in the browser. The certificate is saved to
disk and is used in the next runs so you do not have to accept it again.
and expects a running `agama-web-server` at `https://localhost:9090`.

This can work also remotely, with a Agama instance running in a different
machine (a virtual machine as well). In that case run

```
COCKPIT_TARGET=<IP> npm run server -- --open
AGAMA_SERVER=<IP> npm run server -- --open
```

Where `COCKPIT_TARGET` is the IP address or hostname of the running Agama
instance. This is especially useful if you use the Live ISO which does not contain
any development tools, you can develop the web frontend easily from your workstation.
Where `AGAMA_SERVER` is the IP address, the hostname or the full URL of the
running Agama server instance. This is especially useful if you use the Live ISO
which does not contain any development tools, you can develop the web frontend
easily from your workstation.

### Special Environment Variables

`COCKPIT_TARGET` - When running the development server set up a proxy to the
specified Cockpit server. See the [using a development
server](#using-a-development-server) section above.
`AGAMA_SERVER` - When running the development server set up a proxy to
the specified Agama web server. See the [using a development server]
(#using-a-development-server) section above.

`LOCAL_CONNECTION` - Force behaving as in a local connection, useful for
development or testing some Agama features. For example the keyboard layout
Expand Down Expand Up @@ -89,7 +79,6 @@ you want a JavaScript file to be type-checked, please add a `// @ts-check` comme

### Links

- [Cockpit developer documentation](https://cockpit-project.org/guide/latest/development)
- [Webpack documentation](https://webpack.js.org/configuration/)
- [PatternFly documentation](https://www.patternfly.org)
- [Material Symbols (aka icons)](https://fonts.google.com/icons)
Loading
Loading