Compare commits
	
		
			9 Commits
		
	
	
		
			b4f5b91a57
			...
			d9935e20cb
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d9935e20cb | |||
| 7b0f3b79b1 | |||
| e6612245a5 | |||
| d317c0ba76 | |||
| 539b8299ae | |||
| 5a89495c61 | |||
| fb7849c010 | |||
| 6371009c6f | |||
| a4aa685a4f | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,3 +1,4 @@
 | 
			
		||||
target
 | 
			
		||||
private_repos
 | 
			
		||||
log/
 | 
			
		||||
*.tgz
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -1765,6 +1765,7 @@ dependencies = [
 | 
			
		||||
 "strum 0.27.1",
 | 
			
		||||
 "temp-dir",
 | 
			
		||||
 "temp-file",
 | 
			
		||||
 "tempfile",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tokio-util",
 | 
			
		||||
 "url",
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,36 @@
 | 
			
		||||
use std::{path::PathBuf, sync::Arc};
 | 
			
		||||
 | 
			
		||||
use harmony::{
 | 
			
		||||
    inventory::Inventory,
 | 
			
		||||
    maestro::Maestro,
 | 
			
		||||
    modules::application::{RustWebappScore, features::ContinuousDelivery},
 | 
			
		||||
    modules::application::{
 | 
			
		||||
        RustWebFramework, RustWebapp, RustWebappScore, features::ContinuousDelivery,
 | 
			
		||||
    },
 | 
			
		||||
    topology::{K8sAnywhereTopology, Url},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() {
 | 
			
		||||
    env_logger::init();
 | 
			
		||||
    let application = RustWebapp {
 | 
			
		||||
        name: "harmony-example-rust-webapp".to_string(),
 | 
			
		||||
        project_root: PathBuf::from("./examples/rust/webapp"),
 | 
			
		||||
        framework: Some(RustWebFramework::Leptos),
 | 
			
		||||
    };
 | 
			
		||||
    // TODO RustWebappScore should simply take a RustWebApp as config
 | 
			
		||||
    let app = RustWebappScore {
 | 
			
		||||
        name: "Example Rust Webapp".to_string(),
 | 
			
		||||
        domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
 | 
			
		||||
        features: vec![Box::new(ContinuousDelivery {})],
 | 
			
		||||
        features: vec![Box::new(ContinuousDelivery {
 | 
			
		||||
            application: Arc::new(application.clone()),
 | 
			
		||||
        })],
 | 
			
		||||
        application,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let topology = K8sAnywhereTopology::from_env();
 | 
			
		||||
    let mut maestro = Maestro::new(Inventory::autoload(), topology);
 | 
			
		||||
    let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap();
 | 
			
		||||
    maestro.register_all(vec![Box::new(app)]);
 | 
			
		||||
    harmony_cli::init(maestro, None).await.unwrap();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										14
									
								
								examples/rust/webapp/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								examples/rust/webapp/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
# Generated by Cargo
 | 
			
		||||
# will have compiled files and executables
 | 
			
		||||
debug/
 | 
			
		||||
target/
 | 
			
		||||
 | 
			
		||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
 | 
			
		||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
 | 
			
		||||
Cargo.lock
 | 
			
		||||
 | 
			
		||||
# These are backup files generated by rustfmt
 | 
			
		||||
**/*.rs.bk
 | 
			
		||||
 | 
			
		||||
# MSVC Windows builds of rustc generate these, which store debugging information
 | 
			
		||||
*.pdb
 | 
			
		||||
							
								
								
									
										93
									
								
								examples/rust/webapp/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								examples/rust/webapp/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,93 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "harmony-example-rust-webapp"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
[lib]
 | 
			
		||||
crate-type = ["cdylib", "rlib"]
 | 
			
		||||
 | 
			
		||||
[workspace]
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
actix-files = { version = "0.6", optional = true }
 | 
			
		||||
actix-web = { version = "4", optional = true, features = ["macros"] }
 | 
			
		||||
console_error_panic_hook = "0.1"
 | 
			
		||||
http = { version = "1.0.0", optional = true }
 | 
			
		||||
leptos = { version = "0.7.0" }
 | 
			
		||||
leptos_meta = { version = "0.7.0" }
 | 
			
		||||
leptos_actix = { version = "0.7.0", optional = true }
 | 
			
		||||
leptos_router = { version = "0.7.0" }
 | 
			
		||||
wasm-bindgen = "=0.2.100"
 | 
			
		||||
 | 
			
		||||
[features]
 | 
			
		||||
csr = ["leptos/csr"]
 | 
			
		||||
hydrate = ["leptos/hydrate"]
 | 
			
		||||
ssr = [
 | 
			
		||||
  "dep:actix-files",
 | 
			
		||||
  "dep:actix-web",
 | 
			
		||||
  "dep:leptos_actix",
 | 
			
		||||
  "leptos/ssr",
 | 
			
		||||
  "leptos_meta/ssr",
 | 
			
		||||
  "leptos_router/ssr",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
# Defines a size-optimized profile for the WASM bundle in release mode
 | 
			
		||||
[profile.wasm-release]
 | 
			
		||||
inherits = "release"
 | 
			
		||||
opt-level = 'z'
 | 
			
		||||
lto = true
 | 
			
		||||
codegen-units = 1
 | 
			
		||||
panic = "abort"
 | 
			
		||||
 | 
			
		||||
[package.metadata.leptos]
 | 
			
		||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
 | 
			
		||||
output-name = "harmony-example-rust-webapp"
 | 
			
		||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
 | 
			
		||||
site-root = "target/site"
 | 
			
		||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
 | 
			
		||||
# Defaults to pkg
 | 
			
		||||
site-pkg-dir = "pkg"
 | 
			
		||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
 | 
			
		||||
style-file = "style/main.scss"
 | 
			
		||||
# Assets source dir. All files found here will be copied and synchronized to site-root.
 | 
			
		||||
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
 | 
			
		||||
#
 | 
			
		||||
# Optional. Env: LEPTOS_ASSETS_DIR.
 | 
			
		||||
assets-dir = "assets"
 | 
			
		||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
 | 
			
		||||
site-addr = "0.0.0.0:3000"
 | 
			
		||||
# The port to use for automatic reload monitoring
 | 
			
		||||
reload-port = 3001
 | 
			
		||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
 | 
			
		||||
#   [Windows] for non-WSL use "npx.cmd playwright test"
 | 
			
		||||
#   This binary name can be checked in Powershell with Get-Command npx
 | 
			
		||||
end2end-cmd = "npx playwright test"
 | 
			
		||||
end2end-dir = "end2end"
 | 
			
		||||
#  The browserlist query used for optimizing the CSS.
 | 
			
		||||
browserquery = "defaults"
 | 
			
		||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
 | 
			
		||||
env = "DEV"
 | 
			
		||||
# The features to use when compiling the bin target
 | 
			
		||||
#
 | 
			
		||||
# Optional. Can be over-ridden with the command line parameter --bin-features
 | 
			
		||||
bin-features = ["ssr"]
 | 
			
		||||
 | 
			
		||||
# If the --no-default-features flag should be used when compiling the bin target
 | 
			
		||||
#
 | 
			
		||||
# Optional. Defaults to false.
 | 
			
		||||
bin-default-features = false
 | 
			
		||||
 | 
			
		||||
# The features to use when compiling the lib target
 | 
			
		||||
#
 | 
			
		||||
# Optional. Can be over-ridden with the command line parameter --lib-features
 | 
			
		||||
lib-features = ["hydrate"]
 | 
			
		||||
 | 
			
		||||
# If the --no-default-features flag should be used when compiling the lib target
 | 
			
		||||
#
 | 
			
		||||
# Optional. Defaults to false.
 | 
			
		||||
lib-default-features = false
 | 
			
		||||
 | 
			
		||||
# The profile to use for the lib target when compiling for release
 | 
			
		||||
#
 | 
			
		||||
# Optional. Defaults to "release".
 | 
			
		||||
lib-profile-release = "wasm-release"
 | 
			
		||||
							
								
								
									
										16
									
								
								examples/rust/webapp/Dockerfile.harmony
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								examples/rust/webapp/Dockerfile.harmony
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
FROM rust:bookworm as builder
 | 
			
		||||
RUN apt-get update && apt-get install -y --no-install-recommends clang wget && wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz && tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz && cp cargo-binstall /usr/local/cargo/bin && rm cargo-binstall-x86_64-unknown-linux-musl.tgz cargo-binstall && apt-get clean && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
RUN cargo binstall cargo-leptos -y
 | 
			
		||||
RUN rustup target add wasm32-unknown-unknown
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
COPY . .
 | 
			
		||||
RUN cargo leptos build --release -vv
 | 
			
		||||
FROM debian:bookworm-slim
 | 
			
		||||
RUN groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser
 | 
			
		||||
ENV LEPTOS_SITE_ADDR=0.0.0.0:3000
 | 
			
		||||
EXPOSE 3000/tcp
 | 
			
		||||
WORKDIR /home/appuser
 | 
			
		||||
COPY --from=builder /app/target/site/pkg /home/appuser/pkg
 | 
			
		||||
COPY --from=builder /app/target/release/harmony-example-rust-webapp /home/appuser/harmony-example-rust-webapp
 | 
			
		||||
USER appuser
 | 
			
		||||
CMD /home/appuser/harmony-example-rust-webapp
 | 
			
		||||
							
								
								
									
										24
									
								
								examples/rust/webapp/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								examples/rust/webapp/LICENSE
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
This is free and unencumbered software released into the public domain.
 | 
			
		||||
 | 
			
		||||
Anyone is free to copy, modify, publish, use, compile, sell, or
 | 
			
		||||
distribute this software, either in source code form or as a compiled
 | 
			
		||||
binary, for any purpose, commercial or non-commercial, and by any
 | 
			
		||||
means.
 | 
			
		||||
 | 
			
		||||
In jurisdictions that recognize copyright laws, the author or authors
 | 
			
		||||
of this software dedicate any and all copyright interest in the
 | 
			
		||||
software to the public domain. We make this dedication for the benefit
 | 
			
		||||
of the public at large and to the detriment of our heirs and
 | 
			
		||||
successors. We intend this dedication to be an overt act of
 | 
			
		||||
relinquishment in perpetuity of all present and future rights to this
 | 
			
		||||
software under copyright law.
 | 
			
		||||
 | 
			
		||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 | 
			
		||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 | 
			
		||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 | 
			
		||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 | 
			
		||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 | 
			
		||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 | 
			
		||||
OTHER DEALINGS IN THE SOFTWARE.
 | 
			
		||||
 | 
			
		||||
For more information, please refer to <https://unlicense.org>
 | 
			
		||||
							
								
								
									
										72
									
								
								examples/rust/webapp/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								examples/rust/webapp/README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,72 @@
 | 
			
		||||
<picture>
 | 
			
		||||
    <source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_Solid_White.svg" media="(prefers-color-scheme: dark)">
 | 
			
		||||
    <img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
 | 
			
		||||
</picture>
 | 
			
		||||
 | 
			
		||||
# Leptos Starter Template
 | 
			
		||||
 | 
			
		||||
This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
 | 
			
		||||
 | 
			
		||||
## Creating your template repo
 | 
			
		||||
 | 
			
		||||
If you don't have `cargo-leptos` installed you can install it with
 | 
			
		||||
 | 
			
		||||
`cargo install cargo-leptos --locked`
 | 
			
		||||
 | 
			
		||||
Then run
 | 
			
		||||
 | 
			
		||||
`cargo leptos new --git leptos-rs/start-actix`
 | 
			
		||||
 | 
			
		||||
to generate a new project template (you will be prompted to enter a project name).
 | 
			
		||||
 | 
			
		||||
`cd {projectname}`
 | 
			
		||||
 | 
			
		||||
to go to your newly created project.
 | 
			
		||||
 | 
			
		||||
Of course, you should explore around the project structure, but the best place to start with your application code is in `src/app.rs`.
 | 
			
		||||
 | 
			
		||||
## Running your project
 | 
			
		||||
 | 
			
		||||
`cargo leptos watch`  
 | 
			
		||||
By default, you can access your local project at `http://localhost:3000`
 | 
			
		||||
 | 
			
		||||
## Installing Additional Tools
 | 
			
		||||
 | 
			
		||||
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
 | 
			
		||||
 | 
			
		||||
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
 | 
			
		||||
2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
 | 
			
		||||
3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
 | 
			
		||||
4. `npm install -g sass` - install `dart-sass` (should be optional in future)
 | 
			
		||||
 | 
			
		||||
## Executing a Server on a Remote Machine Without the Toolchain
 | 
			
		||||
After running a `cargo leptos build --release` the minimum files needed are:
 | 
			
		||||
 | 
			
		||||
1. The server binary located in `target/server/release`
 | 
			
		||||
2. The `site` directory and all files within located in `target/site`
 | 
			
		||||
 | 
			
		||||
Copy these files to your remote server. The directory structure should be:
 | 
			
		||||
```text
 | 
			
		||||
leptos_start
 | 
			
		||||
site/
 | 
			
		||||
```
 | 
			
		||||
Set the following environment variables (updating for your project as needed):
 | 
			
		||||
```sh
 | 
			
		||||
export LEPTOS_OUTPUT_NAME="leptos_start"
 | 
			
		||||
export LEPTOS_SITE_ROOT="site"
 | 
			
		||||
export LEPTOS_SITE_PKG_DIR="pkg"
 | 
			
		||||
export LEPTOS_SITE_ADDR="127.0.0.1:3000"
 | 
			
		||||
export LEPTOS_RELOAD_PORT="3001"
 | 
			
		||||
```
 | 
			
		||||
Finally, run the server binary.
 | 
			
		||||
 | 
			
		||||
## Notes about CSR and Trunk:
 | 
			
		||||
Although it is not recommended, you can also run your project without server integration using the feature `csr` and `trunk serve`:
 | 
			
		||||
 | 
			
		||||
`trunk serve --open --features csr`
 | 
			
		||||
 | 
			
		||||
This may be useful for integrating external tools which require a static site, e.g. `tauri`.
 | 
			
		||||
 | 
			
		||||
## Licensing
 | 
			
		||||
 | 
			
		||||
This template itself is released under the Unlicense. You should replace the LICENSE for your own application with an appropriate license if you plan to release it publicly.
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								examples/rust/webapp/assets/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								examples/rust/webapp/assets/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										112
									
								
								examples/rust/webapp/end2end/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								examples/rust/webapp/end2end/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,112 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "end2end",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "lockfileVersion": 3,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "end2end",
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@playwright/test": "^1.44.1",
 | 
			
		||||
        "@types/node": "^20.12.12",
 | 
			
		||||
        "typescript": "^5.4.5"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@playwright/test": {
 | 
			
		||||
      "version": "1.44.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
 | 
			
		||||
      "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "Apache-2.0",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "playwright": "1.44.1"
 | 
			
		||||
      },
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "playwright": "cli.js"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=16"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/node": {
 | 
			
		||||
      "version": "20.12.12",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
 | 
			
		||||
      "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "undici-types": "~5.26.4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/fsevents": {
 | 
			
		||||
      "version": "2.3.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
 | 
			
		||||
      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "darwin"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/playwright": {
 | 
			
		||||
      "version": "1.44.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
 | 
			
		||||
      "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "Apache-2.0",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "playwright-core": "1.44.1"
 | 
			
		||||
      },
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "playwright": "cli.js"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=16"
 | 
			
		||||
      },
 | 
			
		||||
      "optionalDependencies": {
 | 
			
		||||
        "fsevents": "2.3.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/playwright-core": {
 | 
			
		||||
      "version": "1.44.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
 | 
			
		||||
      "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "Apache-2.0",
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "playwright-core": "cli.js"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=16"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/typescript": {
 | 
			
		||||
      "version": "5.4.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
 | 
			
		||||
      "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "Apache-2.0",
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "tsc": "bin/tsc",
 | 
			
		||||
        "tsserver": "bin/tsserver"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=14.17"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/undici-types": {
 | 
			
		||||
      "version": "5.26.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
 | 
			
		||||
      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								examples/rust/webapp/end2end/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								examples/rust/webapp/end2end/package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "end2end",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "scripts": {},
 | 
			
		||||
  "keywords": [],
 | 
			
		||||
  "author": "",
 | 
			
		||||
  "license": "ISC",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@playwright/test": "^1.44.1",
 | 
			
		||||
    "@types/node": "^20.12.12",
 | 
			
		||||
    "typescript": "^5.4.5"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										104
									
								
								examples/rust/webapp/end2end/playwright.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								examples/rust/webapp/end2end/playwright.config.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,104 @@
 | 
			
		||||
import { devices, defineConfig } from "@playwright/test";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Read environment variables from file.
 | 
			
		||||
 * https://github.com/motdotla/dotenv
 | 
			
		||||
 */
 | 
			
		||||
// require('dotenv').config();
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * See https://playwright.dev/docs/test-configuration.
 | 
			
		||||
 */
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  testDir: "./tests",
 | 
			
		||||
  /* Maximum time one test can run for. */
 | 
			
		||||
  timeout: 30 * 1000,
 | 
			
		||||
  expect: {
 | 
			
		||||
    /**
 | 
			
		||||
     * Maximum time expect() should wait for the condition to be met.
 | 
			
		||||
     * For example in `await expect(locator).toHaveText();`
 | 
			
		||||
     */
 | 
			
		||||
    timeout: 5000,
 | 
			
		||||
  },
 | 
			
		||||
  /* Run tests in files in parallel */
 | 
			
		||||
  fullyParallel: true,
 | 
			
		||||
  /* Fail the build on CI if you accidentally left test.only in the source code. */
 | 
			
		||||
  forbidOnly: !!process.env.CI,
 | 
			
		||||
  /* Retry on CI only */
 | 
			
		||||
  retries: process.env.CI ? 2 : 0,
 | 
			
		||||
  /* Opt out of parallel tests on CI. */
 | 
			
		||||
  workers: process.env.CI ? 1 : undefined,
 | 
			
		||||
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
 | 
			
		||||
  reporter: "html",
 | 
			
		||||
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
 | 
			
		||||
  use: {
 | 
			
		||||
    /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
 | 
			
		||||
    actionTimeout: 0,
 | 
			
		||||
    /* Base URL to use in actions like `await page.goto('/')`. */
 | 
			
		||||
    // baseURL: 'http://localhost:3000',
 | 
			
		||||
 | 
			
		||||
    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
 | 
			
		||||
    trace: "on-first-retry",
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /* Configure projects for major browsers */
 | 
			
		||||
  projects: [
 | 
			
		||||
    {
 | 
			
		||||
      name: "chromium",
 | 
			
		||||
      use: {
 | 
			
		||||
        ...devices["Desktop Chrome"],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      name: "firefox",
 | 
			
		||||
      use: {
 | 
			
		||||
        ...devices["Desktop Firefox"],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      name: "webkit",
 | 
			
		||||
      use: {
 | 
			
		||||
        ...devices["Desktop Safari"],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /* Test against mobile viewports. */
 | 
			
		||||
    // {
 | 
			
		||||
    //   name: 'Mobile Chrome',
 | 
			
		||||
    //   use: {
 | 
			
		||||
    //     ...devices['Pixel 5'],
 | 
			
		||||
    //   },
 | 
			
		||||
    // },
 | 
			
		||||
    // {
 | 
			
		||||
    //   name: 'Mobile Safari',
 | 
			
		||||
    //   use: {
 | 
			
		||||
    //     ...devices['iPhone 12'],
 | 
			
		||||
    //   },
 | 
			
		||||
    // },
 | 
			
		||||
 | 
			
		||||
    /* Test against branded browsers. */
 | 
			
		||||
    // {
 | 
			
		||||
    //   name: 'Microsoft Edge',
 | 
			
		||||
    //   use: {
 | 
			
		||||
    //     channel: 'msedge',
 | 
			
		||||
    //   },
 | 
			
		||||
    // },
 | 
			
		||||
    // {
 | 
			
		||||
    //   name: 'Google Chrome',
 | 
			
		||||
    //   use: {
 | 
			
		||||
    //     channel: 'chrome',
 | 
			
		||||
    //   },
 | 
			
		||||
    // },
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  /* Folder for test artifacts such as screenshots, videos, traces, etc. */
 | 
			
		||||
  // outputDir: 'test-results/',
 | 
			
		||||
 | 
			
		||||
  /* Run your local dev server before starting the tests */
 | 
			
		||||
  // webServer: {
 | 
			
		||||
  //   command: 'npm run start',
 | 
			
		||||
  //   port: 3000,
 | 
			
		||||
  // },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										9
									
								
								examples/rust/webapp/end2end/tests/example.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								examples/rust/webapp/end2end/tests/example.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
import { test, expect } from "@playwright/test";
 | 
			
		||||
 | 
			
		||||
test("homepage has title and links to intro page", async ({ page }) => {
 | 
			
		||||
  await page.goto("http://localhost:3000/");
 | 
			
		||||
 | 
			
		||||
  await expect(page).toHaveTitle("Welcome to Leptos");
 | 
			
		||||
 | 
			
		||||
  await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										109
									
								
								examples/rust/webapp/end2end/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								examples/rust/webapp/end2end/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,109 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    /* Visit https://aka.ms/tsconfig to read more about this file */
 | 
			
		||||
 | 
			
		||||
    /* Projects */
 | 
			
		||||
    // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
 | 
			
		||||
    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
 | 
			
		||||
    // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
 | 
			
		||||
    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
 | 
			
		||||
    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
 | 
			
		||||
    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
 | 
			
		||||
 | 
			
		||||
    /* Language and Environment */
 | 
			
		||||
    "target": "es2016",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
 | 
			
		||||
    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
 | 
			
		||||
    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
 | 
			
		||||
    // "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
 | 
			
		||||
    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
 | 
			
		||||
    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
 | 
			
		||||
    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
 | 
			
		||||
    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
 | 
			
		||||
    // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
 | 
			
		||||
    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
 | 
			
		||||
    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
 | 
			
		||||
    // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
 | 
			
		||||
 | 
			
		||||
    /* Modules */
 | 
			
		||||
    "module": "commonjs",                                /* Specify what module code is generated. */
 | 
			
		||||
    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
 | 
			
		||||
    // "moduleResolution": "node10",                     /* Specify how TypeScript looks up a file from a given module specifier. */
 | 
			
		||||
    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
 | 
			
		||||
    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
 | 
			
		||||
    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
 | 
			
		||||
    // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
 | 
			
		||||
    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
 | 
			
		||||
    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
 | 
			
		||||
    // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
 | 
			
		||||
    // "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
 | 
			
		||||
    // "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */
 | 
			
		||||
    // "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */
 | 
			
		||||
    // "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
 | 
			
		||||
    // "resolveJsonModule": true,                        /* Enable importing .json files. */
 | 
			
		||||
    // "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */
 | 
			
		||||
    // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
 | 
			
		||||
 | 
			
		||||
    /* JavaScript Support */
 | 
			
		||||
    // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
 | 
			
		||||
    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
 | 
			
		||||
    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
 | 
			
		||||
 | 
			
		||||
    /* Emit */
 | 
			
		||||
    // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
 | 
			
		||||
    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
 | 
			
		||||
    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
 | 
			
		||||
    // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
 | 
			
		||||
    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
 | 
			
		||||
    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
 | 
			
		||||
    // "outDir": "./",                                   /* Specify an output folder for all emitted files. */
 | 
			
		||||
    // "removeComments": true,                           /* Disable emitting comments. */
 | 
			
		||||
    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
 | 
			
		||||
    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
 | 
			
		||||
    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types. */
 | 
			
		||||
    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
 | 
			
		||||
    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
 | 
			
		||||
    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
 | 
			
		||||
    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
 | 
			
		||||
    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
 | 
			
		||||
    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
 | 
			
		||||
    // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
 | 
			
		||||
    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
 | 
			
		||||
    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
 | 
			
		||||
    // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
 | 
			
		||||
    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
 | 
			
		||||
    // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
 | 
			
		||||
 | 
			
		||||
    /* Interop Constraints */
 | 
			
		||||
    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
 | 
			
		||||
    // "verbatimModuleSyntax": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
 | 
			
		||||
    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
 | 
			
		||||
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
 | 
			
		||||
    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
 | 
			
		||||
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
 | 
			
		||||
 | 
			
		||||
    /* Type Checking */
 | 
			
		||||
    "strict": true,                                      /* Enable all strict type-checking options. */
 | 
			
		||||
    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
 | 
			
		||||
    // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
 | 
			
		||||
    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
 | 
			
		||||
    // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
 | 
			
		||||
    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
 | 
			
		||||
    // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
 | 
			
		||||
    // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
 | 
			
		||||
    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
 | 
			
		||||
    // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
 | 
			
		||||
    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
 | 
			
		||||
    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
 | 
			
		||||
    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
 | 
			
		||||
    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
 | 
			
		||||
    // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
 | 
			
		||||
    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
 | 
			
		||||
    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
 | 
			
		||||
    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
 | 
			
		||||
    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
 | 
			
		||||
 | 
			
		||||
    /* Completeness */
 | 
			
		||||
    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
 | 
			
		||||
    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								examples/rust/webapp/src/app.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								examples/rust/webapp/src/app.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,66 @@
 | 
			
		||||
use leptos::prelude::*;
 | 
			
		||||
use leptos_meta::{provide_meta_context, Stylesheet, Title};
 | 
			
		||||
use leptos_router::{
 | 
			
		||||
    components::{Route, Router, Routes},
 | 
			
		||||
    StaticSegment, WildcardSegment,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[component]
 | 
			
		||||
pub fn App() -> impl IntoView {
 | 
			
		||||
    // Provides context that manages stylesheets, titles, meta tags, etc.
 | 
			
		||||
    provide_meta_context();
 | 
			
		||||
 | 
			
		||||
    view! {
 | 
			
		||||
        // injects a stylesheet into the document <head>
 | 
			
		||||
        // id=leptos means cargo-leptos will hot-reload this stylesheet
 | 
			
		||||
        <Stylesheet id="leptos" href="/pkg/harmony-example-rust-webapp.css"/>
 | 
			
		||||
 | 
			
		||||
        // sets the document title
 | 
			
		||||
        <Title text="Welcome to Leptos"/>
 | 
			
		||||
 | 
			
		||||
        // content for this welcome page
 | 
			
		||||
        <Router>
 | 
			
		||||
            <main>
 | 
			
		||||
                <Routes fallback=move || "Not found.">
 | 
			
		||||
                    <Route path=StaticSegment("") view=HomePage/>
 | 
			
		||||
                    <Route path=WildcardSegment("any") view=NotFound/>
 | 
			
		||||
                </Routes>
 | 
			
		||||
            </main>
 | 
			
		||||
        </Router>
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Renders the home page of your application.
 | 
			
		||||
#[component]
 | 
			
		||||
fn HomePage() -> impl IntoView {
 | 
			
		||||
    // Creates a reactive value to update the button
 | 
			
		||||
    let count = RwSignal::new(0);
 | 
			
		||||
    let on_click = move |_| *count.write() += 1;
 | 
			
		||||
 | 
			
		||||
    view! {
 | 
			
		||||
        <h1>"Welcome to Leptos!"</h1>
 | 
			
		||||
        <button on:click=on_click>"Click Me: " {count}</button>
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 404 - Not Found
 | 
			
		||||
#[component]
 | 
			
		||||
fn NotFound() -> impl IntoView {
 | 
			
		||||
    // set an HTTP status code 404
 | 
			
		||||
    // this is feature gated because it can only be done during
 | 
			
		||||
    // initial server-side rendering
 | 
			
		||||
    // if you navigate to the 404 page subsequently, the status
 | 
			
		||||
    // code will not be set because there is not a new HTTP request
 | 
			
		||||
    // to the server
 | 
			
		||||
    #[cfg(feature = "ssr")]
 | 
			
		||||
    {
 | 
			
		||||
        // this can be done inline because it's synchronous
 | 
			
		||||
        // if it were async, we'd use a server function
 | 
			
		||||
        let resp = expect_context::<leptos_actix::ResponseOptions>();
 | 
			
		||||
        resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    view! {
 | 
			
		||||
        <h1>"Not Found"</h1>
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								examples/rust/webapp/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								examples/rust/webapp/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
pub mod app;
 | 
			
		||||
 | 
			
		||||
#[cfg(feature = "hydrate")]
 | 
			
		||||
#[wasm_bindgen::prelude::wasm_bindgen]
 | 
			
		||||
pub fn hydrate() {
 | 
			
		||||
    use app::*;
 | 
			
		||||
    console_error_panic_hook::set_once();
 | 
			
		||||
    leptos::mount::hydrate_body(App);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										88
									
								
								examples/rust/webapp/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								examples/rust/webapp/src/main.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,88 @@
 | 
			
		||||
#[cfg(feature = "ssr")]
 | 
			
		||||
#[actix_web::main]
 | 
			
		||||
async fn main() -> std::io::Result<()> {
 | 
			
		||||
    use actix_files::Files;
 | 
			
		||||
    use actix_web::*;
 | 
			
		||||
    use leptos::prelude::*;
 | 
			
		||||
    use leptos::config::get_configuration;
 | 
			
		||||
    use leptos_meta::MetaTags;
 | 
			
		||||
    use leptos_actix::{generate_route_list, LeptosRoutes};
 | 
			
		||||
    use harmony_example_rust_webapp::app::*;
 | 
			
		||||
 | 
			
		||||
    let conf = get_configuration(None).unwrap();
 | 
			
		||||
    let addr = conf.leptos_options.site_addr;
 | 
			
		||||
 | 
			
		||||
    HttpServer::new(move || {
 | 
			
		||||
        // Generate the list of routes in your Leptos App
 | 
			
		||||
        let routes = generate_route_list(App);
 | 
			
		||||
        let leptos_options = &conf.leptos_options;
 | 
			
		||||
        let site_root = leptos_options.site_root.clone().to_string();
 | 
			
		||||
 | 
			
		||||
        println!("listening on http://{}", &addr);
 | 
			
		||||
 | 
			
		||||
        App::new()
 | 
			
		||||
            // serve JS/WASM/CSS from `pkg`
 | 
			
		||||
            .service(Files::new("/pkg", format!("{site_root}/pkg")))
 | 
			
		||||
            // serve other assets from the `assets` directory
 | 
			
		||||
            .service(Files::new("/assets", &site_root))
 | 
			
		||||
            // serve the favicon from /favicon.ico
 | 
			
		||||
            .service(favicon)
 | 
			
		||||
            .leptos_routes(routes, {
 | 
			
		||||
                let leptos_options = leptos_options.clone();
 | 
			
		||||
                move || {
 | 
			
		||||
                    view! {
 | 
			
		||||
                        <!DOCTYPE html>
 | 
			
		||||
                        <html lang="en">
 | 
			
		||||
                            <head>
 | 
			
		||||
                                <meta charset="utf-8"/>
 | 
			
		||||
                                <meta name="viewport" content="width=device-width, initial-scale=1"/>
 | 
			
		||||
                                <AutoReload options=leptos_options.clone() />
 | 
			
		||||
                                <HydrationScripts options=leptos_options.clone()/>
 | 
			
		||||
                                <MetaTags/>
 | 
			
		||||
                            </head>
 | 
			
		||||
                            <body>
 | 
			
		||||
                                <App/>
 | 
			
		||||
                            </body>
 | 
			
		||||
                        </html>
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .app_data(web::Data::new(leptos_options.to_owned()))
 | 
			
		||||
        //.wrap(middleware::Compress::default())
 | 
			
		||||
    })
 | 
			
		||||
    .bind(&addr)?
 | 
			
		||||
    .run()
 | 
			
		||||
    .await
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(feature = "ssr")]
 | 
			
		||||
#[actix_web::get("favicon.ico")]
 | 
			
		||||
async fn favicon(
 | 
			
		||||
    leptos_options: actix_web::web::Data<leptos::config::LeptosOptions>,
 | 
			
		||||
) -> actix_web::Result<actix_files::NamedFile> {
 | 
			
		||||
    let leptos_options = leptos_options.into_inner();
 | 
			
		||||
    let site_root = &leptos_options.site_root;
 | 
			
		||||
    Ok(actix_files::NamedFile::open(format!(
 | 
			
		||||
        "{site_root}/favicon.ico"
 | 
			
		||||
    ))?)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(not(any(feature = "ssr", feature = "csr")))]
 | 
			
		||||
pub fn main() {
 | 
			
		||||
    // no client-side main function
 | 
			
		||||
    // unless we want this to work with e.g., Trunk for pure client-side testing
 | 
			
		||||
    // see lib.rs for hydration function instead
 | 
			
		||||
    // see optional feature `csr` instead
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(all(not(feature = "ssr"), feature = "csr"))]
 | 
			
		||||
pub fn main() {
 | 
			
		||||
    // a client-side main function is required for using `trunk serve`
 | 
			
		||||
    // prefer using `cargo leptos serve` instead
 | 
			
		||||
    // to run: `trunk serve --open --features csr`
 | 
			
		||||
    use harmony_example_rust_webapp::app::*;
 | 
			
		||||
 | 
			
		||||
    console_error_panic_hook::set_once();
 | 
			
		||||
 | 
			
		||||
    leptos::mount_to_body(App);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								examples/rust/webapp/style/main.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								examples/rust/webapp/style/main.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
body {
 | 
			
		||||
	font-family: sans-serif;
 | 
			
		||||
	text-align: center;
 | 
			
		||||
}
 | 
			
		||||
@ -57,3 +57,4 @@ similar.workspace = true
 | 
			
		||||
futures-util = "0.3.31"
 | 
			
		||||
tokio-util = "0.7.15"
 | 
			
		||||
strum = { version = "0.27.1", features = ["derive"] }
 | 
			
		||||
tempfile = "3.20.0"
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ use lazy_static::lazy_static;
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
lazy_static! {
 | 
			
		||||
    pub static ref HARMONY_CONFIG_DIR: PathBuf = directories::BaseDirs::new()
 | 
			
		||||
    pub static ref HARMONY_DATA_DIR: PathBuf = directories::BaseDirs::new()
 | 
			
		||||
        .unwrap()
 | 
			
		||||
        .data_dir()
 | 
			
		||||
        .join("harmony");
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,10 @@ pub struct Maestro<T: Topology> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T: Topology> Maestro<T> {
 | 
			
		||||
    pub fn new(inventory: Inventory, topology: T) -> Self {
 | 
			
		||||
    /// Creates a bare maestro without initialization.
 | 
			
		||||
    ///
 | 
			
		||||
    /// This should rarely be used. Most of the time Maestro::initialize should be used instead.
 | 
			
		||||
    pub fn new_without_initialization(inventory: Inventory, topology: T) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            inventory,
 | 
			
		||||
            topology,
 | 
			
		||||
@ -29,7 +32,7 @@ impl<T: Topology> Maestro<T> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn initialize(inventory: Inventory, topology: T) -> Result<Self, InterpretError> {
 | 
			
		||||
        let instance = Self::new(inventory, topology);
 | 
			
		||||
        let instance = Self::new_without_initialization(inventory, topology);
 | 
			
		||||
        instance.prepare_topology().await?;
 | 
			
		||||
        Ok(instance)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ use crate::{
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::{
 | 
			
		||||
    HelmCommand, K8sclient, Topology,
 | 
			
		||||
    DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology,
 | 
			
		||||
    k8s::K8sClient,
 | 
			
		||||
    tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager},
 | 
			
		||||
};
 | 
			
		||||
@ -123,39 +123,47 @@ impl K8sAnywhereTopology {
 | 
			
		||||
    async fn try_get_or_install_k8s_client(&self) -> Result<Option<K8sState>, InterpretError> {
 | 
			
		||||
        let k8s_anywhere_config = &self.config;
 | 
			
		||||
 | 
			
		||||
        if let Some(kubeconfig) = &k8s_anywhere_config.kubeconfig {
 | 
			
		||||
            debug!("Loading kubeconfig {kubeconfig}");
 | 
			
		||||
            match self.try_load_kubeconfig(&kubeconfig).await {
 | 
			
		||||
                Some(client) => {
 | 
			
		||||
                    return Ok(Some(K8sState {
 | 
			
		||||
                        client: Arc::new(client),
 | 
			
		||||
                        _source: K8sSource::Kubeconfig,
 | 
			
		||||
                        message: format!("Loaded k8s client from kubeconfig {kubeconfig}"),
 | 
			
		||||
                    }));
 | 
			
		||||
                }
 | 
			
		||||
                None => {
 | 
			
		||||
                    return Err(InterpretError::new(format!(
 | 
			
		||||
                        "Failed to load kubeconfig from {kubeconfig}"
 | 
			
		||||
                    )));
 | 
			
		||||
        // TODO this deserves some refactoring, it is becoming a bit hard to figure out
 | 
			
		||||
        // be careful when making modifications here
 | 
			
		||||
        if k8s_anywhere_config.use_local_k3d {
 | 
			
		||||
            info!("Using local k3d cluster because of use_local_k3d set to true");
 | 
			
		||||
        } else {
 | 
			
		||||
            if let Some(kubeconfig) = &k8s_anywhere_config.kubeconfig {
 | 
			
		||||
                debug!("Loading kubeconfig {kubeconfig}");
 | 
			
		||||
                match self.try_load_kubeconfig(&kubeconfig).await {
 | 
			
		||||
                    Some(client) => {
 | 
			
		||||
                        return Ok(Some(K8sState {
 | 
			
		||||
                            client: Arc::new(client),
 | 
			
		||||
                            _source: K8sSource::Kubeconfig,
 | 
			
		||||
                            message: format!("Loaded k8s client from kubeconfig {kubeconfig}"),
 | 
			
		||||
                        }));
 | 
			
		||||
                    }
 | 
			
		||||
                    None => {
 | 
			
		||||
                        return Err(InterpretError::new(format!(
 | 
			
		||||
                            "Failed to load kubeconfig from {kubeconfig}"
 | 
			
		||||
                        )));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if k8s_anywhere_config.use_system_kubeconfig {
 | 
			
		||||
            debug!("Loading system kubeconfig");
 | 
			
		||||
            match self.try_load_system_kubeconfig().await {
 | 
			
		||||
                Some(_client) => todo!(),
 | 
			
		||||
                None => todo!(),
 | 
			
		||||
            if k8s_anywhere_config.use_system_kubeconfig {
 | 
			
		||||
                debug!("Loading system kubeconfig");
 | 
			
		||||
                match self.try_load_system_kubeconfig().await {
 | 
			
		||||
                    Some(_client) => todo!(),
 | 
			
		||||
                    None => todo!(),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        info!("No kubernetes configuration found");
 | 
			
		||||
            info!("No kubernetes configuration found");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if !k8s_anywhere_config.autoinstall {
 | 
			
		||||
            debug!("Autoinstall confirmation prompt");
 | 
			
		||||
            let confirmation = Confirm::new( "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? : ")
 | 
			
		||||
                .with_default(false)
 | 
			
		||||
                .prompt()
 | 
			
		||||
                .expect("Unexpected prompt error");
 | 
			
		||||
            debug!("Autoinstall confirmation {confirmation}");
 | 
			
		||||
 | 
			
		||||
            if !confirmation {
 | 
			
		||||
                warn!(
 | 
			
		||||
@ -229,8 +237,16 @@ pub struct K8sAnywhereConfig {
 | 
			
		||||
    ///
 | 
			
		||||
    /// When enabled, autoinstall will setup a K3D cluster on the localhost. https://k3d.io/stable/
 | 
			
		||||
    ///
 | 
			
		||||
    /// Default: true
 | 
			
		||||
    /// Default: false
 | 
			
		||||
    pub autoinstall: bool,
 | 
			
		||||
 | 
			
		||||
    /// Whether to use local k3d cluster.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Takes precedence over other options, useful to avoid messing up a remote cluster by mistake
 | 
			
		||||
    ///
 | 
			
		||||
    /// default: true
 | 
			
		||||
    pub use_local_k3d: bool,
 | 
			
		||||
    harmony_profile: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl K8sAnywhereConfig {
 | 
			
		||||
@ -241,6 +257,13 @@ impl K8sAnywhereConfig {
 | 
			
		||||
                .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
 | 
			
		||||
            autoinstall: std::env::var("HARMONY_AUTOINSTALL")
 | 
			
		||||
                .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
 | 
			
		||||
            // TODO harmony_profile should be managed at a more core level than this
 | 
			
		||||
            harmony_profile: std::env::var("HARMONY_PROFILE").map_or_else(
 | 
			
		||||
                |_| "dev".to_string(),
 | 
			
		||||
                |v| v.parse().ok().unwrap_or("dev".to_string()),
 | 
			
		||||
            ),
 | 
			
		||||
            use_local_k3d: std::env::var("HARMONY_USE_LOCAL_K3D")
 | 
			
		||||
                .map_or_else(|_| true, |v| v.parse().ok().unwrap_or(true)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -275,6 +298,20 @@ impl Topology for K8sAnywhereTopology {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl MultiTargetTopology for K8sAnywhereTopology {
 | 
			
		||||
    fn current_target(&self) -> DeploymentTarget {
 | 
			
		||||
        if self.config.use_local_k3d {
 | 
			
		||||
            return DeploymentTarget::LocalDev;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        match self.config.harmony_profile.to_lowercase().as_str() {
 | 
			
		||||
            "staging" => DeploymentTarget::Staging,
 | 
			
		||||
            "production" => DeploymentTarget::Production,
 | 
			
		||||
            _ => todo!("HARMONY_PROFILE must be set when use_local_k3d is not set"),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl HelmCommand for K8sAnywhereTopology {}
 | 
			
		||||
 | 
			
		||||
#[async_trait]
 | 
			
		||||
 | 
			
		||||
@ -62,6 +62,17 @@ pub trait Topology: Send + Sync {
 | 
			
		||||
    async fn ensure_ready(&self) -> Result<Outcome, InterpretError>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub enum DeploymentTarget {
 | 
			
		||||
    LocalDev,
 | 
			
		||||
    Staging,
 | 
			
		||||
    Production,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub trait MultiTargetTopology: Topology {
 | 
			
		||||
    fn current_target(&self) -> DeploymentTarget;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub type IpAddress = IpAddr;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,8 @@ use async_trait::async_trait;
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
 | 
			
		||||
use crate::topology::Topology;
 | 
			
		||||
 | 
			
		||||
use super::Application;
 | 
			
		||||
/// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability,
 | 
			
		||||
/// ContinuousIntegration, ContinuousDelivery
 | 
			
		||||
#[async_trait]
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,20 @@
 | 
			
		||||
use std::{io::Write, process::Command, sync::Arc};
 | 
			
		||||
 | 
			
		||||
use async_trait::async_trait;
 | 
			
		||||
use log::info;
 | 
			
		||||
use log::{error, info};
 | 
			
		||||
use serde_yaml::Value;
 | 
			
		||||
use tempfile::NamedTempFile;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    config::HARMONY_DATA_DIR,
 | 
			
		||||
    data::Version,
 | 
			
		||||
    inventory::Inventory,
 | 
			
		||||
    modules::{application::ApplicationFeature, helm::chart::HelmChartScore},
 | 
			
		||||
    modules::{
 | 
			
		||||
        application::{Application, ApplicationFeature, HelmPackage, OCICompliant},
 | 
			
		||||
        helm::chart::HelmChartScore,
 | 
			
		||||
    },
 | 
			
		||||
    score::Score,
 | 
			
		||||
    topology::{HelmCommand, Topology, Url},
 | 
			
		||||
    topology::{DeploymentTarget, HelmCommand, MultiTargetTopology, Topology, Url},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// ContinuousDelivery in Harmony provides this functionality :
 | 
			
		||||
@ -38,33 +45,164 @@ use crate::{
 | 
			
		||||
/// - ArgoCD to install/upgrade/rollback/inspect k8s resources
 | 
			
		||||
/// - Kubernetes for runtime orchestration
 | 
			
		||||
#[derive(Debug, Default, Clone)]
 | 
			
		||||
pub struct ContinuousDelivery {}
 | 
			
		||||
pub struct ContinuousDelivery<A: OCICompliant + HelmPackage> {
 | 
			
		||||
    pub application: Arc<A>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
 | 
			
		||||
    async fn deploy_to_local_k3d(
 | 
			
		||||
        &self,
 | 
			
		||||
        app_name: String,
 | 
			
		||||
        chart_url: String,
 | 
			
		||||
        image_name: String,
 | 
			
		||||
    ) -> Result<(), String> {
 | 
			
		||||
        error!(
 | 
			
		||||
            "FIXME This works only with local k3d installations, which is fine only for current demo purposes. We assume usage of K8sAnywhereTopology"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        error!("TODO hardcoded k3d bin path is wrong");
 | 
			
		||||
        let k3d_bin_path = (*HARMONY_DATA_DIR).join("k3d").join("k3d");
 | 
			
		||||
        // --- 1. Import the container image into the k3d cluster ---
 | 
			
		||||
        info!(
 | 
			
		||||
            "Importing image '{}' into k3d cluster 'harmony'",
 | 
			
		||||
            image_name
 | 
			
		||||
        );
 | 
			
		||||
        let import_output = Command::new(&k3d_bin_path)
 | 
			
		||||
            .args(["image", "import", &image_name, "--cluster", "harmony"])
 | 
			
		||||
            .output()
 | 
			
		||||
            .map_err(|e| format!("Failed to execute k3d image import: {}", e))?;
 | 
			
		||||
 | 
			
		||||
        if !import_output.status.success() {
 | 
			
		||||
            return Err(format!(
 | 
			
		||||
                "Failed to import image to k3d: {}",
 | 
			
		||||
                String::from_utf8_lossy(&import_output.stderr)
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // --- 2. Get the kubeconfig for the k3d cluster and write it to a temp file ---
 | 
			
		||||
        info!("Retrieving kubeconfig for k3d cluster 'harmony'");
 | 
			
		||||
        let kubeconfig_output = Command::new(&k3d_bin_path)
 | 
			
		||||
            .args(["kubeconfig", "get", "harmony"])
 | 
			
		||||
            .output()
 | 
			
		||||
            .map_err(|e| format!("Failed to execute k3d kubeconfig get: {}", e))?;
 | 
			
		||||
 | 
			
		||||
        if !kubeconfig_output.status.success() {
 | 
			
		||||
            return Err(format!(
 | 
			
		||||
                "Failed to get kubeconfig from k3d: {}",
 | 
			
		||||
                String::from_utf8_lossy(&kubeconfig_output.stderr)
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let mut temp_kubeconfig = NamedTempFile::new()
 | 
			
		||||
            .map_err(|e| format!("Failed to create temp file for kubeconfig: {}", e))?;
 | 
			
		||||
        temp_kubeconfig
 | 
			
		||||
            .write_all(&kubeconfig_output.stdout)
 | 
			
		||||
            .map_err(|e| format!("Failed to write to temp kubeconfig file: {}", e))?;
 | 
			
		||||
        let kubeconfig_path = temp_kubeconfig.path().to_str().unwrap();
 | 
			
		||||
 | 
			
		||||
        // --- 3. Install or upgrade the Helm chart in the cluster ---
 | 
			
		||||
        info!(
 | 
			
		||||
            "Deploying Helm chart '{}' to namespace '{}'",
 | 
			
		||||
            chart_url, app_name
 | 
			
		||||
        );
 | 
			
		||||
        let release_name = app_name.to_lowercase(); // Helm release names are often lowercase
 | 
			
		||||
        let helm_output = Command::new("helm")
 | 
			
		||||
            .args([
 | 
			
		||||
                "upgrade",
 | 
			
		||||
                "--install",
 | 
			
		||||
                &release_name,
 | 
			
		||||
                &chart_url,
 | 
			
		||||
                "--namespace",
 | 
			
		||||
                &app_name,
 | 
			
		||||
                "--create-namespace",
 | 
			
		||||
                "--wait", // Wait for the deployment to be ready
 | 
			
		||||
                "--kubeconfig",
 | 
			
		||||
                kubeconfig_path,
 | 
			
		||||
            ])
 | 
			
		||||
            .spawn()
 | 
			
		||||
            .map_err(|e| format!("Failed to execute helm upgrade: {}", e))?
 | 
			
		||||
            .wait_with_output()
 | 
			
		||||
            .map_err(|e| format!("Failed to execute helm upgrade: {}", e))?;
 | 
			
		||||
 | 
			
		||||
        if !helm_output.status.success() {
 | 
			
		||||
            return Err(format!(
 | 
			
		||||
                "Failed to deploy Helm chart: {}",
 | 
			
		||||
                String::from_utf8_lossy(&helm_output.stderr)
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        info!("Successfully deployed '{}' to local k3d cluster.", app_name);
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[async_trait]
 | 
			
		||||
impl<T: Topology + HelmCommand + 'static> ApplicationFeature<T> for ContinuousDelivery {
 | 
			
		||||
impl<
 | 
			
		||||
    A: OCICompliant + HelmPackage + Clone + 'static,
 | 
			
		||||
    T: Topology + HelmCommand + MultiTargetTopology + 'static,
 | 
			
		||||
> ApplicationFeature<T> for ContinuousDelivery<A>
 | 
			
		||||
{
 | 
			
		||||
    async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
 | 
			
		||||
        let image = self.application.image_name();
 | 
			
		||||
 | 
			
		||||
        // TODO
 | 
			
		||||
        error!(
 | 
			
		||||
            "TODO reverse helm chart packaging and docker image build. I put helm package first for faster iterations"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let helm_chart = self.application.build_push_helm_package(&image).await?;
 | 
			
		||||
        info!("Pushed new helm chart {helm_chart}");
 | 
			
		||||
 | 
			
		||||
        // let image = self.application.build_push_oci_image().await?;
 | 
			
		||||
        // info!("Pushed new docker image {image}");
 | 
			
		||||
        error!("uncomment above");
 | 
			
		||||
 | 
			
		||||
        info!("Installing ContinuousDelivery feature");
 | 
			
		||||
        let cd_server = HelmChartScore {
 | 
			
		||||
            namespace: todo!(
 | 
			
		||||
                "ArgoCD Helm chart with proper understanding of Tenant, see how Will did it for Monitoring for now"
 | 
			
		||||
            ),
 | 
			
		||||
            release_name: todo!("argocd helm chart whatever"),
 | 
			
		||||
            chart_name: todo!(),
 | 
			
		||||
            chart_version: todo!(),
 | 
			
		||||
            values_overrides: todo!(),
 | 
			
		||||
            values_yaml: todo!(),
 | 
			
		||||
            create_namespace: todo!(),
 | 
			
		||||
            install_only: todo!(),
 | 
			
		||||
            repository: todo!(),
 | 
			
		||||
        // TODO this is a temporary hack for demo purposes, the deployment target should be driven
 | 
			
		||||
        // by the topology only and we should not have to know how to perform tasks like this for
 | 
			
		||||
        // which the topology should be responsible.
 | 
			
		||||
        //
 | 
			
		||||
        // That said, this will require some careful architectural decisions, since the concept of
 | 
			
		||||
        // deployment targets / profiles is probably a layer of complexity that we won't be
 | 
			
		||||
        // completely able to avoid
 | 
			
		||||
        //
 | 
			
		||||
        // I'll try something for now that must be thought through after : att a deployment_profile
 | 
			
		||||
        // function to the topology trait that returns a profile, then anybody who needs it can
 | 
			
		||||
        // access it. This forces every Topology to understand the concept of targets though... So
 | 
			
		||||
        // instead I'll create a new Capability which is MultiTargetTopology and we'll see how it
 | 
			
		||||
        // goes. It still does not feel right though.
 | 
			
		||||
        match topology.current_target() {
 | 
			
		||||
            DeploymentTarget::LocalDev => {
 | 
			
		||||
                self.deploy_to_local_k3d(self.application.name(), helm_chart, image)
 | 
			
		||||
                    .await?;
 | 
			
		||||
            }
 | 
			
		||||
            target => {
 | 
			
		||||
                info!("Deploying to target {target:?}");
 | 
			
		||||
                let cd_server = HelmChartScore {
 | 
			
		||||
                    namespace: todo!(
 | 
			
		||||
                        "ArgoCD Helm chart with proper understanding of Tenant, see how Will did it for Monitoring for now"
 | 
			
		||||
                    ),
 | 
			
		||||
                    release_name: todo!("argocd helm chart whatever"),
 | 
			
		||||
                    chart_name: todo!(),
 | 
			
		||||
                    chart_version: todo!(),
 | 
			
		||||
                    values_overrides: todo!(),
 | 
			
		||||
                    values_yaml: todo!(),
 | 
			
		||||
                    create_namespace: todo!(),
 | 
			
		||||
                    install_only: todo!(),
 | 
			
		||||
                    repository: todo!(),
 | 
			
		||||
                };
 | 
			
		||||
                let interpret = cd_server.create_interpret();
 | 
			
		||||
                interpret.execute(&Inventory::empty(), topology);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        let interpret = cd_server.create_interpret();
 | 
			
		||||
        interpret.execute(&Inventory::empty(), topology);
 | 
			
		||||
 | 
			
		||||
        todo!("1. Create ArgoCD score that installs argo using helm chart, see if Taha's already done it
 | 
			
		||||
            2. Package app (docker image, helm chart)
 | 
			
		||||
            3. Push to registry if staging or prod
 | 
			
		||||
            4. Poke Argo
 | 
			
		||||
            5. Ensure app is up")
 | 
			
		||||
            - [X] Package app (docker image, helm chart)
 | 
			
		||||
            - [X] Push to registry
 | 
			
		||||
            - [ ] Push only if staging or prod
 | 
			
		||||
            - [ ] Deploy to local k3d when target is local
 | 
			
		||||
            - [ ] Poke Argo
 | 
			
		||||
            - [ ] Ensure app is up")
 | 
			
		||||
    }
 | 
			
		||||
    fn name(&self) -> String {
 | 
			
		||||
        "ContinuousDelivery".to_string()
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ use async_trait::async_trait;
 | 
			
		||||
use log::info;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    modules::application::ApplicationFeature,
 | 
			
		||||
    modules::application::{Application, ApplicationFeature},
 | 
			
		||||
    topology::{K8sclient, Topology},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ use async_trait::async_trait;
 | 
			
		||||
use log::info;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    modules::application::ApplicationFeature,
 | 
			
		||||
    modules::application::{Application, ApplicationFeature},
 | 
			
		||||
    topology::{HelmCommand, Topology},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,12 @@
 | 
			
		||||
mod feature;
 | 
			
		||||
pub mod features;
 | 
			
		||||
pub mod oci;
 | 
			
		||||
mod rust;
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
pub use feature::*;
 | 
			
		||||
use log::info;
 | 
			
		||||
pub use oci::*;
 | 
			
		||||
pub use rust::*;
 | 
			
		||||
 | 
			
		||||
use async_trait::async_trait;
 | 
			
		||||
@ -21,7 +25,7 @@ pub trait Application: std::fmt::Debug + Send + Sync {
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct ApplicationInterpret<T: Topology + std::fmt::Debug> {
 | 
			
		||||
    features: Vec<Box<dyn ApplicationFeature<T>>>,
 | 
			
		||||
    application: Box<dyn Application>,
 | 
			
		||||
    application: Arc<Box<dyn Application>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[async_trait]
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										21
									
								
								harmony/src/modules/application/oci.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								harmony/src/modules/application/oci.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
use async_trait::async_trait;
 | 
			
		||||
 | 
			
		||||
use super::Application;
 | 
			
		||||
 | 
			
		||||
#[async_trait]
 | 
			
		||||
pub trait OCICompliant: Application {
 | 
			
		||||
    async fn build_push_oci_image(&self) -> Result<String, String>; // TODO consider using oci-spec and friends crates here
 | 
			
		||||
 | 
			
		||||
    fn image_name(&self) -> String;
 | 
			
		||||
 | 
			
		||||
    fn local_image_name(&self) -> String;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[async_trait]
 | 
			
		||||
pub trait HelmPackage: Application {
 | 
			
		||||
    /// Generates, packages, and pushes a Helm chart for the web application to an OCI registry.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Arguments
 | 
			
		||||
    /// * `image_url` - The full URL of the OCI container image to be used in the Deployment.
 | 
			
		||||
    async fn build_push_helm_package(&self, image_url: &str) -> Result<String, String>;
 | 
			
		||||
}
 | 
			
		||||
@ -1,26 +1,36 @@
 | 
			
		||||
use std::fs;
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
use std::process;
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
use async_trait::async_trait;
 | 
			
		||||
use dockerfile_builder::Dockerfile;
 | 
			
		||||
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR};
 | 
			
		||||
use dockerfile_builder::instruction_builder::CopyBuilder;
 | 
			
		||||
use log::{debug, error, info};
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
 | 
			
		||||
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
 | 
			
		||||
use crate::{
 | 
			
		||||
    score::Score,
 | 
			
		||||
    topology::{Topology, Url},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::{Application, ApplicationFeature, ApplicationInterpret};
 | 
			
		||||
use super::{Application, ApplicationFeature, ApplicationInterpret, HelmPackage, OCICompliant};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Clone)]
 | 
			
		||||
pub struct RustWebappScore<T: Topology + Clone + Serialize> {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub domain: Url,
 | 
			
		||||
    pub features: Vec<Box<dyn ApplicationFeature<T>>>,
 | 
			
		||||
    pub application: RustWebapp,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T: Topology + std::fmt::Debug + Clone + Serialize + 'static> Score<T> for RustWebappScore<T> {
 | 
			
		||||
    fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
 | 
			
		||||
        Box::new(ApplicationInterpret {
 | 
			
		||||
            features: self.features.clone(),
 | 
			
		||||
            application: Box::new(RustWebapp {
 | 
			
		||||
                name: self.name.clone(),
 | 
			
		||||
            }),
 | 
			
		||||
            application: Arc::new(Box::new(self.application.clone())),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -29,9 +39,17 @@ impl<T: Topology + std::fmt::Debug + Clone + Serialize + 'static> Score<T> for R
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
struct RustWebapp {
 | 
			
		||||
    name: String,
 | 
			
		||||
#[derive(Debug, Clone, Serialize)]
 | 
			
		||||
pub enum RustWebFramework {
 | 
			
		||||
    Leptos,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Serialize)]
 | 
			
		||||
pub struct RustWebapp {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    /// The path to the root of the Rust project to be containerized.
 | 
			
		||||
    pub project_root: PathBuf,
 | 
			
		||||
    pub framework: Option<RustWebFramework>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Application for RustWebapp {
 | 
			
		||||
@ -39,3 +57,517 @@ impl Application for RustWebapp {
 | 
			
		||||
        self.name.clone()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[async_trait]
 | 
			
		||||
impl HelmPackage for RustWebapp {
 | 
			
		||||
    async fn build_push_helm_package(&self, image_url: &str) -> Result<String, String> {
 | 
			
		||||
        info!("Starting Helm chart build and push for '{}'", self.name);
 | 
			
		||||
 | 
			
		||||
        // 1. Create the Helm chart files on disk.
 | 
			
		||||
        let chart_dir = self
 | 
			
		||||
            .create_helm_chart_files(image_url)
 | 
			
		||||
            .map_err(|e| format!("Failed to create Helm chart files: {}", e))?;
 | 
			
		||||
        info!("Successfully created Helm chart files in {:?}", chart_dir);
 | 
			
		||||
 | 
			
		||||
        // 2. Package the chart into a .tgz archive.
 | 
			
		||||
        let packaged_chart_path = self
 | 
			
		||||
            .package_helm_chart(&chart_dir)
 | 
			
		||||
            .map_err(|e| format!("Failed to package Helm chart: {}", e))?;
 | 
			
		||||
        info!(
 | 
			
		||||
            "Successfully packaged Helm chart: {}",
 | 
			
		||||
            packaged_chart_path.to_string_lossy()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // 3. Push the packaged chart to the OCI registry.
 | 
			
		||||
        let oci_chart_url = self
 | 
			
		||||
            .push_helm_chart(&packaged_chart_path)
 | 
			
		||||
            .map_err(|e| format!("Failed to push Helm chart: {}", e))?;
 | 
			
		||||
        info!("Successfully pushed Helm chart to: {}", oci_chart_url);
 | 
			
		||||
 | 
			
		||||
        Ok(oci_chart_url)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[async_trait]
 | 
			
		||||
impl OCICompliant for RustWebapp {
 | 
			
		||||
    /// Builds a Docker image for the Rust web application using a multi-stage build,
 | 
			
		||||
    /// pushes it to the configured OCI registry, and returns the full image tag.
 | 
			
		||||
    async fn build_push_oci_image(&self) -> Result<String, String> {
 | 
			
		||||
        // This function orchestrates the build and push process.
 | 
			
		||||
        // It's async to match the trait definition, though the underlying docker commands are blocking.
 | 
			
		||||
        info!("Starting OCI image build and push for '{}'", self.name);
 | 
			
		||||
 | 
			
		||||
        // 1. Build the local image by calling the synchronous helper function.
 | 
			
		||||
        let local_image_name = self.local_image_name();
 | 
			
		||||
        self.build_docker_image(&local_image_name)
 | 
			
		||||
            .map_err(|e| format!("Failed to build Docker image: {}", e))?;
 | 
			
		||||
        info!(
 | 
			
		||||
            "Successfully built local Docker image: {}",
 | 
			
		||||
            local_image_name
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let remote_image_name = self.image_name();
 | 
			
		||||
        // 2. Push the image to the registry.
 | 
			
		||||
        self.push_docker_image(&local_image_name, &remote_image_name)
 | 
			
		||||
            .map_err(|e| format!("Failed to push Docker image: {}", e))?;
 | 
			
		||||
        info!("Successfully pushed Docker image to: {}", remote_image_name);
 | 
			
		||||
 | 
			
		||||
        Ok(remote_image_name)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn local_image_name(&self) -> String {
 | 
			
		||||
        self.name.clone()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn image_name(&self) -> String {
 | 
			
		||||
        format!(
 | 
			
		||||
            "{}/{}/{}",
 | 
			
		||||
            *REGISTRY_URL,
 | 
			
		||||
            *REGISTRY_PROJECT,
 | 
			
		||||
            &self.local_image_name()
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Implementation of helper methods for building and pushing the Docker image.
 | 
			
		||||
impl RustWebapp {
 | 
			
		||||
    /// Generates a multi-stage Dockerfile for a Rust application.
 | 
			
		||||
    fn build_dockerfile(&self) -> Result<PathBuf, Box<dyn std::error::Error>> {
 | 
			
		||||
        let mut dockerfile = Dockerfile::new();
 | 
			
		||||
 | 
			
		||||
        self.build_builder_image(&mut dockerfile);
 | 
			
		||||
 | 
			
		||||
        // Save the Dockerfile to a uniquely named file in the project root to avoid conflicts.
 | 
			
		||||
        let dockerfile_path = self.project_root.join("Dockerfile.harmony");
 | 
			
		||||
        fs::write(&dockerfile_path, dockerfile.to_string())?;
 | 
			
		||||
 | 
			
		||||
        Ok(dockerfile_path)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Builds the Docker image using the generated Dockerfile.
 | 
			
		||||
    pub fn build_docker_image(
 | 
			
		||||
        &self,
 | 
			
		||||
        image_name: &str,
 | 
			
		||||
    ) -> Result<String, Box<dyn std::error::Error>> {
 | 
			
		||||
        info!("Generating Dockerfile for '{}'", self.name);
 | 
			
		||||
        let dockerfile_path = self.build_dockerfile()?;
 | 
			
		||||
 | 
			
		||||
        info!(
 | 
			
		||||
            "Building Docker image with file {} from root {}",
 | 
			
		||||
            dockerfile_path.to_string_lossy(),
 | 
			
		||||
            self.project_root.to_string_lossy()
 | 
			
		||||
        );
 | 
			
		||||
        let output = process::Command::new("docker")
 | 
			
		||||
            .args([
 | 
			
		||||
                "build",
 | 
			
		||||
                "--file",
 | 
			
		||||
                dockerfile_path.to_str().unwrap(),
 | 
			
		||||
                "-t",
 | 
			
		||||
                &image_name,
 | 
			
		||||
                self.project_root.to_str().unwrap(),
 | 
			
		||||
            ])
 | 
			
		||||
            .spawn()?
 | 
			
		||||
            .wait_with_output()?;
 | 
			
		||||
 | 
			
		||||
        self.check_output(&output, "Failed to build Docker image")?;
 | 
			
		||||
 | 
			
		||||
        Ok(image_name.to_string())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Tags and pushes a Docker image to the configured remote registry.
 | 
			
		||||
    fn push_docker_image(
 | 
			
		||||
        &self,
 | 
			
		||||
        image_name: &str,
 | 
			
		||||
        full_tag: &str,
 | 
			
		||||
    ) -> Result<String, Box<dyn std::error::Error>> {
 | 
			
		||||
        info!("Pushing docker image {full_tag}");
 | 
			
		||||
 | 
			
		||||
        // Tag the image for the remote registry.
 | 
			
		||||
        let output = process::Command::new("docker")
 | 
			
		||||
            .args(["tag", image_name, &full_tag])
 | 
			
		||||
            .spawn()?
 | 
			
		||||
            .wait_with_output()?;
 | 
			
		||||
        self.check_output(&output, "Tagging docker image failed")?;
 | 
			
		||||
        debug!(
 | 
			
		||||
            "docker tag output: stdout: {}, stderr: {}",
 | 
			
		||||
            String::from_utf8_lossy(&output.stdout),
 | 
			
		||||
            String::from_utf8_lossy(&output.stderr)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Push the image.
 | 
			
		||||
        let output = process::Command::new("docker")
 | 
			
		||||
            .args(["push", &full_tag])
 | 
			
		||||
            .spawn()?
 | 
			
		||||
            .wait_with_output()?;
 | 
			
		||||
        self.check_output(&output, "Pushing docker image failed")?;
 | 
			
		||||
        debug!(
 | 
			
		||||
            "docker push output: stdout: {}, stderr: {}",
 | 
			
		||||
            String::from_utf8_lossy(&output.stdout),
 | 
			
		||||
            String::from_utf8_lossy(&output.stderr)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        Ok(full_tag.to_string())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Checks the output of a process command for success.
 | 
			
		||||
    fn check_output(
 | 
			
		||||
        &self,
 | 
			
		||||
        output: &process::Output,
 | 
			
		||||
        msg: &str,
 | 
			
		||||
    ) -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
        if !output.status.success() {
 | 
			
		||||
            let error_message = format!("{}: {}", msg, String::from_utf8_lossy(&output.stderr));
 | 
			
		||||
            return Err(error_message.into());
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn build_builder_image(&self, dockerfile: &mut Dockerfile) {
 | 
			
		||||
        match self.framework {
 | 
			
		||||
            Some(RustWebFramework::Leptos) => {
 | 
			
		||||
                // --- Stage 1: Builder for Leptos ---
 | 
			
		||||
                dockerfile.push(FROM::from("rust:bookworm as builder"));
 | 
			
		||||
 | 
			
		||||
                // Install dependencies, cargo-binstall, and clean up in one layer
 | 
			
		||||
                dockerfile.push(RUN::from(
 | 
			
		||||
                    "apt-get update && \
 | 
			
		||||
                     apt-get install -y --no-install-recommends clang wget && \
 | 
			
		||||
                     wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz && \
 | 
			
		||||
                     tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz && \
 | 
			
		||||
                     cp cargo-binstall /usr/local/cargo/bin && \
 | 
			
		||||
                     rm cargo-binstall-x86_64-unknown-linux-musl.tgz cargo-binstall && \
 | 
			
		||||
                     apt-get clean && \
 | 
			
		||||
                     rm -rf /var/lib/apt/lists/*"
 | 
			
		||||
                ));
 | 
			
		||||
 | 
			
		||||
                // Install cargo-leptos
 | 
			
		||||
                dockerfile.push(RUN::from("cargo binstall cargo-leptos -y"));
 | 
			
		||||
 | 
			
		||||
                // Add the WASM target
 | 
			
		||||
                dockerfile.push(RUN::from("rustup target add wasm32-unknown-unknown"));
 | 
			
		||||
 | 
			
		||||
                // Set up workdir, copy source, and build
 | 
			
		||||
                dockerfile.push(WORKDIR::from("/app"));
 | 
			
		||||
                dockerfile.push(COPY::from(". ."));
 | 
			
		||||
                dockerfile.push(RUN::from("cargo leptos build --release -vv"));
 | 
			
		||||
                // --- Stage 2: Final Image ---
 | 
			
		||||
                dockerfile.push(FROM::from("debian:bookworm-slim"));
 | 
			
		||||
 | 
			
		||||
                // Create a non-root user for security.
 | 
			
		||||
                dockerfile.push(RUN::from(
 | 
			
		||||
                    "groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser",
 | 
			
		||||
                ));
 | 
			
		||||
 | 
			
		||||
                dockerfile.push(ENV::from("LEPTOS_SITE_ADDR=0.0.0.0:3000"));
 | 
			
		||||
                dockerfile.push(EXPOSE::from("3000/tcp"));
 | 
			
		||||
                dockerfile.push(WORKDIR::from("/home/appuser"));
 | 
			
		||||
 | 
			
		||||
                // Copy static files
 | 
			
		||||
                dockerfile.push(
 | 
			
		||||
                    CopyBuilder::builder()
 | 
			
		||||
                        .from("builder")
 | 
			
		||||
                        .src("/app/target/site/pkg")
 | 
			
		||||
                        .dest("/home/appuser/pkg")
 | 
			
		||||
                        .build()
 | 
			
		||||
                        .unwrap(),
 | 
			
		||||
                );
 | 
			
		||||
                // Copy the compiled binary from the builder stage.
 | 
			
		||||
                error!(
 | 
			
		||||
                    "FIXME Should not be using score name here, instead should use name from Cargo.toml"
 | 
			
		||||
                );
 | 
			
		||||
                let binary_path_in_builder = format!("/app/target/release/{}", self.name);
 | 
			
		||||
                let binary_path_in_final = format!("/home/appuser/{}", self.name);
 | 
			
		||||
                dockerfile.push(
 | 
			
		||||
                    CopyBuilder::builder()
 | 
			
		||||
                        .from("builder")
 | 
			
		||||
                        .src(binary_path_in_builder)
 | 
			
		||||
                        .dest(&binary_path_in_final)
 | 
			
		||||
                        .build()
 | 
			
		||||
                        .unwrap(),
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                // Run as the non-root user.
 | 
			
		||||
                dockerfile.push(USER::from("appuser"));
 | 
			
		||||
 | 
			
		||||
                // Set the command to run the application.
 | 
			
		||||
                dockerfile.push(CMD::from(binary_path_in_final));
 | 
			
		||||
            }
 | 
			
		||||
            None => {
 | 
			
		||||
                // --- Stage 1: Builder for a generic Rust app ---
 | 
			
		||||
                dockerfile.push(FROM::from("rust:latest as builder"));
 | 
			
		||||
 | 
			
		||||
                // Install the wasm32 target as required.
 | 
			
		||||
                dockerfile.push(RUN::from("rustup target add wasm32-unknown-unknown"));
 | 
			
		||||
                dockerfile.push(WORKDIR::from("/app"));
 | 
			
		||||
 | 
			
		||||
                // Copy the source code and build the application.
 | 
			
		||||
                dockerfile.push(COPY::from(". ."));
 | 
			
		||||
                dockerfile.push(RUN::from("cargo build --release --locked"));
 | 
			
		||||
                // --- Stage 2: Final Image ---
 | 
			
		||||
                dockerfile.push(FROM::from("debian:bookworm-slim"));
 | 
			
		||||
 | 
			
		||||
                // Create a non-root user for security.
 | 
			
		||||
                dockerfile.push(RUN::from(
 | 
			
		||||
                    "groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser",
 | 
			
		||||
                ));
 | 
			
		||||
 | 
			
		||||
                // Copy only the compiled binary from the builder stage.
 | 
			
		||||
                error!(
 | 
			
		||||
                    "FIXME Should not be using score name here, instead should use name from Cargo.toml"
 | 
			
		||||
                );
 | 
			
		||||
                let binary_path_in_builder = format!("/app/target/release/{}", self.name);
 | 
			
		||||
                let binary_path_in_final = format!("/usr/local/bin/{}", self.name);
 | 
			
		||||
                dockerfile.push(
 | 
			
		||||
                    CopyBuilder::builder()
 | 
			
		||||
                        .from("builder")
 | 
			
		||||
                        .src(binary_path_in_builder)
 | 
			
		||||
                        .dest(&binary_path_in_final)
 | 
			
		||||
                        .build()
 | 
			
		||||
                        .unwrap(),
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                // Run as the non-root user.
 | 
			
		||||
                dockerfile.push(USER::from("appuser"));
 | 
			
		||||
 | 
			
		||||
                // Set the command to run the application.
 | 
			
		||||
                dockerfile.push(CMD::from(binary_path_in_final));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Creates all necessary files for a basic Helm chart.
 | 
			
		||||
    fn create_helm_chart_files(
 | 
			
		||||
        &self,
 | 
			
		||||
        image_url: &str,
 | 
			
		||||
    ) -> Result<PathBuf, Box<dyn std::error::Error>> {
 | 
			
		||||
        let chart_name = format!("{}-chart", self.name);
 | 
			
		||||
        let chart_dir = self.project_root.join("helm").join(&chart_name);
 | 
			
		||||
        let templates_dir = chart_dir.join("templates");
 | 
			
		||||
        fs::create_dir_all(&templates_dir)?;
 | 
			
		||||
 | 
			
		||||
        let (image_repo, image_tag) = image_url.rsplit_once(':').unwrap_or((image_url, "latest"));
 | 
			
		||||
 | 
			
		||||
        // Create Chart.yaml
 | 
			
		||||
        let chart_yaml = format!(
 | 
			
		||||
            r#"
 | 
			
		||||
apiVersion: v2
 | 
			
		||||
name: {}
 | 
			
		||||
description: A Helm chart for the {} web application.
 | 
			
		||||
type: application
 | 
			
		||||
version: 0.1.0
 | 
			
		||||
appVersion: "{}"
 | 
			
		||||
"#,
 | 
			
		||||
            chart_name, self.name, image_tag
 | 
			
		||||
        );
 | 
			
		||||
        fs::write(chart_dir.join("Chart.yaml"), chart_yaml)?;
 | 
			
		||||
 | 
			
		||||
        // Create values.yaml
 | 
			
		||||
        let values_yaml = format!(
 | 
			
		||||
            r#"
 | 
			
		||||
# Default values for {}.
 | 
			
		||||
# This is a YAML-formatted file.
 | 
			
		||||
# Declare variables to be passed into your templates.
 | 
			
		||||
 | 
			
		||||
replicaCount: 1
 | 
			
		||||
 | 
			
		||||
image:
 | 
			
		||||
  repository: {}
 | 
			
		||||
  pullPolicy: IfNotPresent
 | 
			
		||||
  # Overridden by the chart's appVersion
 | 
			
		||||
  tag: "{}"
 | 
			
		||||
 | 
			
		||||
service:
 | 
			
		||||
  type: ClusterIP
 | 
			
		||||
  port: 3000
 | 
			
		||||
 | 
			
		||||
ingress:
 | 
			
		||||
  enabled: true
 | 
			
		||||
  # Annotations for cert-manager to handle SSL.
 | 
			
		||||
  annotations:
 | 
			
		||||
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
 | 
			
		||||
    # Add other annotations like nginx ingress class if needed
 | 
			
		||||
    # kubernetes.io/ingress.class: nginx
 | 
			
		||||
  hosts:
 | 
			
		||||
    - host: chart-example.local
 | 
			
		||||
      paths:
 | 
			
		||||
        - path: /
 | 
			
		||||
          pathType: ImplementationSpecific
 | 
			
		||||
  tls:
 | 
			
		||||
   - secretName: {}-tls
 | 
			
		||||
     hosts:
 | 
			
		||||
       - chart-example.local
 | 
			
		||||
 | 
			
		||||
"#,
 | 
			
		||||
            chart_name, image_repo, image_tag, self.name
 | 
			
		||||
        );
 | 
			
		||||
        fs::write(chart_dir.join("values.yaml"), values_yaml)?;
 | 
			
		||||
 | 
			
		||||
        // Create templates/_helpers.tpl
 | 
			
		||||
        let helpers_tpl = r#"
 | 
			
		||||
{{/*
 | 
			
		||||
Expand the name of the chart.
 | 
			
		||||
*/}}
 | 
			
		||||
{{- define "chart.name" -}}
 | 
			
		||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
 | 
			
		||||
{{- end }}
 | 
			
		||||
 | 
			
		||||
{{/*
 | 
			
		||||
Create a default fully qualified app name.
 | 
			
		||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
 | 
			
		||||
*/}}
 | 
			
		||||
{{- define "chart.fullname" -}}
 | 
			
		||||
{{- $name := default .Chart.Name .Values.nameOverride }}
 | 
			
		||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
 | 
			
		||||
{{- end }}
 | 
			
		||||
"#;
 | 
			
		||||
        fs::write(templates_dir.join("_helpers.tpl"), helpers_tpl)?;
 | 
			
		||||
 | 
			
		||||
        // Create templates/service.yaml
 | 
			
		||||
        let service_yaml = r#"
 | 
			
		||||
apiVersion: v1
 | 
			
		||||
kind: Service
 | 
			
		||||
metadata:
 | 
			
		||||
  name: {{ include "chart.fullname" . }}
 | 
			
		||||
spec:
 | 
			
		||||
  type: {{ .Values.service.type }}
 | 
			
		||||
  ports:
 | 
			
		||||
    - port: {{ .Values.service.port }}
 | 
			
		||||
      targetPort: 3000
 | 
			
		||||
      protocol: TCP
 | 
			
		||||
      name: http
 | 
			
		||||
  selector:
 | 
			
		||||
    app: {{ include "chart.name" . }}
 | 
			
		||||
"#;
 | 
			
		||||
        fs::write(templates_dir.join("service.yaml"), service_yaml)?;
 | 
			
		||||
 | 
			
		||||
        // Create templates/deployment.yaml
 | 
			
		||||
        let deployment_yaml = r#"
 | 
			
		||||
apiVersion: apps/v1
 | 
			
		||||
kind: Deployment
 | 
			
		||||
metadata:
 | 
			
		||||
  name: {{ include "chart.fullname" . }}
 | 
			
		||||
spec:
 | 
			
		||||
  replicas: {{ .Values.replicaCount }}
 | 
			
		||||
  selector:
 | 
			
		||||
    matchLabels:
 | 
			
		||||
      app: {{ include "chart.name" . }}
 | 
			
		||||
  template:
 | 
			
		||||
    metadata:
 | 
			
		||||
      labels:
 | 
			
		||||
        app: {{ include "chart.name" . }}
 | 
			
		||||
    spec:
 | 
			
		||||
      containers:
 | 
			
		||||
        - name: {{ .Chart.Name }}
 | 
			
		||||
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
 | 
			
		||||
          imagePullPolicy: {{ .Values.image.pullPolicy }}
 | 
			
		||||
          ports:
 | 
			
		||||
            - name: http
 | 
			
		||||
              containerPort: 3000
 | 
			
		||||
              protocol: TCP
 | 
			
		||||
"#;
 | 
			
		||||
        fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?;
 | 
			
		||||
 | 
			
		||||
        // Create templates/ingress.yaml
 | 
			
		||||
        let ingress_yaml = r#"
 | 
			
		||||
{{- if .Values.ingress.enabled -}}
 | 
			
		||||
apiVersion: networking.k8s.io/v1
 | 
			
		||||
kind: Ingress
 | 
			
		||||
metadata:
 | 
			
		||||
  name: {{ include "chart.fullname" . }}
 | 
			
		||||
  annotations:
 | 
			
		||||
    {{- toYaml .Values.ingress.annotations | nindent 4 }}
 | 
			
		||||
spec:
 | 
			
		||||
  {{- if .Values.ingress.tls }}
 | 
			
		||||
  tls:
 | 
			
		||||
    {{- range .Values.ingress.tls }}
 | 
			
		||||
    - hosts:
 | 
			
		||||
        {{- range .hosts }}
 | 
			
		||||
        - {{ . | quote }}
 | 
			
		||||
        {{- end }}
 | 
			
		||||
      secretName: {{ .secretName }}
 | 
			
		||||
    {{- end }}
 | 
			
		||||
  {{- end }}
 | 
			
		||||
  rules:
 | 
			
		||||
    {{- range .Values.ingress.hosts }}
 | 
			
		||||
    - host: {{ .host | quote }}
 | 
			
		||||
      http:
 | 
			
		||||
        paths:
 | 
			
		||||
          {{- range .paths }}
 | 
			
		||||
          - path: {{ .path }}
 | 
			
		||||
            pathType: {{ .pathType }}
 | 
			
		||||
            backend:
 | 
			
		||||
              service:
 | 
			
		||||
                name: {{ include "chart.fullname" $ }}
 | 
			
		||||
                port:
 | 
			
		||||
                  number: 3000
 | 
			
		||||
          {{- end }}
 | 
			
		||||
    {{- end }}
 | 
			
		||||
{{- end }}
 | 
			
		||||
"#;
 | 
			
		||||
        fs::write(templates_dir.join("ingress.yaml"), ingress_yaml)?;
 | 
			
		||||
 | 
			
		||||
        Ok(chart_dir)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Packages a Helm chart directory into a .tgz file.
 | 
			
		||||
    fn package_helm_chart(
 | 
			
		||||
        &self,
 | 
			
		||||
        chart_dir: &PathBuf,
 | 
			
		||||
    ) -> Result<PathBuf, Box<dyn std::error::Error>> {
 | 
			
		||||
        let chart_dirname = chart_dir.file_name().expect("Should find a chart dirname");
 | 
			
		||||
        info!(
 | 
			
		||||
            "Launching `helm package {}` cli with CWD {}",
 | 
			
		||||
            chart_dirname.to_string_lossy(),
 | 
			
		||||
            &self.project_root.join("helm").to_string_lossy()
 | 
			
		||||
        );
 | 
			
		||||
        let output = process::Command::new("helm")
 | 
			
		||||
            .args(["package", chart_dirname.to_str().unwrap()])
 | 
			
		||||
            .current_dir(&self.project_root.join("helm")) // Run package from the parent dir
 | 
			
		||||
            .output()?;
 | 
			
		||||
 | 
			
		||||
        self.check_output(&output, "Failed to package Helm chart")?;
 | 
			
		||||
 | 
			
		||||
        // Helm prints the path of the created chart to stdout.
 | 
			
		||||
        let tgz_name = String::from_utf8(output.stdout)?
 | 
			
		||||
            .trim()
 | 
			
		||||
            .split_whitespace()
 | 
			
		||||
            .last()
 | 
			
		||||
            .unwrap_or_default()
 | 
			
		||||
            .to_string();
 | 
			
		||||
        if tgz_name.is_empty() {
 | 
			
		||||
            return Err("Could not determine packaged chart filename.".into());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // The output from helm is relative, so we join it with the execution directory.
 | 
			
		||||
        Ok(self.project_root.join("helm").join(tgz_name))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Pushes a packaged Helm chart to an OCI registry.
 | 
			
		||||
    fn push_helm_chart(
 | 
			
		||||
        &self,
 | 
			
		||||
        packaged_chart_path: &PathBuf,
 | 
			
		||||
    ) -> Result<String, Box<dyn std::error::Error>> {
 | 
			
		||||
        // The chart name is the file stem of the .tgz file
 | 
			
		||||
        let chart_file_name = packaged_chart_path.file_stem().unwrap().to_str().unwrap();
 | 
			
		||||
        let oci_push_url = format!("oci://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT);
 | 
			
		||||
        let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name);
 | 
			
		||||
 | 
			
		||||
        info!(
 | 
			
		||||
            "Pushing Helm chart {} to {}",
 | 
			
		||||
            packaged_chart_path.to_string_lossy(),
 | 
			
		||||
            oci_push_url
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let output = process::Command::new("helm")
 | 
			
		||||
            .args(["push", packaged_chart_path.to_str().unwrap(), &oci_push_url])
 | 
			
		||||
            .output()?;
 | 
			
		||||
 | 
			
		||||
        self.check_output(&output, "Pushing Helm chart failed")?;
 | 
			
		||||
 | 
			
		||||
        // The final URL includes the version tag, which is part of the file name
 | 
			
		||||
        let version = chart_file_name.rsplit_once('-').unwrap().1;
 | 
			
		||||
        debug!("pull url {oci_pull_url}");
 | 
			
		||||
        debug!("push url {oci_push_url}");
 | 
			
		||||
        Ok(format!("{}:{}", oci_pull_url, version))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ use log::info;
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    config::HARMONY_CONFIG_DIR,
 | 
			
		||||
    config::HARMONY_DATA_DIR,
 | 
			
		||||
    data::{Id, Version},
 | 
			
		||||
    interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
 | 
			
		||||
    inventory::Inventory,
 | 
			
		||||
@ -22,7 +22,7 @@ pub struct K3DInstallationScore {
 | 
			
		||||
impl Default for K3DInstallationScore {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            installation_path: HARMONY_CONFIG_DIR.join("k3d"),
 | 
			
		||||
            installation_path: HARMONY_DATA_DIR.join("k3d"),
 | 
			
		||||
            cluster_name: "harmony".to_string(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -153,7 +153,7 @@ mod test {
 | 
			
		||||
    fn init_test_maestro() -> Maestro<HAClusterTopology> {
 | 
			
		||||
        let inventory = Inventory::autoload();
 | 
			
		||||
        let topology = HAClusterTopology::autoload();
 | 
			
		||||
        let mut maestro = Maestro::new(inventory, topology);
 | 
			
		||||
        let mut maestro = Maestro::new_without_initialization(inventory, topology);
 | 
			
		||||
 | 
			
		||||
        maestro.register_all(vec![
 | 
			
		||||
            Box::new(SuccessScore {}),
 | 
			
		||||
 | 
			
		||||
@ -41,7 +41,7 @@ pub mod tui {
 | 
			
		||||
/// async fn main() {
 | 
			
		||||
///     let inventory = Inventory::autoload();
 | 
			
		||||
///     let topology = HAClusterTopology::autoload();
 | 
			
		||||
///     let mut maestro = Maestro::new(inventory, topology);
 | 
			
		||||
///     let mut maestro = Maestro::new_without_initialization(inventory, topology);
 | 
			
		||||
///
 | 
			
		||||
///     maestro.register_all(vec![
 | 
			
		||||
///         Box::new(SuccessScore {}),
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user