Skip to content

Commit

Permalink
feat: support application base path, add Docker build scripts (#15)
Browse files Browse the repository at this point in the history
* Configure pipeline for ssi

* prepare pipeline

* uppercase creds

* set namespace for rollout and service cmd

* correct ports

* use value from secret

* secret at correct place

* use value from secret

* use similar protocol

* use https protocol

* use correct port

* different application host

* add servers test

* more tracing test

* add servers test

* add relative path to server

* remove unused env var

* set correct branches

* update readme

* undo changes cargo

* improve docs

* set url instead of host

* add base path + url

* clean env variables to create consistent test behaviour

* automatically build docker

* disable restart always

* only manual dispatch workflow

* fix: remove `init_env_vars`

* added tests for AddFunctions url

* remove return

* Add simple changelog

* Add simple changelog

* implement feedback

* improve add functions url

* add clippy feedback

* implement feedback

* chore: extract cloud config values to env variables

* fix: error message when BASE_PATH is not set

---------

Co-authored-by: nanderstabel <nander.stabel@impierce.com>
Co-authored-by: Daniel Mader <daniel.mader@impierce.com>
  • Loading branch information
3 people authored Feb 6, 2024
1 parent dff1df7 commit 4f54df6
Show file tree
Hide file tree
Showing 22 changed files with 354 additions and 42 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
AGENT_CONFIG_LOG_FORMAT=json
AGENT_CONFIG_EVENT_STORE=postgres
AGENT_APPLICATION_HOST=my-domain.example.org
AGENT_APPLICATION_URL=https://my-domain.example.org
AGENT_ISSUANCE_CREDENTIAL_NAME="Demo Credential"
AGENT_ISSUANCE_CREDENTIAL_LOGO_URL=https://my-domain.example.org/credential_logo.png
AGENT_STORE_DB_CONNECTION_STRING=postgresql://demo_user:demo_pass@localhost:5432/demo
67 changes: 67 additions & 0 deletions .github/workflows/push.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.

name: Build and Deploy to GKE

on:
workflow_dispatch:

env:
IMAGE: unicore

jobs:
setup-build-publish-deploy:
name: Setup, Build, Publish, and Deploy
runs-on: ubuntu-latest
environment: dev
env:
PROJECT_ID: ${{ secrets.PROJECT_ID }}

permissions:
contents: "read"
id-token: "write"

steps:
- name: Checkout
uses: actions/checkout@v4

- name: "Auth"
uses: "google-github-actions/auth@v2"
with:
token_format: "access_token"
workload_identity_provider: projects/${{ secrets.PROJECT_NR }}/locations/global/workloadIdentityPools/workload-ip/providers/workload-ip-provider
service_account: k8s-user@${{ secrets.PROJECT_ID }}.iam.gserviceaccount.com

- name: "Set up Cloud SDK"
uses: "google-github-actions/setup-gcloud@v2"

- name: "Use gcloud CLI"
run: "gcloud info"

- name: Build
working-directory: ".pipeline"
run: chmod u+x ./build.sh && ./build.sh

# Get the GKE credentials so we can deploy to the cluster
- uses: google-github-actions/get-gke-credentials@v2
with:
cluster_name: ${{ vars.GKE_CLUSTER_NAME }}
project_id: ${{ secrets.PROJECT_ID }}
location: ${{ vars.GKE_COMPUTE_ZONE }}

- name: Create secret
run: |
kubectl -n ingress-apisix delete secret unicore-db-secret --ignore-not-found
kubectl -n ingress-apisix create secret generic unicore-db-secret \
--from-literal='connection-string=${{ secrets.AGENT_STORE_DB_CONNECTION_STRING }}'
## Deploy the Docker image to the GKE cluster
- name: Deploy
working-directory: ".pipeline"
run: chmod u+x ./deploy.sh && ./deploy.sh
1 change: 1 addition & 0 deletions .pipeline/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/
14 changes: 14 additions & 0 deletions .pipeline/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Pipeline

In order to run the pipeline build script locally, create a `.env` file in `.github/.pipeline` and add the following content:

```sh
IMAGE=unicore
ARTIFACT_REGISTRY_HOST=<ask-the-repository-owner>
ARTIFACT_REGISTRY_REPOSITORY=<ask-the-repository-owner>
PROJECT_ID=<ask-the-repository-owner>
GITHUB_SHA=test_sha
APISIX_PATH=unicore
```

Then execute `./build.sh`.
47 changes: 47 additions & 0 deletions .pipeline/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/bin/bash

set -e

[ -z "$IMAGE" ] && echo "Need to set IMAGE" && exit 1;
[ -z "$ARTIFACT_REGISTRY_HOST" ] && echo "Need to set ARTIFACT_REGISTRY_HOST" && exit 1;
[ -z "$ARTIFACT_REGISTRY_REPOSITORY" ] && echo "Need to set ARTIFACT_REGISTRY_REPOSITORY" && exit 1;
[ -z "$PROJECT_ID" ] && echo "Need to set PROJECT_ID" && exit 1;
[ -z "$GITHUB_SHA" ] && echo "Need to set GITHUB_SHA" && exit 1;

export CONTAINER_REPO="$ARTIFACT_REGISTRY_HOST/$PROJECT_ID/$ARTIFACT_REGISTRY_REPOSITORY"

echo $CONTAINER_REPO

# Configure Docker to use the gcloud command-line tool as a credential
# helper for authentication
gcloud auth configure-docker $ARTIFACT_REGISTRY_HOST

[ -e build/ ] && rm -rf build

echo "-------------------------------------------------------------"
echo "Create build directory"
echo "-------------------------------------------------------------"

mkdir build && cp *.yaml build && cd build

echo "-------------------------------------------------------------"
echo "Replace environment variables in files"
echo "-------------------------------------------------------------"

sed -i -e 's|@IMAGE@|'"$IMAGE"'|g' *.yaml
sed -i -e 's|@CONTAINER_REPO@|'"$CONTAINER_REPO/$IMAGE:$GITHUB_SHA"'|g' *.yaml

echo "-------------------------------------------------------------"
echo "Display yaml files"
echo "-------------------------------------------------------------"

for f in *.yaml; do printf "\n---\n"; cat "${f}"; done

cd ../../agent_application

echo "-------------------------------------------------------------"
echo "Build and push docker container"
echo "-------------------------------------------------------------"

docker build -t "$CONTAINER_REPO/$IMAGE:$GITHUB_SHA" -f docker/Dockerfile ..
docker push "$CONTAINER_REPO/$IMAGE:$GITHUB_SHA"
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### 24-01-2024

Environment variable `AGENT_APPLICATION_HOST` has changed to `AGENT_APPLICATION_URL` and requires the complete URL. e.g.:
`https://my.domain.com/unicore`. In case you don't have rewrite root enabled on your reverse proxy, you will have to set `AGENT_CONFIG_BASE_PATH` as well. e.g.: `unicore`.
1 change: 1 addition & 0 deletions Cargo.lock

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

21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

Build and run the **SSI Agent** in a local Docker environment following [these steps](./agent_application/docker/README.md).

## Breaking changes

From time to time breaking changes can occur. Please make sure you read the [CHANGELOG](./CHANGELOG.md) before updating.

## Architecture

![alt text](UniCore.drawio.png "UniCore")
Expand All @@ -16,35 +20,43 @@ UniCore makes use of several practical architectural principles—specifically,
Sourcing. Together, these principles contribute to a robust and scalable software solution.

### Hexagonal Architecture

Hexagonal Architecture promotes modularity by separating the core business logic from external dependencies. UniCore's
core functionality remains untangled from external frameworks, making it adaptable to changes without affecting the
overall system.

#### Core

The core business logic of UniCore currently consists of the [**Core Issuance Agent**](./agent_issuance/README.md). This
component is responsible for handling the issuance of credentials and offers. It defines the rules by which incoming
**Commands** can change the state by emitting **Events**. The Core Issuance Agent has two major functions:

- **Preparations**: Preparing the data that will be used in the issuance of credentials and credential offers.
- **Credential Issuance**: Issuing credentials according to the OpenID for Verifiable Credential Issuance specification.

#### Adapters

UniCore's adapters are responsible for handling the communication between the core and external systems. Adapters can
either be **Inbound** or **Outbound**. Inbound adapters are responsible for receiving incoming requests and translating
them into commands that can be understood by the core. Outbound adapters are responsible for translating the core's
**Events** into outgoing requests. In our current implementation, we have the following adapters:

- [**REST API**](./agent_api_rest/) (Inbound): The REST API is responsible for receiving incoming HTTP requests from clients and translating them
into commands that can be understood by the core.
- [**Event Store**](./agent_store/) (Outbound): The Event Store is responsible for storing the events emitted by the
core. By default, the Event Store is implemented using PostgreSQL. Alternatively, it can be implemented using an
in-memory database for testing purposes.

#### Application

The [**Application**](./agent_application/) is responsible for orchestrating the core and adapters. It is responsible for initializing the core and
adapters and connecting them together.

### CQRS

CQRS is a design pattern that separates the responsibility for handling commands (changing state) from handling queries
(retrieving state).

- **Commands**: Commands are actions that are responsible for executing business logic
and updating the application state.
- **Queries**: Queries are responsible for reading data without modifying the state.
Expand All @@ -53,10 +65,11 @@ The separation of commands and queries simplifies the design and maintenance of
optimization of each side independently.

### Event Sourcing
Event Sourcing is a pattern in which the application's state is determined by a sequence of events. Each event signifies a state change and is preserved in an event store. These **Events** serve as immutable facts about alterations in the application's state. The **Event Store**, functioning as a database, records events in the order of their occurrence. Consequently, it enables the reconstruction of the application's state at any given moment. This pattern not only ensures a dependable audit log for monitoring changes but also facilitates querying the system's state at various intervals.

Event Sourcing is a pattern in which the application's state is determined by a sequence of events. Each event signifies a state change and is preserved in an event store. These **Events** serve as immutable facts about alterations in the application's state. The **Event Store**, functioning as a database, records events in the order of their occurrence. Consequently, it enables the reconstruction of the application's state at any given moment. This pattern not only ensures a dependable audit log for monitoring changes but also facilitates querying the system's state at various intervals.

## Interaction Sequence

This sequence diagram illustrates the dynamic interaction flow within UniCore, focusing on the preparation and issuance of credentials and offers. The diagram also illustrates the OpenID4VCI Pre-Authorized Code Flow, which is used by wallets to obtain access tokens and credentials.

```mermaid
Expand All @@ -72,7 +85,7 @@ sequenceDiagram
autonumber
Note over api_rest, store: Command and Query<br/>Responsibility Segregation (CQRS)
Note over api_rest, store: Command and Query<br/>Responsibility Segregation (CQRS)
Note over client, store: Agent Preparations
Expand All @@ -95,7 +108,7 @@ sequenceDiagram
wallet->>api_rest: GET /.well-known/oauth-authorization-server
api_rest->>store: Query
store->>api_rest: View
api_rest->>wallet: 200 OK application/json
api_rest->>wallet: 200 OK application/json
wallet->>api_rest: GET /.well-known/openid-credential-issuer
api_rest->>store: Query
Expand Down Expand Up @@ -143,4 +156,4 @@ OpenID4VCI Pre-Authorized Code Flow
28-29: See steps 2-3.
30-31: See steps 4-5.
32: The API returns a `200 OK` response with the credential(s) in the response body.
```
```
2 changes: 1 addition & 1 deletion agent_api_rest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ edition = "2021"

[dependencies]
agent_issuance = { path = "../agent_issuance" }
agent_shared = {path = "../agent_shared"}

oid4vci = { git = "https://git@github.com/impierce/openid4vc.git", branch = "feat/refactor-request" }
oid4vc-core = { git = "https://git@github.com/impierce/openid4vc.git", branch = "feat/refactor-request" }

axum = "0.6"
axum-auth = "0.4"
axum-macros = "0.3"
Expand Down
2 changes: 1 addition & 1 deletion agent_api_rest/src/credential_issuer/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ mod tests {
.oneshot(
Request::builder()
.method(http::Method::POST)
.uri(format!("/openid4vci/credential"))
.uri("/openid4vci/credential")
.header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
.header(http::header::AUTHORIZATION, format!("Bearer {}", access_token))
.body(Body::from(
Expand Down
2 changes: 1 addition & 1 deletion agent_api_rest/src/credential_issuer/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ mod tests {
.oneshot(
Request::builder()
.method(http::Method::POST)
.uri(format!("/auth/token"))
.uri("/auth/token")
.header(
http::header::CONTENT_TYPE,
mime::APPLICATION_WWW_FORM_URLENCODED.as_ref(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ mod tests {
startup_commands::{create_credentials_supported, load_credential_issuer_metadata},
state::{initialize, CQRS},
};
use agent_shared::config;
use agent_shared::{config, UrlAppendHelpers};
use agent_store::in_memory;
use axum::{
body::Body,
Expand Down Expand Up @@ -85,12 +85,13 @@ mod tests {

let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
let credential_issuer_metadata: CredentialIssuerMetadata = serde_json::from_slice(&body).unwrap();

assert_eq!(
credential_issuer_metadata,
CredentialIssuerMetadata {
credential_issuer: BASE_URL.clone(),
authorization_server: None,
credential_endpoint: BASE_URL.join("openid4vci/credential").unwrap(),
credential_endpoint: BASE_URL.append_path_segment("openid4vci/credential"),
batch_credential_endpoint: None,
deferred_credential_endpoint: None,
credentials_supported: vec![CredentialsSupportedObject {
Expand Down
64 changes: 57 additions & 7 deletions agent_api_rest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod credentials;
mod offers;

use agent_issuance::{model::aggregate::IssuanceData, queries::IssuanceDataView, state::ApplicationState};
use agent_shared::{config, ConfigError};
use axum::{
routing::{get, post},
Router,
Expand All @@ -21,31 +22,80 @@ use offers::offers;
pub const AGGREGATE_ID: &str = "agg-id-F39A0C";

pub fn app(state: ApplicationState<IssuanceData, IssuanceDataView>) -> Router {
let base_path = get_base_path();

let path = |suffix: &str| -> String {
if let Ok(base_path) = &base_path {
format!("/{}{}", base_path, suffix)
} else {
suffix.to_string()
}
};

Router::new()
.route("/v1/credentials", post(credentials))
.route("/v1/offers", post(offers))
.route(&path("/v1/credentials"), post(credentials))
.route(&path("/v1/offers"), post(offers))
.route(
"/.well-known/oauth-authorization-server",
&path("/.well-known/oauth-authorization-server"),
get(oauth_authorization_server),
)
.route("/.well-known/openid-credential-issuer", get(openid_credential_issuer))
.route("/auth/token", post(token))
.route("/openid4vci/credential", post(credential))
.route(
&path("/.well-known/openid-credential-issuer"),
get(openid_credential_issuer),
)
.route(&path("/auth/token"), post(token))
.route(&path("/openid4vci/credential"), post(credential))
.with_state(state)
}

fn get_base_path() -> Result<String, ConfigError> {
config!("base_path").map(|mut base_path| {
if base_path.starts_with('/') {
base_path.remove(0);
}

if base_path.ends_with('/') {
base_path.pop();
}

if base_path.is_empty() {
panic!("AGENT_APPLICATION_BASE_PATH can't be empty, remove or set path");
}

tracing::info!("Base path: {:?}", base_path);

base_path
})
}

#[cfg(test)]
mod tests {
use super::*;
use agent_issuance::command::IssuanceCommand;
use agent_issuance::state::CQRS;
use agent_issuance::{command::IssuanceCommand, services::IssuanceServices};
use agent_store::in_memory;
use serde_json::json;

pub const PRE_AUTHORIZED_CODE: &str = "pre-authorized_code";
pub const SUBJECT_ID: &str = "00000000-0000-0000-0000-000000000000";

lazy_static::lazy_static! {
pub static ref BASE_URL: url::Url = url::Url::parse("https://example.com").unwrap();
}

async fn handler() {}

#[tokio::test]
#[should_panic]
async fn test_base_path_routes() {
let state = in_memory::ApplicationState::new(vec![], IssuanceServices {}).await;

std::env::set_var("AGENT_APPLICATION_BASE_PATH", "unicore");
let router = app(state);

let _ = router.route("/auth/token", post(handler));
}

pub async fn create_unsigned_credential(state: ApplicationState<IssuanceData, IssuanceDataView>) -> String {
state
.execute_with_metadata(
Expand Down
Loading

0 comments on commit 4f54df6

Please sign in to comment.