kvaas es una api que tiene la capacidad de almacenar credenciales de acceso y luego poder acceder a ellas a través de una API publica con un token de acceso.
Está escrito completamente con Node.js y Typescript :)
- Node.js > 12
- Docker
- Bash
- Make
- Crear un archivo
.env
en la ruta principal del proyecto. - Agregar datos en las variables de conexión de la base de datos. (estás variables serán utilizadas cuando se levante el contenedor Docker de Mongo)
- Levantar el contenedor de mongo ejecutando desde la consola:
$ make mongo-run
- Acceder a Mongo a través de cualquier cliente, con las credenciales especificadas en la variable de entorno.
- Crear la base de datos en Mongo
- Crear un usuario para la base de datos creada
- Crear un token de acceso en el proyecto donde se gestionarán las claves en Github. (no es este repositorio)
- Copiar el token obtenido de Github en el archivo `.env``
- Llenar los otros parámetros requieridos en el .env
- Realizar la migración ejecutando desde la consola:
$ make mongo-migrate
Nota: La migración solo es para crear la colección users y agregar el usuario de administración. El resto de colecciones se irán creando de forma dinámica, cuando se agreguen datos al repositorio de claves.
- Para empezar a desarrollar, ejecute desde la consola: '$make dev
o
npm run dev`
- Declara el nuevo controller dentro de
./api/interface/controllers
. Ejemplo:
import {Request, Response} from 'express'
import { IController } from '@kvaas:TypesDefinitions';
export const NewController: IController = {
endpoint: '',
method: '',
handler: async (req: Request, res: Response) => {
res.send("ok")
}
}
- El controller creado anteriormente, se debe cargar en el servidor. Para ello, modifica el archivo
./api/main.ts
import { NewController } from './interface/controllers/NewController';
const controllers: IController[] = [
...,
...,
...,
NewController
]
La implementación se debe escribir en un nuevo archivo con extensión .ts
y que deberá ser almacenado dentro de la ruta ./api/domain
.
Este archivo únicamente contiene los métodos que deberán ser implementados por el repositorio actual para ser utilizado en los casos de uso.
Ejemplo:
export class NewRepositoryDomain {
methodToBeImplemented(param:string) {
throw new Error('ERR_METHOD_NOT_IMPLEMENTED');
}
}
Esta firma permite que nuevas implementaciones puedan ser desarrolladas con facilidad. Esto quiere decir que, si en algún momento se migra de Mongo a MySQL, deberán respetar la firma anteriormente establecida. Cool verdad?
Aquí se debe crear la clase con los métodos de la firma establecida anteriormente que permitirá manipular la información con la base de datos que actualmente se esté utilizando. Crea la clase en la ruta ./api/infra/repositories/mongo
.
Es importante mencionar que los controladores no deben acceder a esta clase directamente. La capa intermedia de acceso son los casos de uso. Con esto, el controlador no se ata a una implementación como tal (para Mongo por ejemplo).
Mongo es el soporte actual, por lo tanto, el código de consultas que se implemente en esta clase, es especificamente para Mongo. Si en algún momento se decide migrar de Mongo a MySQL, entonces habrá un repositorio para MySQL con una carpeta llamada mysql y dentro de esta, estarán las clases correspondientes.
import { Db } from 'mongodb'
import { NewRepositoryDomain } from '../../../domain/NewRepositoryDomain';
export class NewRepository implements NewRepositoryDomain {
private db:Db
constructor(db: Db) {
this.db = db
}
async methodToBeImplemented(param:string) {
//aquí va la lógica de consultas y persistencia de datos
}
}
¿Por qué? una clase en lugar de una función? Bueno, en el proceso de inicialización, cada repositorio es inyectado a través de su constructor, la conexión de la base de datos. Puedes ver el gráfico de ciclo de vida de inicialización de la aplicación
- Registrar tu repositorio en el contenedor de repositorios. Para ello, deberás modificar el archivo ubicado en
./api/infra/bootstrap/dialect.ts
export default async () => {
...
...
...
//support for MongoDB
if (environment.database.dialect === constants.SUPPORTED_STORAGE.MONGO.NAME) {
...
...
const { NewRepository } = await import('../../infra/repositories/mongo/NewRepository');
...
...
container.register({
db: asValue(db),
...,
...,
...,
newRepository: asClass(NewRepository)
});
}
...
...
...
return await container
}
Con los pasos anteriormente realizados, ahora dispondrás del repositorio en cada petición.
El caso de uso es una capa intermedia que permite abstraer al controlador que motor de base de datos se está utilizando. Incluso, para la capa usecase, es desconocido cual es la implementación actual porque solo conoce de las firmas del contrato (los métodos disponibles). El contenedor de registro si conoce que motor de base de datos se está utilizando por debajo, pero solo provee acceso a los métodos a través de la firma del contrato.
Con la breve introducción del caso de uso, procedemos a crear el caso de uso para NewRepository. Creamos un archivo con extensión .ts
dentro de la ruta ./api/usecase
import { AwilixContainer } from "awilix";
import { UserRepositoryDomain } from "../domain/NewRepository";
export const FindSomething = async (container: AwilixContainer, params:any) => {
//container.resolve('newRepository') es otra alternativa para obtener el repositorio. Sin embargo, no provee intellysense.
//por lo tanto, el editor de código no te mostrará automaticamente la lista de métodos implementados.
//Por ese motivo, es mejor mapear a la estructura correspondiente como se muestra a continuación.
const repository = container.resolve<UserRepositoryDomain>('newRepository')
const outcome = repository.methodToBeImplemented(param)
return outcome
}
Como se puede observar en el código anterior, este caso de uso no conoce en lo absoluto la implementación de la clase NewRepository
creada para mongo. Pero si utiliza la firma del contrato que se ha especificado en domain
.
Si algún rato, MySQL se quiere utilizar en lugar de mongo, el controlador y el caso de uso no se les modificará para nada.
El contenedor que contiene(valga la redundancia) los repositorios, se inyecta como un parámetro más dentro del objecto app
de express.js
. Esto pasa en el ciclo de inicialización de la aplicación. Ver el gráfico de dicho ciclo más adelante.
Utilizando el ejemplo anterior
import {Request, Response} from 'express'
import { IController } from '@kvaas:TypesDefinitions';
export const NewController: IController = {
endpoint: 'RUTA DE ACCESO',
method: 'get|post|put|delete',
handler: async (req: Request, res: Response) => {
//obteniendo el contenedor de repositorios
const container = req.app.container
}
}
### Acceder alcontenedor de repositorios en un middleware
Puedes ver un ejemplo real en el middleware de authorización ubicado en ./api(infra/webserver/middleware/auth.ts
.
Pero en resumen, un middleware en express.js tiene una estructura similar a la de un controlador, con la diferencia de que tiene un tercer parámetro: este es next
. Entonces para acceder al contenedor de repositorios, se lo hace de la misma manera que como se lo hizo anteriormente:
import { Request, Response, NextFunction } from 'express';
export const middleware = (req: Request, res: Response, next: NextFunc) => {
//obteniendo el contenedor de repositorios.
const container = req.app.container
//llamar al caso de uso pasándole el container
}
Recuerda que este contenedor deberás pasar a los casos de usos para que puedan utilizar los métodos correspondientes de la implementación actual.
Ahora veamos un ejemplo integrando todos los juguetes.
controller ubicado en ./api/interface/controllers/NewController.ts
//controller
import {Request, Response} from 'express'
import { IController } from '@kvaas:TypesDefinitions';
import { FindSomething } from '../../usecase/CloudService';
export const NewController: IController = {
endpoint: '',
method: '',
handler: async (req: Request, res: Response) => {
const outcome = FindSomething(req.app.container, SOME_STRING_VALUE)
}
}
Y tu caso de uso es (del ejemplo de caso de uso creado anteriormente):
usecase ubicado en ./api/domain/controllers/NewController.ts
//usecase
import { AwilixContainer } from "awilix";
import { UserRepositoryDomain } from "../domain/NewRepository";
export const FindSomething = async (container: AwilixContainer, params:string) => {
const repository = container.resolve<UserRepositoryDomain>('newRepository')
const outcome = repository.methodToBeImplemented(param)
return outcome
}
Como te habrás dado cuenta, en varias importaciones nos encontramos con
import { IController } from '@kvaas:TypesDefinitions';
Son las deficiones para el proyecto actual. Si necesitas agregar más deficiones, entonces deberás modificar el archivo ./api/types/kvaas/index.d.ts
declare module '@kvaas:TypesDefinitions' {
import { Request, Response } from "express";
export interface IDb {
db: any
close(): any;
connect(): any;
}
export interface IController {
endpoint: string;
method: string;
handler: (req: Request, res: Response) => {};
}
export interface StatusOperation {
status: string;
code: number;
data: any;
}
export interface HttpValidationRequest {
status: string;
code: number;
message: string
}
}
Si no quieres utilizar el archivo anterior y crear tu propio espacio de nomnbres: entonces deberás declarar tu definición con extensión .d.ts
y cargarlo en el archivo ./api/main.ts
en la parte superior.
/// <reference path="./types/kvaas/newDefinition.d.ts"/>
authentication: Bearer Token
endpoint: /api/v1/users
method: POST
body: (JSON)
Key | Type | Limit | Required | Observations |
---|---|---|---|---|
string | 255 | si | - | |
password | string | 255 | si | - |
Por ejemplo:
{
"email": EMAIL,
"password": PASSWORD
}
body: (JSON)
Key | Type | Value | Observations |
---|---|---|---|
status | string | - | - |
status | code | - | - |
status | message | token generado | El token generado, deberá ser entregado al desarrollador o cliente externo. |
Por ejemplo:
{
"status": STATUS,
"code": HTTP_STATUS_CODE,
"message": TOKEN_GENERATED
}
curl \
--request POST 'localhost:3000/api/v1/users' \
--header 'Authorization: Bearer {TOKEN_GENERATE_IN_MIGRATION_PROCESS}' \
--header 'Content-Type: project/json' \
--data-raw '{
"email": "[email protected]",
"password": "1234567890"
}'
En la mayoría de servicios cloud, se requiere un endpoint para verificar el estado de la aplicación. Este parámetro es configurable a través de la variable de entorno HEALTH_ENDPOINT
authentication: Sin autenticación
endpoint: HEALTH_ENDPOINT
method: GET
body: ninguno
body: string
Key | Type | Value | Observations |
---|---|---|---|
status | string | "ok" | - |
Por ejemplo:
ok
curl --silent --location --request GET 'localhost:3000/health'
authentication: Bearer Token
endpoint: /api/v1/services
method: GET
body: ninguno
body: json
Key | Type | Value | Observations |
---|---|---|---|
cloudServices | array [string] | id de los servicios cloud | - |
Por ejemplo:
{
"cloudServices": [
"_aws",
"_gcp"
]
}
¿Por qué el prefijo? Porque cada servicio registrado en los archivos YAML, se crea una colección, y claro, más colecciones pueden crearse dentro de Mongo, entonces este prefijo se agrega automáticamente cuando se crea la colección con el propósito de idenfiticar que es una colección de un servicio cloud.
curl \
--request GET 'localhost:3000/api/v1/services' \
--header 'Authorization: Bearer {USER_TOKEN}'
Los proyectos son por ejemplo: ClubMiles, Ecosistemas, etc.
authentication: Bearer Token
endpoint: /api/v1/services/{SERVICE_ID}/projects
method: GET
body: ninguno
Key | Type | Value | Observations |
---|---|---|---|
SERVICE_ID | string | id del servicio cloud | - |
body: json
Key | Type | Value | Observations |
---|---|---|---|
projects | array [string] | id de los proyectos que están en el servicio cloud con id SERVICE_ID | - |
Por ejemplo:
{
"projects": [
{
"_id": "pichinchamiles"
},
{
"_id": "clubmiles"
}
]
}
curl \
--request GET 'localhost:3000/api/v1/services/{SERVICE_ID}/projects' \
--header 'Authorization: Bearer {USER_TOKEN}'
Una aplicación puede tener credenciales de acceso para diferentes ambientes. Este endpoint devolverá en que ambientes el usuario tendrá acceso para una aplicación en particular.
authentication: Bearer Token
endpoint: /api/v1/services/{CLOUD_ID}/projects/{PROJECT_ID}/environments
method: GET
body: ninguno
Key | Type | Value | Observations |
---|---|---|---|
CLOUD_ID | string | id del servicio cloud | - |
PROJECT_ID | string | id de la aplicación | - |
body: json
Key | Type | Value | Observations |
---|---|---|---|
environments | array [string] | id de los environments asociados a un projecto que está registrada en un servicio cloud | - |
Por ejemplo:
{
"environments": [
{
"_id": "production"
},
{
"_id": "staging"
}
]
}
curl \
--request GET 'localhost:3000/api/v1/services/{CLOUD_ID}/projects/{SERVICE_ID}/environments' \
--header 'Authorization: Bearer {USER_TOKEN}'
El administrador puede crear datos de acceso a aplicaciones web, servicios web, etc., ya sea para un grupo de usuarios o para un único usuario. Este endpoint permitirá obtener las credenciales de acceso a las que un usuario está asociado a una aplicación dentro de un proyecto como tal.
Por ejemplo. Clubmiles es el proyecto, y las aplicaciones asociadas son: Aplicación web, Base de datos, etc.
authentication: Bearer Token
endpoint: /api/v1/services/{CLOUD_ID}/projects/{PROJECT_ID}/environments/{ENVIRONMENT_ID}
method: GET
body: ninguno
parametros en la url
Key | Type | Value | Observations |
---|---|---|---|
CLOUD_ID | string | id del servicio cloud | - |
PROJECT_ID | string | id de la aplicación | - |
ENVIRONMENT_ID | string | id del ambiente | - |
query string
Key | Type | Value | Observations |
---|---|---|---|
kind | string | nombre de la aplicación | - |
ejemplo 1: sin usar query string
body: json
Key | Type | Value | Observations |
---|---|---|---|
status | string | "ok | failed" |
code | int | HTTP_STATUS_CODE | - |
credentials | array[JSON] | lista JSON con todos los servicios a los que está asociado el usuario | - |
Por ejemplo:
{
"status": "ok",
"code": 200,
"credentials": [
{
"projectName": NOMBRE DEL PROYECTO,
"environment": {ENVIRONMENT_ID},
"kind": TIPO_DE_APLICACION,
"description": DESCRIPCIÓN DE LA APLICACIÓN,
"key": NOMBRE_DE_USUARIO|CORREO_ELECTRONICO|OTROS,
"value": CONTRASEÑA|TOKEN|IDENTIFICADOR_UNICO,
"endpoint": IP_PARA_CONECTARSE_AL_SERVICIO
},
{
"projectName": NOMBRE DE LA APLICACION_B,
"environment": {ENVIRONMENT_ID},
"kind": TIPO_DE_APL,
"description": DESCRIPCIÓN DE LA APLICACIÓN,
"key": NOMBRE_DE_USUARIO|CORREO_ELECTRONICO|OTROS,
"value": CONTRASEÑA|TOKEN|IDENTIFICADOR_UNICO,
"endpoint": IP_PARA_CONECTARSE_AL_SERVICIO
},
]
}
curl \
--request GET 'localhost:3000/api/v1/services/{CLOUD_ID}/projects/{SERVICE_ID}/environments' \
--header 'Authorization: Bearer {USER_TOKEN}'
Actualmente no existen pipelines creados para realizar un flujo CI/CD automatizado. Esta consideración y tarea la debe tomar el equipo de DevOps.
Sin embargo, para propósitos de pruebas en Cloud, hay algunos comandos que permiten realizar manualmente el despliegue de la aplicación en Google Cloud Run. Para ello, deberás configurar tu entorno con la cuenta y el proyecto de Google Correspondiente.
Los comandos para llevar a cabo el despliegue manual son:
$ make docker-build
$ make docker-publish
$ make deploy-on-gcr
Esta secuencia de comandos puedes utilizarlo en un pipeline en GithubAction, Bibucket Pipeline, CircleCI, pero ya queda a consideración del equipo de DevOps llevar a cabo esta tarea de automatizar el flujo de CI/CD. ¿Deseas crear un flujo CI/CD con la secuencia de comandos anterior? Mira este ejemplo para más información
La implementación actual tiene un archivo Makefile como punto centralizado para llevar a cabo cualquier acción: construir el proyecto, ejecutar el servidor, construir la imagen docker, desplegar en la nube, entre otros. A continuación, se muestra una lista de cada uno de los comandos que puedes utilizar.
$ make dev
: Permite levantar la aplicación web para empezar a desarrollar. Por debajo es un simple $ npm run dev
.
$ make build
: Permite construir la aplicación (compila de Typescript a Javascript) para la puesta en producción. Por debajo es un simple $ npm run build
$make clean
: Remueve los bundles construidos para la aplicación. Por debajo es un simple$ npm run clean
$ make docker-build
: Construirá la imagen Docker con la aplicación. Por debajo es una llamada al script./scripts/docker/build.sh
$ make docker-run
: Ejecuta el contenedor Docker con la aplicación. Por debajo es una llamada al script./scripts/docker/run.sh
$ make docker-publish
: Publica el contenedor docker en un repositorio de contenedores. Para ello deberás estár conectado a dicho contenedor. Por debajo es una llamada al script./scripts/docker/publish.sh
Actualmente solo existe un script
para subir la aplicación en Google Cloud Run.
$ make deploy-on-gcr
: Desplegará la aplicación en Google Cloud Run. Por debajo es una llamada al script./deploy/google/gcr.sh
. Este comando previamente construye la imagen y la publica en el registro de contenedores correspondiente.
$ make mongo-run
: Levantará un contenedor con Mongo. Creará un volumen al mismo nivel de la aplicación para persistir la data.
$ make mongo-stop
: Apagará el contenedor y lo destruirá.
$ make mongo-migrate
: Permitirá crear la colección user y el usuario administrador correspondiente.
A continuación, se muestra un esquema de tres escenerios: creación y almacenamiento de claves, creación de usuarios y token de accesos y, consulta de claves.
Aunque kvaas no es un sistema complejo como tal, tiene sus bases en una arquitectura limpia. El propósito es separar de mejor manera responsabilidades, extender funcionalidades a través de contratos sin cambiar el core, entre otros beneficios.
├── api/
│ ├── domain /
│ ├── infra /
│ │ ├── boostrap /
│ │ ├── config /
│ │ ├── migrations /
│ │ ├── repositories /
│ │ │ ├── mongo /
│ │ ├── storage /
│ │ ├── webserver /
│ │ │ ├── middleware /
│ ├── interface /
│ ├── types /
│ │ ├── express /
│ │ ├── kvaas /
│ ├── usecase /
│ ├── utils /
│ │ ├── main.ts
├── build /
├── deploy /
├── docker-volumen /
├── docs /
├── iaac /
├── node_modules /
├── scripts /
├── tests /
├── .env
├── .env.example
├── .gitignore
├── CONTRIBUTING.md
├── Makefile
├── nodemon.json
├── package-lock.json
├── package.json
├── README.MD
├── tsconfig.json
api
: Api contiene toda la lógica de kvaas.domain
: Contiene los contratos para procesar la información.infra
: Contiene todo lo que corresponde a la parte de infraestructura y externo al modelo de negocio de la aplicación.bootstrap
: Aquí se carga el motor de base de datos que es especificado a través de variables de entorno. Actualmente solo está soportado para Mongo. Sin embargo, hay una guía que permite extender el soporte para MySQL. Aquí se utiliza singleton para la instancia de la base de datos e inyección de dependencias para los repositories.config
: Contiene toda la información de constantes y variables de entorno que se cargan en el instante de iniciar la apliación.migrations
: Contiene los scripts para migraciones. Actualmente solo existe para la creación de la colecció de users en Mongo. Las demás colecciones son creadas automáticamente cuando se trae la información de Github.repositories
: Contiene todas las interfaces de peticiones a la base de datos correspondiente implementada. Actualmente solo hay los repositorios que permiten conectarse a la base de datos de Mongo y leer, escribir, actualizar la información en Mongo.storage
: Contiene la conexión a la base de datos que se requiere. Actualmente solo tiene la implementación para Mongo.types
: Dado que se está trabajando con Typescript, en esta carpeta se agregan las interfaces que permiten extender el core de Express y definir contratos para kvaas.usecase
: Aquí se implementan los métodos que permiten devolver o almacenar la información en el motor de Base de datos que se haya implementado. Aquí no hay ningúna lógica de consultas, solo métodos de las firmas requeridas.utils
: pequeñas librerías utilizadas por el framework para realizar operaciones adicionales no disponibles en el framework como tal.main.ts
: el punto de entrada de la aplicación.
build
: Sirve para almacenar todos los elementos que serán construidos. Ya sean bundles de la aplicación así como archivos como Dockerfile para construir los contenedores.deploy
: Contiene scripts o archivos de configuración que permitirán desplegar la aplicación en cualquier servicio en la nube. Actualmente, existen un script para desplegar la aplicación en `Google Cloud Run``docker-volume
: Es una carpeta que se generará cuando se levante el contenedor de Mongo. Permite persistir la información incluso después de que el contenedor sea borrado o apagado. Esta carpeta no tiene seguimiento git.docs
: En esta carpeta se almacena todo tipo de archivos e información que permita tener mayor información del proyecto.iaac
: (Infraestructure as a Code) En esta carpeta se debe colocar archivos que permitan crear infraestructura para el proyecto actual. Por ejemplo, archivos de terraform, asinble, entre otros.node_modules
: archivo que contiene las dependencias de la API. Nada que hacer aquí.scripts
: Contiene una serie de scripts que permiten gestionar los build, los deploys, iaacs, etc. Actualmente hay scripts para levantar, construi, publicar y ejecutar el contenedor Docker de la aplicación.tests
: Carpeta para pruebas unitarias, pruebas de integración y/o pruebas end to end. Actualmente no existen pruebas.
Se ha preparado una documentación separada donde encontrará lineamientos para poder contribuir al proyecto. Por favor, lea el documento CONTRIBUTING para conocer más acerca de dichos lineamientos. Es importante mencionar que los lineamientos están enfocados en buenas prácticas, como gestionar el flujo de trabajo de GIT, cómo manejar el versionamiento, cómo realizar commits, entre otros.