Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: support application base path, add Docker build scripts #15

Merged
merged 40 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f248d01
Configure pipeline for ssi
Jan 11, 2024
6a016bb
prepare pipeline
Jan 11, 2024
32247b2
uppercase creds
Jan 11, 2024
2b1a91f
set namespace for rollout and service cmd
Jan 11, 2024
89231b4
correct ports
Jan 11, 2024
5a906bd
use value from secret
Jan 11, 2024
ac4743b
secret at correct place
Jan 11, 2024
eb30989
use value from secret
Jan 11, 2024
bead279
use similar protocol
Jan 11, 2024
c975f01
use https protocol
Jan 11, 2024
bed1718
use correct port
Jan 11, 2024
31d3198
different application host
Jan 11, 2024
6aa6d91
add servers test
Jan 11, 2024
e626067
more tracing test
Jan 11, 2024
cc2b6cd
add servers test
Jan 11, 2024
5b49a73
add relative path to server
Jan 11, 2024
fbf2e29
remove unused env var
Jan 11, 2024
5c4fd08
set correct branches
Jan 12, 2024
1161ae2
update readme
Jan 12, 2024
0aaae8c
undo changes cargo
Jan 12, 2024
0d8f760
improve docs
Jan 12, 2024
70154cb
set url instead of host
Jan 15, 2024
717542f
add base path + url
Jan 15, 2024
7a428d0
clean env variables to create consistent test behaviour
Jan 16, 2024
8dea60e
automatically build docker
Jan 16, 2024
73fd5e6
Merge branch 'dev' of https://github.com/impierce/ssi-agent into feat…
Jan 18, 2024
c16eb81
disable restart always
Jan 24, 2024
c5a63bb
only manual dispatch workflow
Jan 24, 2024
88e5de7
fix: remove `init_env_vars`
nanderstabel Jan 24, 2024
e9d7ae2
added tests for AddFunctions url
Jan 24, 2024
e3a0a4c
Merge branch 'feat/pipeline' of https://github.com/impierce/ssi-agent…
Jan 24, 2024
0caf9a6
remove return
Jan 24, 2024
d72f726
Add simple changelog
Jan 24, 2024
90ec0d1
Add simple changelog
Jan 24, 2024
3e86f1e
implement feedback
Jan 25, 2024
5ffe8c2
improve add functions url
Jan 25, 2024
b125271
add clippy feedback
Jan 25, 2024
1601b7d
implement feedback
Jan 26, 2024
a2d6c08
chore: extract cloud config values to env variables
daniel-mader Jan 26, 2024
d1c91a0
fix: error message when BASE_PATH is not set
daniel-mader Jan 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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