Skip to content

Latest commit

 

History

History
251 lines (199 loc) · 8.67 KB

README.md

File metadata and controls

251 lines (199 loc) · 8.67 KB

Reactive Challenge

Hi! In this file you will find the most important information about a challenge resolution described in this file.

Chosen technologies

I choose to use Spring Webflux and Kotlin coroutines, which provides reactive programming support for web applications. I created the Dockerfile to build the docker image, sending it to the AWS ECR to be used by the ECS. The Jenkinsfile contains the description of the pipeline used to build, test and deploy the application to AWS.

Application Development

The application only had one endpoint, so it was really simple to implement. If we think in an up-down approach, we start by the RepositoryController.kt where we define the endpoint and pass the request to the service layer. The RepositoryService.kt is then responsible for calling a GitHub client wrapper( GithubClient.kt ) that will call the GitHub API and retrieve the data. Coroutines allows us to do this in a non-blocking way, so we can have better performance.

To run the application locally, have in mind that you need to have a GITHUB_TOKEN to be able to call the GitHub API. I had a .env.local file with the environment variables.

$ source .env.local
$ ./gradlew bootRun

I tested the service layer using unit tests available at RepositoryServiceTest.kt. The main purpose of these tests is to check if the service logic is working as expected. It is using Mockito to mock the response from the GitHub client and also JUnit to run the tests. In terms of integration tests, I created the test class RepositoryControllerTestIT.kt that is responsible for loading the whole application and test the endpoint. The GitHub Http api is mocked using MockWebServer from OkHttp. In the file RepositoryControllerMockTestIT.kt I mock the service and test error handling at the controller level.

Normally I have two different gradle tasks to run the tests, one for unit tests and another for integration tests. But this time, to simplify, it's only one task.

$ ./gradlew test

There was emphasis on how to deal when a GitHub user doesn't exist. I decided to create a custom exception that is thrown when the http client returns a 404 status code. The exception is then caught by the following ExceptionHandler.kt. The response is custom just like it was asked in the challenge description.

{
  "status": 404,
  "message": "Username not found: JohnDoe"
}

In the case of the client trying to request data in XML format, I handled it by telling the endpoint to produce only JSON responses.

@GetMapping("/{username}", produces = [MediaType.APPLICATION_JSON_VALUE])

In reality, Spring Webflux doesn't support XML responses natively, there is some workarounds as we can see in the following link. The problem with these workarounds is that they are not intuitive, and when I implemented them they simply didn't work. We have limited time to complete the challenge, so I decided that the following message was okay enough. I know the implications of having different formatted responses, specially for the ones consuming the API, it's not good, and I would never deliver it like this.

$ curl -H "Accept: application/xml" https://.../api/repository/JohnDoe
{
  "timestamp": "2024-01-15T22:55:50.391+00:00",
  "path": "/repository/JohnDoe",
  "status": 406,
  "error": "Not Acceptable",
  "requestId": "68bf40e6-4"
}

Code Quality & Standards

The code was written having in mind the best practices and clean code. I followed the Kotlin Coding Conventions and the comments are compliant with JavaDoc. The other practices are inside the code, and are better seen than explained.

Swagger

The Swagger documentation is generated automatically using a gradle task. I copied the generated swagger file from the build folder for us to have an example.

$ ./gradlew generateOpenApiDocs

This automatic generation can be useful later if we want to customize the API Gateway. With the swagger file we know exactly which endpoints we have and what are the expected responses.

Swagger also has a UI component that we can find after generating it automatically, when running the application.


Docker

The Dockerfile is very simple, it just copies the jar file and runs it. I also added comments to explain what we could do if we wanted also to build the jar. There is also the option of creating a native image, that basically uses GraalVM to compile the application to a native image, but it was a little out of the context. I decided to only copy the jar and not build it because we have the Jenkins pipeline for that.

Locally, we can run and build the docker image with the following commands:

$ docker build -t github-repos:latest .
$ docker run --env-file .env.local.docker -p 8090:8080 --name github-app github-repos

AWS & CloudFormation

I used the AWS CDK to create the infrastructure. It's a framework that allows to create the infrastructure using code, in the end it generates a CloudFormation template. It is a very simple way to create and manage resources in AWS as you can see in the stack file infra-stack.ts.

I started by creating the VPC.

const vpc = new Vpc(this, getNameWithEnv("GithubReposVpc"), {
    maxAzs: 2,
    natGateways: 1,
    restrictDefaultSecurityGroup: false,
});

Followed by the ECS cluster.

const appCluster = new ecs.Cluster(this, getNameWithEnv("GithubReposEcs"), {
    vpc: vpc,
    clusterName: getNameWithEnv("GithubReposCluster"),
});

And then the Fargate service with the ALB.

const sbApp = new ApplicationLoadBalancedFargateService(
    this,
    getNameWithEnv("GithubReposApp"),
    {
        cluster: appCluster,
        desiredCount: 1,
        cpu: 256,
        memoryLimitMiB: 512,
        taskImageOptions: {
            image: ecs.ContainerImage.fromAsset(".."),
            containerPort: 8080,
            secrets: {
                GITHUB_TOKEN: ecs.Secret.fromSecretsManager(
                    appSecrets,
                    "githubToken"
                ),
            },
        },
        assignPublicIp: false,
        publicLoadBalancer: false,
    }
);

Lastly, I just created the Rest Api Gateway to connect to the ALB and expose the application.

const api = new HttpApi(this, getNameWithEnv("GithubReposApi"));

api.addRoutes({
    path: "/{proxy+}",
    methods: [HttpMethod.ANY],
    integration: new HttpAlbIntegration(
        getNameWithEnv("GithubReposAppIntegration"),
        sbApp.listener
    ),
});

Jenkins

The following Jenkinsfile describes the pipeline used to build, test and deploy the application. I will also describe the steps here.

Starting by building the application.

stage('Build') {
    steps {
        sh './gradlew build -x test'
    }
}

Then, we run the tests. If I didn't have cheated with the tests, I would have two different tasks to run, unit and integration tests.

stage('Test') {
    steps {
        sh './gradlew test'
    }
}

After tests, it's usually a good idea to use SonarQube to analyze the code for bugs, vulnerabilities, smells, coverage, etc. I delayed that to another time, instead I deployed the application to AWS.

stage('Deploy') {
    dir('infra') {
        sh 'npm install'
        sh 'npx cdk deploy --require-approval never'
    }
}

It is also good to point out that we can generate the OpenAPI documentation before this stage and use it to customize the API Gateway.

Final considerations

I had a lot of fun doing this challenge. I hope you learned a little bit about me and my work. Bye, have a good day!