Skip to content

Commit 58ad389

Browse files
committed
Add federation example
1 parent 7b56a38 commit 58ad389

File tree

7 files changed

+100
-32
lines changed

7 files changed

+100
-32
lines changed

README.md

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ Make sure you have docker installed and run:
3636
docker compose up
3737
```
3838

39-
This will start the `orchestrator`, `orchestrator-ui`, `netbox`, `postgres` and `redis`.
39+
This will start the `orchestrator`, `orchestrator-ui`, `netbox`, `federation`, `postgres` and `redis`.
4040

4141
To include LSO, run the following command instead:
4242

4343
```
44-
COMPOSE_PROFILES=lso docker compose up
44+
COMPOSE_PROFILES=lso docker compose up
4545
```
4646

4747
This will build the Docker image for LSO locally, and make the orchestrator use the included Ansible playbooks.
@@ -302,7 +302,7 @@ to define the helper functions as locally as possible.
302302
The `main.py` can be as simple as shown below, and can be deployed by a
303303
ASGI server like Uvicorn[^5].
304304

305-
```python
305+
```python
306306
from orchestrator import OrchestratorCore
307307
from orchestrator.cli.main import app as core_cli
308308
from orchestrator.settings import AppSettings
@@ -439,8 +439,8 @@ is not supported.
439439

440440
The Orchestrator uses the concept of a Product to describe what can be built to the end user. When
441441
a user runs a workflow to create a Product, this results in a unique instance of that product called a Subscription.
442-
A Subscription is always tied to a certain lifecycle state (eg. Initial, Provisioning, Active, Terminated, etc) and
443-
is unique per customer. In other words a Subscription contains all the information needed to uniquely identify a certain
442+
A Subscription is always tied to a certain lifecycle state (eg. Initial, Provisioning, Active, Terminated, etc) and
443+
is unique per customer. In other words a Subscription contains all the information needed to uniquely identify a certain
444444
resource owned by a user/customer that conforms to a certain definition, namely a Product.
445445

446446
### Product description in Python
@@ -451,12 +451,12 @@ additional functionality to dynamically cast variables from the
451451
database, where they are stored as a string, to their correct type in
452452
Python at runtime. Pydantic uses Python type hints to validate that the
453453
correct type is assigned. The use of typing, when used together with
454-
type checkers, already helps to make the code more robust, furthermore the use of Pydantic makes it possible to check
454+
type checkers, already helps to make the code more robust, furthermore the use of Pydantic makes it possible to check
455455
variables at runtime which greatly improves reliability.
456456

457457
#### Example of "Runtime typecasting/safety"
458-
In the example below we attempt to access a resource that has been stored in an instance of a product
459-
(subscription instance). It shows how it can be done directly through the ORM and it shows the added value of Domain
458+
In the example below we attempt to access a resource that has been stored in an instance of a product
459+
(subscription instance). It shows how it can be done directly through the ORM and it shows the added value of Domain
460460
Models on top of the ORM.
461461

462462
**Serialisation direct from the database**
@@ -502,7 +502,7 @@ lifting, and makes sure the database remains generic and it's schema remains sta
502502
#### Product Structure
503503
A Product definition has two parts in its structure. The Higher order product type that contains information describing
504504
the product in a more general sense, and multiple layers of product blocks that logically describe the set of resources
505-
that make up the product definition. The product type describes the fixed inputs and the top-level product blocks.
505+
that make up the product definition. The product type describes the fixed inputs and the top-level product blocks.
506506
The fixed inputs are used to differentiate between variants of the same product, for example the speed of a network
507507
port. There is always at least one top level product block that contains
508508
the resource types to administer the customer facing input. Beside
@@ -577,10 +577,10 @@ this product.
577577

578578
#### Wiring it up in the Orchestrator
579579
<details>
580-
<summary>This section contains advanced information about how to configure the Orchestrator. It is also possible to use
580+
<summary>This section contains advanced information about how to configure the Orchestrator. It is also possible to use
581581
a more user friendly tool available <a href="https://workfloworchestrator.
582582
org/orchestrator-core/reference-docs/cli/#generate">here</a>.
583-
This tool uses a configuration file to generate the boilerplate, migrations and configuration necessary to make use of
583+
This tool uses a configuration file to generate the boilerplate, migrations and configuration necessary to make use of
584584
the product straight away.
585585
</summary>
586586

@@ -644,7 +644,7 @@ Every time a subscription is transitioned from one lifecycle to another,
644644
an automatic check is performed to ensure that resource types that are
645645
not optional are in fact present on that instantiation of the product
646646
block. This safeguards for incomplete administration for that lifecycle
647-
state.
647+
state.
648648

649649
#### Resource Type lifecycle. When to use `None`
650650
The resource types on an inactive product block are usually all
@@ -733,11 +733,11 @@ while displaying detailed subscription information.
733733

734734
## Workflows - Basics
735735

736-
Workflows are used to orchestrate the lifecycle of a Product Subscription and process the user or systems intent and
737-
apply that to the service. As mentioned above a Subscription is created, then modified `N` number of times, after
738-
which it is terminated. During it's life a Subscription may also be validated on a regular basis to check whether
739-
there is any drift between the state captured in the Orchestrator and actual state on the system. This workflow is
740-
slightly different compared to the workflows that process intent and apply that to a system, as it does not modify
736+
Workflows are used to orchestrate the lifecycle of a Product Subscription and process the user or systems intent and
737+
apply that to the service. As mentioned above a Subscription is created, then modified `N` number of times, after
738+
which it is terminated. During it's life a Subscription may also be validated on a regular basis to check whether
739+
there is any drift between the state captured in the Orchestrator and actual state on the system. This workflow is
740+
slightly different compared to the workflows that process intent and apply that to a system, as it does not modify
741741
the system.
742742

743743
Four types of workflows are defined, three lifecycle related ones to
@@ -764,9 +764,9 @@ types is done automatically. That is why it is important to correctly
764764
type the step function parameters.
765765

766766
#### Example
767-
Given this function, when a user correctly makes use of the step decorator it is very easy to extract variables and
768-
make a calculation. It creates readable code, that is easy to understand and reason about. Furthermore the variables
769-
become available in the step in their correct type according to the domain model. Logic errors due wrong type
767+
Given this function, when a user correctly makes use of the step decorator it is very easy to extract variables and
768+
make a calculation. It creates readable code, that is easy to understand and reason about. Furthermore the variables
769+
become available in the step in their correct type according to the domain model. Logic errors due wrong type
770770
interpretation are much less prone to happen.
771771

772772
**Bad use of the step decorator**
@@ -776,7 +776,7 @@ def my_ugly_step(state: State) -> State:
776776
variable_1 = int(state["variable_1"])
777777
variable_2 = str(state["varialble_2"])
778778
subscription = SubscriptionModel.from_subscription_id(state["subscription_id"])
779-
779+
780780
if variable_1 > 42:
781781
subscription.product_block_model.variable_1 = -1
782782
subscription.product_block_model.variable_2 = "Infinity"
@@ -787,7 +787,7 @@ def my_ugly_step(state: State) -> State:
787787
state["subscription"] = subscription
788788
return state
789789
```
790-
In the above example you see we do a simple calculation based on `variable_1`. When computing with even more
790+
In the above example you see we do a simple calculation based on `variable_1`. When computing with even more
791791
variables, you van imagine how unreadable the function will be. Now consider the next example.
792792

793793
**Good use of the step decorator**
@@ -800,18 +800,18 @@ def my_beautiful_step(variable_1: int, variable_2: str, subscription: Subscripti
800800
else:
801801
subscription.product_block_model.variable_1 = variable_1
802802
subscription.product_block_model.variable_2 = variable_2
803-
803+
804804
return state | {"subscriotion": subscription}
805805
```
806806

807-
As you can see the Orchestrator the orchestrator helps you a lot to condense the logic in your function. The `@step`
807+
As you can see the Orchestrator the orchestrator helps you a lot to condense the logic in your function. The `@step`
808808
decorator does the following:
809809

810810
* Loads the previous steps state from the database.
811811
* Inspects the step functions signature
812812
* Finds the arguments in the state and injects them as function arguments to the step function
813813
* It casts them to the correct type by using the type hints of the step function.
814-
* Finally it updates the state of the workflow and persists all model changes to the database upon reaching the
814+
* Finally it updates the state of the workflow and persists all model changes to the database upon reaching the
815815
`return` of the step function.
816816

817817
### Forms
@@ -852,10 +852,10 @@ subscription with minimal or no impact to the customer.
852852
</details>
853853

854854
#### Form _Magic_
855-
As mentioned before, forms are dynamically created from the backend. This means, **little to no** frontend coding is
856-
needed to make complex wizard like input forms available to the user. When selecting an action in the UI. The first
857-
thing the frontend does is make an api call to load a form from the backend. The resulting `JSONschema` is parsed
858-
and the correct widgets are loaded in the frontend. Upon submit this is posted to the backend that does all
855+
As mentioned before, forms are dynamically created from the backend. This means, **little to no** frontend coding is
856+
needed to make complex wizard like input forms available to the user. When selecting an action in the UI. The first
857+
thing the frontend does is make an api call to load a form from the backend. The resulting `JSONschema` is parsed
858+
and the correct widgets are loaded in the frontend. Upon submit this is posted to the backend that does all
859859
validation and signals to the user if there are any errors. The following forms are supported:
860860

861861
* Multiselect
@@ -865,8 +865,8 @@ validation and signals to the user if there are any errors. The following forms
865865
* Radio
866866

867867
## Workflow examples
868-
What follows are a few examples of how workflows implement the best common practices implemented by SURF. It
869-
explains in detail what a typical workflow could look like for provision in network element. These examples can be
868+
What follows are a few examples of how workflows implement the best common practices implemented by SURF. It
869+
explains in detail what a typical workflow could look like for provision in network element. These examples can be
870870
examined in greater detail by exploring the `.workflows.node` directory.
871871

872872
### Create workflow

docker-compose.yml

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,36 @@ services:
5959
volumes:
6060
- netbox-redis-cache-data:/data
6161

62+
rover-compose:
63+
image: alpine
64+
build:
65+
context: docker/federation
66+
dockerfile: rover.Dockerfile
67+
depends_on:
68+
orchestrator:
69+
condition: service_started
70+
netbox:
71+
condition: service_healthy
72+
environment:
73+
- APOLLO_ELV2_LICENSE=accept
74+
- APOLLO_TELEMETRY_DISABLED=true
75+
command: supergraph compose --config /app/supergraph-config.yaml --output /app/supergraph.graphql --skip-update-check
76+
volumes:
77+
- ./docker/federation:/app
78+
79+
federation:
80+
image: ghcr.io/apollographql/router:v1.47.0
81+
ports:
82+
- "4000:4000"
83+
depends_on:
84+
rover-compose:
85+
condition: service_completed_successfully
86+
environment:
87+
- APOLLO_TELEMETRY_DISABLED=true
88+
command: --config /app/router-config.yaml --supergraph /app/supergraph.graphql
89+
volumes:
90+
- ./docker/federation:/app
91+
6292
#
6393
# Netbox
6494
#
@@ -116,7 +146,7 @@ services:
116146

117147
orchestrator:
118148
&orchestrator
119-
image: "ghcr.io/workfloworchestrator/orchestrator-core:latest"
149+
image: "ghcr.io/workfloworchestrator/orchestrator-core:edge" # TODO latest points to 2.2.0 - need 2.3.0rcX with graphql schema fixes
120150
env_file: ./docker/orchestrator/orchestrator.env
121151
environment:
122152
LSO_ENABLED: ${COMPOSE_PROFILES:+True}

docker/federation/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.graphql

docker/federation/router-config.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
cors:
2+
allow_any_origin: true
3+
4+
supergraph:
5+
introspection: true
6+
# The socket address and port to listen on
7+
listen: 0.0.0.0:4000
8+
9+
homepage:
10+
enabled: false
11+
12+
sandbox:
13+
enabled: true

docker/federation/rover.Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM ubuntu
2+
3+
RUN apt update && apt install curl -y
4+
5+
RUN useradd --create-home --shell /bin/bash rover-user
6+
7+
USER rover-user
8+
9+
WORKDIR /app
10+
11+
RUN curl -sSL https://rover.apollo.dev/nix/v0.23.0 | sh
12+
13+
ENTRYPOINT ["/home/rover-user/.rover/bin/rover"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
federation_version: =2.7.1
2+
subgraphs:
3+
orchestrator:
4+
routing_url: http://orchestrator:8080/api/graphql
5+
schema:
6+
subgraph_url: http://orchestrator:8080/api/graphql
7+
netbox:
8+
routing_url: http://netbox:8080/graphql/
9+
schema:
10+
subgraph_url: http://netbox:8080/graphql/

docker/orchestrator/orchestrator.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ NETBOX_URL=http://netbox:8080/
88
OAUTH2_ACTIVE=False
99
LSO_PLAYBOOK_URL=http://example-orchestrator-lso-1:8000/api/playbook
1010
ORCHESTRATOR_URL=http://example-orchestrator-orchestrator-1:8080
11+
FEDERATION_ENABLED=True

0 commit comments

Comments
 (0)