Skip to content

Commit

Permalink
fix: build status (#451)
Browse files Browse the repository at this point in the history
* feat: add build check status

* feat: init status

* fix: app startup

* fix: call :check

* feat: status state

* fix: kub url

* fix: kub url

* fix: add alias

* fix: build error & already exist

* fix: credo

* fix: format

* fix: credo

* fix: prod secret

* fix: config

* fix: test

* fix: test

* fix: dialyzer
  • Loading branch information
pichoemr authored Sep 28, 2023
1 parent 09301c5 commit ebceb27
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 13 deletions.
5 changes: 4 additions & 1 deletion apps/lenra/lib/lenra/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule Lenra.Application do
use Application

alias Lenra.Errors.BusinessError
alias Lenra.Kubernetes

require Logger

Expand Down Expand Up @@ -59,7 +60,8 @@ defmodule Lenra.Application do
end},
id: :finch_gitlab_http
),
{Cluster.Supervisor, [Application.get_env(:libcluster, :topologies), [name: Lenra.ClusterSupervisor]]}
{Cluster.Supervisor, [Application.get_env(:libcluster, :topologies), [name: Lenra.ClusterSupervisor]]},
Lenra.Kubernetes.StatusDynSup
]

# See https://hexdocs.pm/elixir/Supervisor.html
Expand All @@ -70,6 +72,7 @@ defmodule Lenra.Application do
res = Supervisor.start_link(children, opts)
Lenra.Seeds.run()
Logger.info("Lenra Supervisor Started")
Kubernetes.StatusDynSup.init_status()
res
end
end
7 changes: 5 additions & 2 deletions apps/lenra/lib/lenra/apps.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ defmodule Lenra.Apps do
"""
import Ecto.Query

alias Lenra.Kubernetes.StatusDynSup
alias Lenra.Repo
alias Lenra.Subscriptions

alias Lenra.{Accounts, EmailWorker, GitlabApiServices, KubernetesApiServices, OpenfaasServices}
alias Lenra.{Accounts, EmailWorker, GitlabApiServices, OpenfaasServices}

alias Lenra.Kubernetes.ApiServices

alias Lenra.Apps.{
App,
Expand Down Expand Up @@ -247,7 +250,7 @@ defmodule Lenra.Apps do
)

"kubernetes" ->
KubernetesApiServices.create_pipeline(
ApiServices.create_pipeline(
app.service_name,
app.repository,
app.repository_branch,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
defmodule Lenra.KubernetesApiServices do
defmodule Lenra.Kubernetes.ApiServices do
@moduledoc """
The service used to call Kubernetes API.
Curently only support the request to create a new pipeline.
"""

alias Lenra.Apps
alias Lenra.Apps.Build
alias Lenra.Apps.Deployment
alias Lenra.Kubernetes.StatusDynSup
alias Lenra.Repo
require Logger

@doc """
Expand All @@ -14,7 +18,14 @@ defmodule Lenra.KubernetesApiServices do
The build_id is the id of the freshly created build. It is used to set to create the runner callback url
The build_number is the number of the freshly created build. It us used to set the docker image URL.
"""
def create_pipeline(service_name, app_repository, app_repository_branch, build_id, build_number) do
def create_pipeline(
service_name,
app_repository,
app_repository_branch,
build_id,
build_number,
retry \\ 0
) do
runner_callback_url = Application.fetch_env!(:lenra, :runner_callback_url)
runner_secret = Application.fetch_env!(:lenra, :runner_secret)
kubernetes_api_url = Application.fetch_env!(:lenra, :kubernetes_api_url)
Expand All @@ -35,7 +46,9 @@ defmodule Lenra.KubernetesApiServices do

base64_repository = Base.encode64(app_repository)
base64_repository_branch = Base.encode64(app_repository_branch || "")

base64_callback_url = Base.encode64("#{runner_callback_url}/runner/builds/#{build_id}?secret=#{runner_secret}")

base64_image_name = Base.encode64(Apps.image_name(service_name, build_number))

secret_body =
Expand Down Expand Up @@ -191,9 +204,53 @@ defmodule Lenra.KubernetesApiServices do
}
})

Finch.build(:post, jobs_url, headers, body)
|> Finch.request(PipelineHttp)
|> response()
response =
Finch.build(:post, jobs_url, headers, body)
|> Finch.request(PipelineHttp)
|> response()

case response do
{:ok, _data} = response ->
response

:secret_exist ->
Finch.build(:delete, secrets_url <> "/build_name", headers)
|> Finch.request(PipelineHttp)
|> response()

if retry < 1 do
create_pipeline(
service_name,
app_repository,
app_repository_branch,
build_id,
build_number,
retry + 1
)
else
set_fail(build_id)
end

_error ->
set_fail(build_id)
end

StatusDynSup.start_build_status(build_id, kubernetes_build_namespace, build_name)

response
end

defp set_fail(build_id) do
build = Repo.get(Build, build_id)
deployment = Repo.get_by(Deployment, build_id: build_id)

Ecto.Multi.new()
|> Ecto.Multi.update(:build, Apps.update_build(build, %{status: :failure}))
|> Ecto.Multi.update(
:deployment,
Apps.update_deployement(deployment, %{status: :failure})
)
|> Repo.transaction()
end

defp response({:ok, %Finch.Response{status: status_code, body: body}})
Expand All @@ -206,8 +263,19 @@ defmodule Lenra.KubernetesApiServices do
raise "Kubernetes API could not be reached. It should not happen. #{reason}"
end

defp response({:ok, %Finch.Response{status: status_code, body: body}})
when status_code not in [200, 201, 202] do
raise "Kubernetes API error (#{status_code}) #{body}"
defp response(
{:ok,
%Finch.Response{
status: status_code
}}
)
when status_code in [409] do
:secret_exist
end

defp response({:ok, %Finch.Response{status: status_code, body: body}}) do
Logger.critical("#{__MODULE__} kubernetes return status code #{status_code} with message #{inspect(body)}")

{:error, :kubernetes_error}
end
end
148 changes: 148 additions & 0 deletions apps/lenra/lib/lenra/kubernetes/status.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
defmodule Lenra.Kubernetes.Status do
@moduledoc """
Lenra.Kubernetes.Status check status for kubernetes build
"""
use GenServer
use SwarmNamed

alias Lenra.Apps.Build
alias LenraCommon.Errors.DevError
alias LenraCommon.Errors.TechnicalError
alias Lenra.{Apps, Repo}

require Logger

@check_delay 10_000

def start_link(opts) do
case Keyword.fetch(opts, :build_id) do
{:ok, build_id} ->
GenServer.start_link(__MODULE__, opts, name: get_full_name({build_id}))

:error ->
raise DevError.exception(message: "Status need a build_id, a namespace and an job_name")
end
end

def init(init_arg) do
{:ok, build_id} = Keyword.fetch(init_arg, :build_id)
{:ok, namespace} = Keyword.fetch(init_arg, :namespace)
{:ok, job_name} = Keyword.fetch(init_arg, :job_name)

{:ok, [build_id: build_id, namespace: namespace, job_name: job_name]}
end

def handle_info(:check, state) do
case check_job_status(state) do
:success -> check_and_update_build_status(state[:build_id], :success)
:failure -> check_and_update_build_status(state[:build_id], :failure)
:running -> Process.send_after(self(), :check, @check_delay)
end

{:noreply, state}
end

defp check_job_status(job) do
Logger.debug("#{__MODULE__} Check job status for #{inspect(job)}")
kubernetes_api_url = Application.fetch_env!(:lenra, :kubernetes_api_url)
kubernetes_api_token = Application.fetch_env!(:lenra, :kubernetes_api_token)

url = "#{kubernetes_api_url}/apis/batch/v1/namespaces/#{job[:namespace]}/jods/#{job[:job_name]}/status"

headers = [{"Authorization", "Bearer #{kubernetes_api_token}"}]

Finch.build(:get, url, headers)
|> Finch.request(PipelineHttp)
|> response()
|> case do
{:ok, body} ->
body
|> extract_job_status
|> case do
%{"succeeded" => 1} ->
Logger.debug("#{__MODULE__} Check job #{inspect(job)} success")
:success

%{"failed" => 1} ->
Logger.debug("#{__MODULE__} Check job #{inspect(job)} failure")
:failure

_error ->
Logger.debug("#{__MODULE__} Check job #{inspect(job)} frunning")
:running
end

{:error, reason} ->
Logger.debug("#{__MODULE__} Error while fetching job status")
{:error, reason}
end
end

defp extract_job_status(response) do
# Extract the job status from the response
response["status"]
end

defp check_and_update_build_status(build_id, status) do
build = Repo.get(Build, build_id)

if build.status == status do
{:stop, :normal, [], nil}
else
update_build_status(build, status)
end
end

defp update_build_status(build, status) do
Logger.debug("#{__MODULE__} Update build status tp #{status}")

case Apps.update_build(build, %{status: status}) do
{:ok, _res} ->
update_deployment(build)
{:stop, :normal, [], nil}

{:error, _reason} ->
Logger.error("#{__MODULE__} Error while updating build status")
{:stop, :normal, [], nil}
end
end

def update_deployment(build) do
build.id
|> Apps.get_deployement_for_build()
|> Apps.update_deployement(%{status: :failure})
end

defp response({:ok, %Finch.Response{status: 200, body: body}}) do
{:ok, Jason.decode!(body)}
end

defp response({:ok, %Finch.Response{status: status_code, body: body}}) do
case status_code do
400 ->
Logger.critical(TechnicalError.bad_request(body))
TechnicalError.bad_request_tuple(body)

404 ->
Logger.error(TechnicalError.error_404(body))
TechnicalError.error_404_tuple(body)

500 ->
Logger.critical(TechnicalError.bad_request(body))
TechnicalError.error_500_tuple(body)

504 ->
Logger.critical(TechnicalError.error_500_tuple(body))
TechnicalError.error_500_tuple(body)

_err ->
Logger.critical(TechnicalError.unknown_error(body))
TechnicalError.unknown_error_tuple(body)
end
end

def terminate(_reason, state) do
# Perform cleanup operations here, if needed
{:ok, state}
end
end
72 changes: 72 additions & 0 deletions apps/lenra/lib/lenra/kubernetes/status_dyn_sup.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
defmodule Lenra.Kubernetes.StatusDynSup do
@moduledoc """
Lenra.Kubernetes.StatusDynSup Manage status Genserver
"""
use DynamicSupervisor

import Ecto.Query

alias Lenra.Apps.Build
alias Lenra.Kubernetes.Status
alias Lenra.Repo

require Logger

def start_link(opts) do
DynamicSupervisor.start_link(__MODULE__, opts, name: {:via, :swarm, __MODULE__})
end

@impl true
def init(_init_arg) do
Logger.debug("#{__MODULE__} init")
DynamicSupervisor.init(strategy: :one_for_one)
end

def start_build_status(build_id, namespace, job_name) do
Logger.debug("#{__MODULE__} ensure start status for #{inspect([build_id, namespace, job_name])}")

case start_child(build_id, namespace, job_name) do
{:ok, pid} ->
Logger.info("Lenra.Kubernetes.Status started")
Process.send_after(pid, :check, 10_000)
{:ok, pid}

{:error, {:already_started, pid}} ->
{:ok, pid}

err ->
Logger.critical(inspect(err))
err
end
end

defp start_child(build_id, namespace, job_name) do
init_value = [
build_id: build_id,
namespace: namespace,
job_name: job_name
]

DynamicSupervisor.start_child({:via, :swarm, __MODULE__}, {Status, init_value})
end

def init_status do
kubernetes_build_namespace = Application.fetch_env!(:lenra, :kubernetes_build_namespace)

builds =
Repo.all(
from(
b in Build,
where: b.status == :pending
)
)

Map.new(builds, fn build ->
preloaded_build = Repo.preload(build, :application)

build_name = "build-#{preloaded_build.application.service_name}-#{build.build_number}"

start_build_status(build.id, kubernetes_build_namespace, build_name)
end)
end
end
Loading

0 comments on commit ebceb27

Please sign in to comment.