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

Fix file logging issue in production container #180

Merged
merged 9 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
mongodb/data/*
!mongodb/data/.keep
inventory_management_system_api/logging.ini
/keys/
/keys/jwt-key.pub

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
10 changes: 0 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,5 @@ RUN --mount=type=cache,target=/root/.cache \
\
python3 -m pip install .;

ENV API__TITLE="Inventory Management System API"
ENV API__DESCRIPTION="This is the API for the Inventory Management System"
ENV API__ROOT_PATH=""
ENV DATABASE__PROTOCOL=mongodb
ENV DATABASE__USERNAME=root
ENV DATABASE__PASSWORD=example
ENV DATABASE__HOSTNAME=localhost
ENV DATABASE__PORT=27017
ENV DATABASE__NAME=ims

CMD ["uvicorn", "inventory_management_system_api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
EXPOSE 8000
17 changes: 3 additions & 14 deletions Dockerfile.prod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ WORKDIR /inventory-management-system-api-run
COPY README.md pyproject.toml ./
# Copy inventory_management_system_api source files
COPY inventory_management_system_api/ inventory_management_system_api/
COPY logs/ logs/

RUN set -eux; \
\
Expand All @@ -18,22 +19,10 @@ RUN set -eux; \
addgroup -S inventory-management-system-api; \
adduser -S -D -G inventory-management-system-api -H -h /inventory-management-system-api-run inventory-management-system-api; \
\
# Create a log file \
touch inventory-management-system-api.log; \
# Change ownership of log file - app will need to write to it
chown -R inventory-management-system-api:inventory-management-system-api inventory-management-system-api.log;
# Change ownership of logs/ - app will need to write log files to it \
chown -R inventory-management-system-api:inventory-management-system-api logs/;

USER inventory-management-system-api

ENV API__TITLE="Inventory Management System API"
ENV API__DESCRIPTION="This is the API for the Inventory Management System"
ENV API__ROOT_PATH=""
ENV DATABASE__PROTOCOL=mongodb
ENV DATABASE__USERNAME=root
ENV DATABASE__PASSWORD=example
ENV DATABASE__HOSTNAME=localhost
ENV DATABASE__PORT=27017
ENV DATABASE__NAME=ims

CMD ["uvicorn", "inventory_management_system_api.main:app", "--host", "0.0.0.0", "--port", "8000"]
EXPOSE 8000
167 changes: 123 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,39 @@
# Inventory Management System

## How to Run

This is a Python microservice created using FastAPI and requires a MongoDB instance to run against.

### Prerequisites

- Docker installed (if you want to run the microservice inside Docker)
- Python 3.10 (or above) and MongoDB 6.0 installed on your machine if you are not using Docker
- [MongoDB Compass](https://www.mongodb.com/products/compass) installed (if you want to interact with the database using a GUI)
- Docker and Docker Compose installed (if you want to run the microservice inside Docker)
- Python 3.10 (or above) and MongoDB 6.0 installed on your machine (if you are not using Docker)
- Public key (must be OpenSSH encoded) to decode JWT access tokens (if JWT authentication/authorization is enabled)
- [MongoDB Compass](https://www.mongodb.com/products/compass) installed (if you want to interact with the database using
a GUI)
- This repository cloned

### Docker Setup

The easiest way to run the application with Docker for local development is using the `docker-compose.yml` file. It is
configured to spin up a MongoDB instance that can be accessed at `localhost:27017` using `root` as the username and
`example` as the password. It also starts the application in a reload mode using the `Dockerfile`. Use the `Dockerfile`
or `Dockerfile.prod` to run just the application itself in a container. The former is for local development and must
not be used in production.

Ensure that Docker is installed and running on your machine before proceeding.

1. Create a `.env` file alongside the `.env.example` file. Use the example file as a reference and modify the values accordingly.

1. Create a `.env` file alongside the `.env.example` file. Use the example file as a reference and modify the values
accordingly.
```bash
cp .env.example .env
cp inventory_management_system_api/.env.example inventory_management_system_api/.env
```

2. Create a `logging.ini` file alongside the `logging.example.ini` file. Use the example file as a reference and modify it accordingly:

2. Create a `logging.ini` file alongside the `logging.example.ini` file. Use the example file as a reference and modify
it accordingly:
```bash
cp logging.example.ini logging.ini
cp inventory_management_system_api/logging.example.ini inventory_management_system_api/logging.ini
```

3. (**Required only if JWT Auth is enabled**) Create a `keys` directory in the root of the project directory and inside it create a copy of the public key generated by the authentication component. This is needed for decoding of JWT access tokens signed by the corresponding private key.
3. (**Required only if JWT Auth is enabled**) Create a `keys` directory in the root of the project directory and inside
it create a copy of the public key generated by the authentication component. This is needed for decoding of JWT
access tokens signed by the corresponding private key.

#### Using Docker Compose File
#### Using `docker-compose.yml`
joelvdavies marked this conversation as resolved.
Show resolved Hide resolved
The easiest way to run the application with Docker for local development is using the `docker-compose.yml` file. It is
configured to spin up a MongoDB instance that can be accessed at `localhost:27017` using `root` as the username and
`example` as the password. It also starts the application in a reload mode using the `Dockerfile`.

1. Build and start the Docker containers:
```bash
Expand All @@ -44,73 +42,154 @@ Ensure that Docker is installed and running on your machine before proceeding.
The microservice should now be running inside Docker at http://localhost:8000 and its Swagger UI could be accessed
at http://localhost:8000/docs. A MongoDB instance should also be running at http://localhost:27017.

#### Using Dockerfiles
#### Using `Dockerfile`
Use the `Dockerfile` to run just the application itself in a container. Use this only for local development (not
production)!

1. Build an image using either the `Dockerfile` or `Dockerfile.prod` from the root of the project directory:
1. Build an image using the `Dockerfile` from the root of the project directory:
```bash
docker build -f Dockerfile -t inventory_management_system_api_image .
```

2. Start the container using the image built and map it to port `8000` locally (please note that the public key volume
is only needed if JWT Auth is enabled):
```bash
docker run -p 8000:8000 --name inventory_management_system_api_container -v ./keys/jwt-key.pub:/inventory-management-system-api-run/keys/jwt-key.pub -v ./logs:/inventory-management-system-api-run/logs inventory_management_system_api_image
```
or with values for the environment variables:
```bash
docker run -p 8000:8000 --name inventory_management_system_api_container --env DATABASE__NAME=test-ims -v ./keys/jwt-key.pub:/inventory-management-system-api-run/keys/jwt-key.pub -v ./logs:/inventory-management-system-api-run/logs inventory_management_system_api_image
```
The microservice should now be running inside Docker at http://localhost:8000 and its Swagger UI could be accessed
at http://localhost:8000/docs.

#### Using `Dockerfile.prod`
Use the `Dockerfile.prod` to run just the application itself in a container. This can be used for production.

1. While in root of the project directory, change the permissions of the `logs` directory so that it is writable by
other users. This allows the container to save the application logs to it.
```bash
sudo chmod -R 0777 logs/
```

2. Build an image using the `Dockerfile.prod` from the root of the project directory:
```bash
docker build -f Dockerfile.prod -t ims_api_image .
docker build -f Dockerfile.prod -t inventory_management_system_api_image .
```

2. Start the container using the image built and map it to port `8000` locally:
3. Start the container using the image built and map it to port `8000` locally:
```bash
docker run -p 8000:8000 --name ims_api_container ims_api_image
docker run -p 8000:8000 --name inventory_management_system_api_container -v ./keys/jwt-key.pub:/inventory-management-system-api-run/keys/jwt-key.pub -v ./logs:/inventory-management-system-api-run/logs inventory_management_system_api_image
joelvdavies marked this conversation as resolved.
Show resolved Hide resolved
```
or with values for the environment variables:
```bash
docker run -p 8000:8000 --name ims_api_container --env DATABASE__NAME=test-ims ims_api_image
docker run -p 8000:8000 --name inventory_management_system_api_container --env DATABASE__NAME=test-ims -v ./keys/jwt-key.pub:/inventory-management-system-api-run/keys/jwt-key.pub -v ./logs:/inventory-management-system-api-run/logs inventory_management_system_api_image
```
The microservice should now be running inside Docker at http://localhost:8000 and its Swagger UI could be accessed
at http://localhost:8000/docs.

### Local Setup

Ensure that MongoDB is installed and running on your machine before proceeding.
Ensure that Python is installed on your machine before proceeding.

1. Create a Python virtual environment and activate it in the root of the project directory:
```bash
python -m venv venv
source venv/bin/activate
```

2. Install the required dependencies using pip:
```bash
pip install .[dev]
```
3. Create a `.env` file using the `.env.example` file and modify the values inside accordingly.
4. Create a `logging.ini` file using the `logging.example.ini` file and modify it accordingly.
5. Start the microservice using Uvicorn:

3. Create a `.env` file alongside the `.env.example` file. Use the example file as a reference and modify the values
accordingly.
```bash
cp inventory_management_system_api/.env.example inventory_management_system_api/.env
```

4. Create a `logging.ini` file alongside the `logging.example.ini` file. Use the example file as a reference and modify
it accordingly:
```bash
cp inventory_management_system_api/logging.example.ini inventory_management_system_api/logging.ini
```

5. (**Required only if JWT Auth is enabled**) Create a `keys` directory in the root of the project directory and inside
it create a copy of the public key generated by the authentication component. This is needed for decoding of JWT
access tokens signed by the corresponding private key.

6. Start the microservice using Uvicorn:
```bash
uvicorn inventory_management_system_api.main:app --log-config inventory_management_system_api/logging.ini --reload
```
The microservice should now be running locally at http://localhost:8000. The Swagger UI can be accessed
at http://localhost:8000/docs.
6. To run the unit tests, run :

7. To run the unit tests, run :
```bash
pytest -c test/pytest.ini test/unit/
```
7. To run the e2e tests, run:

8. To run the e2e tests, run:
```bash
pytest -c test/pytest.ini test/e2e/
```

## API Configuration
9. To run all the tests, run:
```bash
pytest -c test/pytest.ini test/
```

## Notes

### Application Configuration
The configuration for the application is handled
using [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). It allows for loading config
values from environment variables or the `.env` file. Please note that even when using the `.env` file, Pydantic will
still read environment variables as well as the `.env` file, environment variables will always take priority over
values loaded from the `.env` file.

Listed below are the environment variables supported by the application.

| Environment Variable | Description | Mandatory | Default Value |
|-----------------------------------|--------------------------------------------------------------------------------------------------------------|---------------------|-------------------------------------------------------|
| `API__TITLE` | The title of the API which is added to the generated OpenAPI. | No | `Inventory Management System API` |
| `API__DESCRIPTION` | The description of the API which is added to the generated OpenAPI. | No | `This is the API for the Inventory Management System` |
| `API__ROOT_PATH` | (If using a proxy) The path prefix handled by a proxy that is not seen by the app. | No | ` ` |
| `AUTHENTICATION__ENABLED` | Whether JWT auth is enabled. | Yes | `False` |
| `AUTHENTICATION__PUBLIC_KEY_PATH` | The path to the public key to be used for decoding JWT access token signed by the corresponding private key. | If JWT auth enabled | |
| `AUTHENTICATION__JWT_ALGORITHM` | The algorithm to use to decode the JWT access token. | If JWT auth enabled | |
| `DATABASE__PROTOCOL` | The protocol of the database to use for the `MongoClient` to connect to the database. | Yes | |
| `DATABASE__USERNAME` | The username of the database user for the `MongoClient` to connect to the database. | Yes | |
| `DATABASE__PASSWORD` | The password of the database user for the `MongoClient` to connect to the database. | Yes | |
| `DATABASE__HOSTNAME` | The hostname of the database to use for the `MongoClient` to connect to the database. | Yes | |
| `DATABASE__PORT` | The port of the database to use for the `MongoClient` to connect to the database. | Yes | |
| `DATABASE__NAME` | The name of the database to use for the `MongoClient` to connect to the database. | Yes | |

### JWT Authentication/Authorization
This microservice supports JWT authentication/authorization and this can be enabled or disabled by setting
the `AUTHENTICATION__ENABLED` environment variable to `True` or `False`. When enabled, all the endpoints require a JWT
access token to be supplied. This ensures that only authenticated and authorized users can access the resources. To
decode the JWT access token, the application needs the public key that corresponding to the private key used for
encoding the token. Once the JWT access token is decoded successfully, it checks that it has a `username` in the
payload, and it has not expired. This means that any microservice can be used to generate JWT access tokens so long as
it meets the above criteria. The [LDAP-JWT Authentication Service](https://github.com/ral-facilities/ldap-jwt-auth) is
a microservice that provides user authentication against an LDAP server and returns a JWT access token.

### Adding units

Units should be added to the MongoDB database using `mongoimport` on the provided units file found at `/data/units.json`. If adding more units to this file, ensure the `_id` values are valid `ObjectId`'s.
Units should be added to the MongoDB database using `mongoimport` on the provided units file found at
`/data/units.json`. If adding more units to this file, ensure the `_id` values are valid `ObjectId`'s.

#### Updating a local MongoDB instance

To update the list of units, replacing all existing with the contents of the `./data/units.json` file use the command

`mongoimport --username 'root' --password 'example' --authenticationDatabase=admin --db ims --collection units --type=json --jsonArray --drop ./data/units.json`

```bash
mongoimport --username 'root' --password 'example' --authenticationDatabase=admin --db ims --collection units --type=json --jsonArray --drop ./data/units.json
```
from the root directory of this repo, replacing the username and password as appropriate.

#### Updating a MongoDB instance running in a docker container

When running using docker first locate the running container with the instance of MongoDB using `docker ps`. Then use

`docker exec -i CONTAINER_ID sh -c "mongoimport --username 'root' --password 'example' --authenticationDatabase=admin --db ims --collection units --type=json --jsonArray --drop" < ./data/units.json`

```bash
docker exec -i CONTAINER_ID sh -c "mongoimport --username 'root' --password 'example' --authenticationDatabase=admin --db ims --collection units --type=json --jsonArray --drop" < ./data/units.json
```
Replacing `CONTAINER_ID` with the actual container ID of the MongoDB instance.
2 changes: 1 addition & 1 deletion inventory_management_system_api/logging.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ propagate=0
[handler_fileHandler]
class=logging.handlers.TimedRotatingFileHandler
formatter=fileFormatter
args=('inventory-management-system-api.log', 'D', 1, 20,)
args=('./logs/inventory-management-system-api.log', 'D', 1, 20,)

[handler_consoleHandler]
class=StreamHandler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
router = APIRouter(prefix="/v1/catalogue-categories", tags=["catalogue categories"])


@router.get(path="/", summary="Get catalogue categories", response_description="List of catalogue categories")
@router.get(path="", summary="Get catalogue categories", response_description="List of catalogue categories")
def get_catalogue_categories(
parent_id: Annotated[Optional[str], Query(description="Filter catalogue categories by parent ID")] = None,
catalogue_category_service: CatalogueCategoryService = Depends(),
Expand Down Expand Up @@ -97,7 +97,7 @@ def get_catalogue_category_breadcrumbs(


@router.post(
path="/",
path="",
summary="Create a new catalogue category",
response_description="The created catalogue category",
status_code=status.HTTP_201_CREATED,
Expand Down
4 changes: 2 additions & 2 deletions inventory_management_system_api/routers/v1/catalogue_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
router = APIRouter(prefix="/v1/catalogue-items", tags=["catalogue items"])


@router.get(path="/", summary="Get catalogue items", response_description="List of catalogue items")
@router.get(path="", summary="Get catalogue items", response_description="List of catalogue items")
def get_catalogue_items(
catalogue_category_id: Annotated[
Optional[str], Query(description="Filter catalogue items by catalogue category ID")
Expand Down Expand Up @@ -70,7 +70,7 @@ def get_catalogue_item(


@router.post(
path="/",
path="",
summary="Create a new catalogue item",
response_description="The created catalogue item",
status_code=status.HTTP_201_CREATED,
Expand Down
4 changes: 2 additions & 2 deletions inventory_management_system_api/routers/v1/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@


@router.post(
path="/",
path="",
summary="Create a new item",
response_description="The created item",
status_code=status.HTTP_201_CREATED,
Expand Down Expand Up @@ -77,7 +77,7 @@ def delete_item(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=message) from exc


@router.get(path="/", summary="Get items", response_description="List of items")
@router.get(path="", summary="Get items", response_description="List of items")
def get_items(
item_service: Annotated[ItemService, Depends(ItemService)],
system_id: Annotated[Optional[str], Query(description="Filter items by system ID")] = None,
Expand Down
4 changes: 2 additions & 2 deletions inventory_management_system_api/routers/v1/manufacturer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@


@router.post(
path="/",
path="",
summary="Create new manufacturer",
response_description="The new manufacturer",
status_code=status.HTTP_201_CREATED,
Expand All @@ -51,7 +51,7 @@ def create_manufacturer(


@router.get(
path="/",
path="",
summary="Get all manufacturers",
response_description="List of manufacturers",
)
Expand Down
Loading