feat(adr): add architecture decision record for interactive project setup #6

Closed
johnride wants to merge 0 commits from adr/005-interactive-project into master
Owner

Add an Architecture Decision Record (ADR) outlining the approach to integrate LAMP projects into Harmony's automated delivery pipeline using either Score Spec or a custom Rust DSL. A decision will have to be made between the two in the short term to decide which we will implement first. The ADR details the benefits and consequences of each option, focusing on providing a seamless transition for developers while leveraging Harmony's enterprise-grade features.

Add an Architecture Decision Record (ADR) outlining the approach to integrate LAMP projects into Harmony's automated delivery pipeline using either Score Spec or a custom Rust DSL. A decision will have to be made between the two in the short term to decide which we will implement first. The ADR details the benefits and consequences of each option, focusing on providing a seamless transition for developers while leveraging Harmony's enterprise-grade features.
johnride added the
RFC
Architecture Decision Record
labels 2025-03-10 04:37:28 +00:00
johnride added 1 commit 2025-03-10 04:37:29 +00:00
Add an Architecture Decision Record (ADR) outlining the approach to integrate LAMP projects into Harmony's automated delivery pipeline using either Score Spec or a custom Rust DSL. A decision will have to be made between the two in the short term to decide which we will implement first. The ADR details the benefits and consequences of each option, focusing on providing a seamless transition for developers while leveraging Harmony's enterprise-grade features.
Author
Owner

I've had a little chat with Claude 3.7 trying to figure out what each option would look like for three different scenarios :

  1. Basic LAMP stack with the php code in the src/ folder
  2. Regular Svelte app created with the default sv tool
  3. Enterprise grade Java monolith

Claude 3.7 example of what it could look like with score vs rust

I'll create concrete sample configurations for each of the three project types in both Rust DSL and Score Spec formats.

1. Basic LAMP Stack

Score Spec Version

apiVersion: score.dev/v1b1
metadata:
  name: lamp-app
service:
  containers:
    web:
      image: php:8.2-apache
      volumes:
        - source: ./src
          target: /var/www/html
      resources:
        cpu: 0.5
        memory: 1Gi
      ports:
        - port: 80
          type: http
      env:
        DB_HOST: ${resources.db.hostname}
        DB_USER: ${resources.db.username}
        DB_PASSWORD: ${resources.db.password}
        DB_NAME: ${resources.db.database}
  resources:
    db:
      type: mysql
      version: "8.0"
      properties:
        database: lamp_db
        username: lamp_user
        password: secret
  volumes:
    mysql-data:
      size: 10Gi

Rust DSL Version

#[harmony::service(name = "lamp-app")]
fn define_service() -> Service {
    let php_container = Container::new("web")
        .image("php:8.2-apache")
        .mount("./src", "/var/www/html")
        .resources(Resources::new().cpu(0.5).memory("1Gi"))
        .port(80, PortType::Http)
        .env("DB_HOST", "${resources.db.hostname}")
        .env("DB_USER", "${resources.db.username}")
        .env("DB_PASSWORD", "${resources.db.password}")
        .env("DB_NAME", "${resources.db.database}");
    
    let mysql = Resource::new("db")
        .type_("mysql")
        .version("8.0")
        .property("database", "lamp_db")
        .property("username", "lamp_user")
        .property("password", "secret");
    
    let mysql_volume = Volume::new("mysql-data")
        .size("10Gi");
    
    Service::new()
        .container(php_container)
        .resource(mysql)
        .volume(mysql_volume)
}

2. Regular Svelte App

Score Spec Version

apiVersion: score.dev/v1b1
metadata:
  name: svelte-app
service:
  containers:
    app:
      image: node:18-alpine
      command: ["npm", "run", "dev"]
      volumes:
        - source: ./
          target: /app
      workingDir: /app
      resources:
        cpu: 0.5
        memory: 512Mi
      ports:
        - port: 5173
          type: http
      env:
        NODE_ENV: development
        VITE_API_URL: ${resources.api.url}
  resources:
    api:
      type: external-service
      properties:
        url: https://api.example.com
  workload:
    livenessProbe:
      httpGet:
        path: /
        port: 5173
      initialDelaySeconds: 10
    readinessProbe:
      httpGet:
        path: /
        port: 5173
      initialDelaySeconds: 5

Rust DSL Version

#[harmony::service(name = "svelte-app")]
fn define_service() -> Service {
    let node_container = Container::new("app")
        .image("node:18-alpine")
        .command(vec!["npm", "run", "dev"])
        .mount("./", "/app")
        .working_dir("/app")
        .resources(Resources::new().cpu(0.5).memory("512Mi"))
        .port(5173, PortType::Http)
        .env("NODE_ENV", "development")
        .env("VITE_API_URL", "${resources.api.url}");
    
    let api = Resource::new("api")
        .type_("external-service")
        .property("url", "https://api.example.com");
    
    let workload = Workload::new()
        .liveness_probe(Probe::http_get("/", 5173).initial_delay(10))
        .readiness_probe(Probe::http_get("/", 5173).initial_delay(5));
    
    Service::new()
        .container(node_container)
        .resource(api)
        .workload(workload)
}

3. Enterprise Grade Java Monolith

Score Spec Version

apiVersion: score.dev/v1b1
metadata:
  name: java-enterprise-app
service:
  containers:
    app:
      image: eclipse-temurin:17-jre
      command: ["java", "-jar", "-Xms512m", "-Xmx2048m", "-Dspring.profiles.active=${environment}", "/app/application.jar"]
      volumes:
        - source: ./target/application.jar
          target: /app/application.jar
        - source: config-volume
          target: /app/config
      resources:
        cpu: 2
        memory: 4Gi
      ports:
        - port: 8080
          type: http
      env:
        JAVA_OPTS: "-XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs"
        DB_URL: jdbc:postgresql://${resources.db.hostname}:5432/${resources.db.database}
        DB_USER: ${resources.db.username}
        DB_PASSWORD: ${resources.db.password}
        REDIS_HOST: ${resources.cache.hostname}
        LOG_LEVEL: INFO
  resources:
    db:
      type: postgresql
      version: "14"
      properties:
        database: enterprise_db
        username: app_user
        password: secure_password
        highAvailability: true
    cache:
      type: redis
      version: "6.2"
      properties:
        clusterMode: true
        replicaCount: 2
    mq:
      type: rabbitmq
      version: "3.9"
      properties:
        clusterSize: 3
  volumes:
    config-volume:
      size: 1Gi
    logs-volume:
      size: 5Gi
  workload:
    replicas: 3
    livenessProbe:
      httpGet:
        path: /actuator/health/liveness
        port: 8080
      initialDelaySeconds: 90
      periodSeconds: 15
    readinessProbe:
      httpGet:
        path: /actuator/health/readiness
        port: 8080
      initialDelaySeconds: 60
    scaling:
      horizontal:
        minReplicas: 3
        maxReplicas: 10
        metrics:
          - type: cpu
            targetUtilization: 70

Rust DSL Version

#[harmony::service(name = "java-enterprise-app")]
fn define_service() -> Service {
    let java_container = Container::new("app")
        .image("eclipse-temurin:17-jre")
        .command(vec![
            "java", "-jar", "-Xms512m", "-Xmx2048m", 
            "-Dspring.profiles.active=${environment}", "/app/application.jar"
        ])
        .mount("./target/application.jar", "/app/application.jar")
        .mount_volume("config-volume", "/app/config")
        .resources(Resources::new().cpu(2).memory("4Gi"))
        .port(8080, PortType::Http)
        .env("JAVA_OPTS", "-XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs")
        .env("DB_URL", "jdbc:postgresql://${resources.db.hostname}:5432/${resources.db.database}")
        .env("DB_USER", "${resources.db.username}")
        .env("DB_PASSWORD", "${resources.db.password}")
        .env("REDIS_HOST", "${resources.cache.hostname}")
        .env("LOG_LEVEL", "INFO");
    
    let database = Resource::new("db")
        .type_("postgresql")
        .version("14")
        .property("database", "enterprise_db")
        .property("username", "app_user")
        .property("password", "secure_password")
        .property("highAvailability", true);
    
    let cache = Resource::new("cache")
        .type_("redis")
        .version("6.2")
        .property("clusterMode", true)
        .property("replicaCount", 2);
    
    let message_queue = Resource::new("mq")
        .type_("rabbitmq")
        .version("3.9")
        .property("clusterSize", 3);
    
    let config_volume = Volume::new("config-volume")
        .size("1Gi");
    
    let logs_volume = Volume::new("logs-volume")
        .size("5Gi");
    
    let workload = Workload::new()
        .replicas(3)
        .liveness_probe(
            Probe::http_get("/actuator/health/liveness", 8080)
                .initial_delay(90)
                .period(15)
        )
        .readiness_probe(
            Probe::http_get("/actuator/health/readiness", 8080)
                .initial_delay(60)
        )
        .horizontal_scaling(
            HorizontalScaling::new()
                .min_replicas(3)
                .max_replicas(10)
                .metric(ScalingMetric::Cpu, 70)
        );
    
    Service::new()
        .container(java_container)
        .resource(database)
        .resource(cache)
        .resource(message_queue)
        .volume(config_volume)
        .volume(logs_volume)
        .workload(workload)
}

These examples showcase how both approaches can describe the same infrastructure needs, with the Score Spec version being more declarative and YAML-based, while the Rust DSL provides type safety and a more programmatic approach. The Rust DSL would enable better IDE integration, compile-time validation, and extensibility, while Score Spec leverages existing CNCF standards and may be more approachable for developers already familiar with YAML-based configurations.

I've had a little chat with Claude 3.7 trying to figure out what each option would look like for three different scenarios : 1. Basic LAMP stack with the php code in the src/ folder 2. Regular Svelte app created with the default sv tool 3. Enterprise grade Java monolith --- ## Claude 3.7 example of what it _could_ look like with score vs rust I'll create concrete sample configurations for each of the three project types in both Rust DSL and Score Spec formats. ## 1. Basic LAMP Stack ### Score Spec Version ```yaml apiVersion: score.dev/v1b1 metadata: name: lamp-app service: containers: web: image: php:8.2-apache volumes: - source: ./src target: /var/www/html resources: cpu: 0.5 memory: 1Gi ports: - port: 80 type: http env: DB_HOST: ${resources.db.hostname} DB_USER: ${resources.db.username} DB_PASSWORD: ${resources.db.password} DB_NAME: ${resources.db.database} resources: db: type: mysql version: "8.0" properties: database: lamp_db username: lamp_user password: secret volumes: mysql-data: size: 10Gi ``` ### Rust DSL Version ```rust #[harmony::service(name = "lamp-app")] fn define_service() -> Service { let php_container = Container::new("web") .image("php:8.2-apache") .mount("./src", "/var/www/html") .resources(Resources::new().cpu(0.5).memory("1Gi")) .port(80, PortType::Http) .env("DB_HOST", "${resources.db.hostname}") .env("DB_USER", "${resources.db.username}") .env("DB_PASSWORD", "${resources.db.password}") .env("DB_NAME", "${resources.db.database}"); let mysql = Resource::new("db") .type_("mysql") .version("8.0") .property("database", "lamp_db") .property("username", "lamp_user") .property("password", "secret"); let mysql_volume = Volume::new("mysql-data") .size("10Gi"); Service::new() .container(php_container) .resource(mysql) .volume(mysql_volume) } ``` ## 2. Regular Svelte App ### Score Spec Version ```yaml apiVersion: score.dev/v1b1 metadata: name: svelte-app service: containers: app: image: node:18-alpine command: ["npm", "run", "dev"] volumes: - source: ./ target: /app workingDir: /app resources: cpu: 0.5 memory: 512Mi ports: - port: 5173 type: http env: NODE_ENV: development VITE_API_URL: ${resources.api.url} resources: api: type: external-service properties: url: https://api.example.com workload: livenessProbe: httpGet: path: / port: 5173 initialDelaySeconds: 10 readinessProbe: httpGet: path: / port: 5173 initialDelaySeconds: 5 ``` ### Rust DSL Version ```rust #[harmony::service(name = "svelte-app")] fn define_service() -> Service { let node_container = Container::new("app") .image("node:18-alpine") .command(vec!["npm", "run", "dev"]) .mount("./", "/app") .working_dir("/app") .resources(Resources::new().cpu(0.5).memory("512Mi")) .port(5173, PortType::Http) .env("NODE_ENV", "development") .env("VITE_API_URL", "${resources.api.url}"); let api = Resource::new("api") .type_("external-service") .property("url", "https://api.example.com"); let workload = Workload::new() .liveness_probe(Probe::http_get("/", 5173).initial_delay(10)) .readiness_probe(Probe::http_get("/", 5173).initial_delay(5)); Service::new() .container(node_container) .resource(api) .workload(workload) } ``` ## 3. Enterprise Grade Java Monolith ### Score Spec Version ```yaml apiVersion: score.dev/v1b1 metadata: name: java-enterprise-app service: containers: app: image: eclipse-temurin:17-jre command: ["java", "-jar", "-Xms512m", "-Xmx2048m", "-Dspring.profiles.active=${environment}", "/app/application.jar"] volumes: - source: ./target/application.jar target: /app/application.jar - source: config-volume target: /app/config resources: cpu: 2 memory: 4Gi ports: - port: 8080 type: http env: JAVA_OPTS: "-XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs" DB_URL: jdbc:postgresql://${resources.db.hostname}:5432/${resources.db.database} DB_USER: ${resources.db.username} DB_PASSWORD: ${resources.db.password} REDIS_HOST: ${resources.cache.hostname} LOG_LEVEL: INFO resources: db: type: postgresql version: "14" properties: database: enterprise_db username: app_user password: secure_password highAvailability: true cache: type: redis version: "6.2" properties: clusterMode: true replicaCount: 2 mq: type: rabbitmq version: "3.9" properties: clusterSize: 3 volumes: config-volume: size: 1Gi logs-volume: size: 5Gi workload: replicas: 3 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 90 periodSeconds: 15 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 60 scaling: horizontal: minReplicas: 3 maxReplicas: 10 metrics: - type: cpu targetUtilization: 70 ``` ### Rust DSL Version ```rust #[harmony::service(name = "java-enterprise-app")] fn define_service() -> Service { let java_container = Container::new("app") .image("eclipse-temurin:17-jre") .command(vec![ "java", "-jar", "-Xms512m", "-Xmx2048m", "-Dspring.profiles.active=${environment}", "/app/application.jar" ]) .mount("./target/application.jar", "/app/application.jar") .mount_volume("config-volume", "/app/config") .resources(Resources::new().cpu(2).memory("4Gi")) .port(8080, PortType::Http) .env("JAVA_OPTS", "-XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs") .env("DB_URL", "jdbc:postgresql://${resources.db.hostname}:5432/${resources.db.database}") .env("DB_USER", "${resources.db.username}") .env("DB_PASSWORD", "${resources.db.password}") .env("REDIS_HOST", "${resources.cache.hostname}") .env("LOG_LEVEL", "INFO"); let database = Resource::new("db") .type_("postgresql") .version("14") .property("database", "enterprise_db") .property("username", "app_user") .property("password", "secure_password") .property("highAvailability", true); let cache = Resource::new("cache") .type_("redis") .version("6.2") .property("clusterMode", true) .property("replicaCount", 2); let message_queue = Resource::new("mq") .type_("rabbitmq") .version("3.9") .property("clusterSize", 3); let config_volume = Volume::new("config-volume") .size("1Gi"); let logs_volume = Volume::new("logs-volume") .size("5Gi"); let workload = Workload::new() .replicas(3) .liveness_probe( Probe::http_get("/actuator/health/liveness", 8080) .initial_delay(90) .period(15) ) .readiness_probe( Probe::http_get("/actuator/health/readiness", 8080) .initial_delay(60) ) .horizontal_scaling( HorizontalScaling::new() .min_replicas(3) .max_replicas(10) .metric(ScalingMetric::Cpu, 70) ); Service::new() .container(java_container) .resource(database) .resource(cache) .resource(message_queue) .volume(config_volume) .volume(logs_volume) .workload(workload) } ``` These examples showcase how both approaches can describe the same infrastructure needs, with the Score Spec version being more declarative and YAML-based, while the Rust DSL provides type safety and a more programmatic approach. The Rust DSL would enable better IDE integration, compile-time validation, and extensibility, while Score Spec leverages existing CNCF standards and may be more approachable for developers already familiar with YAML-based configurations.
johnride added 1 commit 2025-03-10 19:19:01 +00:00
Implement LAMP server module with basic configuration.
Refactor and remove commented out Dhcpd struct and associated tests in opnsense/xml_utils.
Ensure codebase adheres to best practices and maintainability standards.
johnride added 1 commit 2025-03-10 21:04:59 +00:00
- Introduce `LAMPScore` struct with additional fields: `domain`, `config`, and `php_version`.
- Define default implementation for `LAMPConfig`.
- Update `Url` enum to use `Url(url::Url)` instead of `Remote(url::Url)`.
- Adjust references in `HttpServer` and `TftpServer` implementations.
- Modify `Interpret` trait implementation to use `name()` method from `LAMPScore`.
johnride added 1 commit 2025-03-17 19:48:17 +00:00
This ADR proposes to use k3s as a default runtime on linux and k3d on other platforms supporting docker
johnride added 1 commit 2025-03-17 20:20:57 +00:00
Add a new section to the ADR document outlining potential future work with WebAssembly (WASM) as an alternative runtime, comparing it to Java's bytecode and JVM model, highlighting potential benefits in observability, heap allocation, and garbage collection. Note current maturity limitations compared to our target customer base.
johnride added 1 commit 2025-03-18 02:36:46 +00:00
Select k3d as the primary solution for running Kubernetes clusters on Windows and macOS, alongside native k3s on Linux, to achieve a consistent "zero setup" experience across platforms while considering resource usage, complexity, and long-term alternatives like WebAssembly.
johnride added 1 commit 2025-03-25 12:03:49 +00:00
johnride added 1 commit 2025-03-26 20:39:51 +00:00
Refactor the orchestration process to use an `Interpret` trait instead of directly applying scores. This change introduces a more flexible and extensible design for executing commands associated with different types of topologies. The `CommandScore` and `K8sResourceScore` now implement this trait, providing a clear separation between score definition and execution logic. Update the `Maestro::orchestrate` method to compile scores into interpreters before executing them against their respective topologies.
johnride added 1 commit 2025-03-27 03:11:19 +00:00
Refactor various `Score` and `Interpret` implementations to utilize generic `Topology` traits, removing hardcoded dependencies on `HAClusterTopology`. This enhancement allows for more flexible and extensible code, accommodating different types of network topologies.
johnride reviewed 2025-03-27 21:01:07 +00:00
@@ -0,0 +188,4 @@
fn name(&self) -> &str {
&self.name
}
}
Author
Owner
impl Score for K8sResourceScore {
    type Topology: Topology + KubernetesCapability

    fn apply(&self, topology: &T) -> Result<(), String> {
        println!("Applying K8sResourceScore '{}' to topology '{}'", self.name, topology.name());
        topology.apply_manifest(&self.manifest)
    }

    fn name(&self) -> &str {
        &self.name
    }
}
```rust impl Score for K8sResourceScore { type Topology: Topology + KubernetesCapability fn apply(&self, topology: &T) -> Result<(), String> { println!("Applying K8sResourceScore '{}' to topology '{}'", self.name, topology.name()); topology.apply_manifest(&self.manifest) } fn name(&self) -> &str { &self.name } } ```
johnride added 1 commit 2025-03-31 19:03:14 +00:00
johnride added 1 commit 2025-03-31 19:07:48 +00:00
Remove the unnecessary `HAClusterTopology` import from multiple modules to clean up dependencies and reduce clutter. This change does not affect functionality as `HAClusterTopology` is no longer required in these files.
johnride added 1 commit 2025-04-02 19:52:03 +00:00
Introduce the `Topology` trait to ensure that `Maestro` can compile-time safely bind compatible `Scores` and `Topologies`. This refactoring includes updating `HarmonyTuiEvent`, `ScoreListWidget`, and related structures to work with generic `Topology` types, enhancing type safety and modularity.
wjro added 1 commit 2025-04-02 20:53:04 +00:00
johnride added 1 commit 2025-04-03 16:22:32 +00:00
This commit completes the refactoring of the `HAClusterTopology` struct to implement all required traits via passthrough to the underlying infrastructure providers.

- Implemented all traits (`DnsServer`, `LoadBalancer`, `HttpServer`, etc.) on `HAClusterTopology`.
- Each trait method now simply calls the corresponding method on the underlying infrastructure provider.
- This ensures that all functionality is delegated to the correct provider without duplicating logic.
- Updated trait implementations to accept `&self` instead of `&mut self` where appropriate.
- Fixed a compilation error in `remove_record` by changing the signature to accept `&self`.
- Added unimplemented!() stubs for HttpServer traits.
johnride added 2 commits 2025-04-03 17:42:05 +00:00
- Implemented `OKDLoadBalancerScore` and integrated it as a `FrontendScore`.
- Added `FrontendScore` trait for TUI displayable scores.
- Implemented `Display` for `OKDLoadBalancerScore`.
- Updated `ScoreListWidget` to handle `FrontendScore` types.
- Included load balancer score in the TUI.
johnride added 1 commit 2025-04-05 18:36:48 +00:00
This commit adds `serde` dependency and derives `Serialize` trait for `Score` types. This is necessary for serialization and deserialization of these types, which is required to display Scores to various user interfaces

- Added `serde` dependency to `harmony_types/Cargo.toml`.
- Added `serde::Serialize` derive macro to `MacAddress` in `harmony_types/src/lib.rs`.
- Added `serde::Serialize` derive macro to `Config` in `opnsense-config/src/config/config.rs`.
- Added `serde::Serialize` derive macro to `Score` in `harmony_types/src/lib.rs`.
- Added `serde::Serialize` derive macro to `Config` and `Score` in relevant modules.
- Added placeholder `todo!()` implementations for `serialize` methods. These will be implemented in future commits.
johnride added 1 commit 2025-04-05 18:39:46 +00:00
Decouples score definitions from UI implementations by mandating `serde::Serialize` and `serde::Deserialize` for all `Score` structs. UIs will interact with scores via their serialized representation, enabling scalability and reducing complexity for score authors.

This approach:

- Scales better with new score types and UI targets.
- Simplifies score authoring by removing the need for UI-specific display traits.
- Leverages the `serde` ecosystem for robust data handling.

Adding new field types requires updates to all UIs, a trade-off acknowledged in the ADR.
stremblay reviewed 2025-04-07 14:12:23 +00:00
@@ -0,0 +328,4 @@
1. **From Runtime to Compile-Time**: Move capability checking from runtime to compile-time.
2. **From Objects to Functions**: Think of scores less as objects and more as functions that transform topologies.
Owner

Un score, c'est pas "un objet qui contient son environnement, ...", c'est plutôt une entité standalone, qui se fait "pitcher" dans un contexte. Une fois dans ce contexte, si tous les requis du score sont disponibles, il est en mesure de s'exécuter dans ce contexte

Un score, c'est pas "un objet qui contient son environnement, ...", c'est plutôt une entité standalone, qui se fait "pitcher" dans un contexte. Une fois dans ce contexte, si tous les requis du score sont disponibles, il est en mesure de s'exécuter dans ce contexte
johnride added 1 commit 2025-04-09 20:10:50 +00:00
Adds a quick demo command using `cargo run -p example-tui` to launch a minimalist TUI with demo scores.

Also includes a core architecture diagram and overview in the README for better understanding of the project structure.
johnride added 1 commit 2025-04-11 15:01:52 +00:00
- Corrected XML test data to remove unnecessary `<descr>` tags, resolving failing tests.
- Removed the unused `ratatui_utils` module and its associated code.
- Simplified example in `harmony_tui/src/lib.rs` to use `tokio::main` and register scores directly with `Maestro`. This aligns with the project's evolving structure.
johnride closed this pull request 2025-04-11 15:05:39 +00:00

Pull request closed

Sign in to join this conversation.
No Reviewers
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: NationTech/harmony#6
No description provided.