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

feat: Add Env Secrets in Server API #533

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d58d8e4
feat: Add Env Secrets in Server API
shiipou Feb 13, 2024
47a822d
fix: use k8s api to create and read env's secrets
shiipou Feb 13, 2024
dbeb4eb
chore(warning): remove warning bevause env_id is not used
shiipou Feb 13, 2024
8af3c2b
fix: add or remove secret from openfaas deployment
shiipou Feb 13, 2024
52b1734
fix: wrong matching :ok must be :error in k8s api fetch
shiipou Feb 13, 2024
30ce951
fix: add buisness error for the API responses
shiipou Feb 13, 2024
795c7dd
fix: fix some errors
shiipou Feb 13, 2024
a2454bf
fix: switch from rlang string (`'`) to elixir string (`"`)
shiipou Feb 13, 2024
0ba030b
fix: wrong error matching
shiipou Feb 13, 2024
ad3b028
fix: now we can list/create/delete
shiipou Feb 23, 2024
6bd8eb7
Merge branch 'main' into feat/Add-Env-Secrets-in-Server-API
shiipou Feb 23, 2024
0ac31ae
fix: variable "secrets" does not exist
shiipou Feb 23, 2024
5c01bc1
fix: openfaas call must now work ?
shiipou Feb 23, 2024
fd8e885
fix format
jonas-martinez Feb 27, 2024
8f813b6
fix some credo
jonas-martinez Mar 1, 2024
72177a5
fix(WIP): openfaas update
shiipou Mar 1, 2024
1efe68b
fix: Openfaas update now work
shiipou Mar 1, 2024
d7d6421
Merge branch 'main' into feat/Add-Env-Secrets-in-Server-API
jonas-martinez Mar 29, 2024
05091e9
Return error on kubernetes not configured
jonas-martinez Apr 2, 2024
8a1f176
Fix credo
jonas-martinez Apr 2, 2024
5a549ca
fix format
jonas-martinez Apr 2, 2024
3cf53fb
fix credo
jonas-martinez Apr 2, 2024
e5d0ece
fix credo
jonas-martinez Apr 2, 2024
4790525
fix credo
jonas-martinez Apr 2, 2024
ce46d0e
Merge branch 'main' into feat/Add-Env-Secrets-in-Server-API
jonas-martinez May 29, 2024
f14877a
fix dialyzer
jonas-martinez May 29, 2024
9ea41a6
fix format
jonas-martinez May 29, 2024
083cc5d
Merge branch 'main' into feat/Add-Env-Secrets-in-Server-API
jonas-martinez Jun 17, 2024
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
1,161 changes: 1,161 additions & 0 deletions .vscode/postman/lenra_server.postman_collection.json

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions .vscode/postman/local.postman_environment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"id": "697ac5e7-6171-4875-a90d-276e7bc0d3ee",
"name": "Local",
"values": [
{
"key": "lenra_endpoint",
"value": "http://localhost:4000",
"type": "default",
"enabled": true
},
{
"key": "hydra_endpoint",
"value": "http://localhost:4444",
"type": "default",
"enabled": true
},
{
"key": "hydra_redirect_url",
"value": "https://oauthdebugger.com/debug",
"type": "default",
"enabled": true
},
{
"key": "hydra_client_scope",
"value": "profile store manage:account manage:apps",
"type": "default",
"enabled": true
},
{
"key": "lenra_client_id",
"value": "",
"type": "default",
"enabled": true
}
],
"_postman_variable_scope": "environment",
"_postman_exported_at": "2024-02-06T12:00:49.888Z",
"_postman_exported_using": "Postman/10.22.13-240205-0449"
}
39 changes: 39 additions & 0 deletions .vscode/postman/staging.postman_environment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"id": "1461dd99-7ff3-4a84-a3d3-2b2f57fa466d",
"name": "Staging",
"values": [
{
"key": "lenra_endpoint",
"value": "https://api.staging.lenra.io",
"type": "default",
"enabled": true
},
{
"key": "hydra_endpoint",
"value": "https://auth.staging.lenra.io",
"type": "default",
"enabled": true
},
{
"key": "hydra_redirect_url",
"value": "https://oauthdebugger.com/debug",
"type": "default",
"enabled": true
},
{
"key": "hydra_client_scope",
"value": "profile store manage:account manage:apps",
"type": "default",
"enabled": true
},
{
"key": "lenra_client_id",
"value": "4f162a6c-dfdd-4c75-8305-097e30a19e6f",
"type": "default",
"enabled": true
}
],
"_postman_variable_scope": "environment",
"_postman_exported_at": "2024-02-06T11:58:57.655Z",
"_postman_exported_using": "Postman/10.22.13-240205-0449"
}
6 changes: 5 additions & 1 deletion apps/lenra/lib/lenra/errors/business_error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ defmodule Lenra.Errors.BusinessError do
"Currently not capable to handle this type of pipeline. (`pipeline_runner` can be: [GitLab, Kubernetes])"},
{:subscription_required, "You need a subscirption", 402},
{:stripe_error, "Stripe error"},
{:subscription_already_exist, "You already have a subscription for this app", 403}
{:subscription_already_exist, "You already have a subscription for this app", 403},
{:env_secret_already_exist, "You already have a secret with this key", 403},
{:env_secret_not_found, "The secret your tried to update didn't exist", 404},
{:api_return_unexpected_response, "A dependency API used in this call return an unexpected response", 500},
{:kubernetes_unexpected_response, "Kubernetes return an unexpected response", 500}
]
end
266 changes: 226 additions & 40 deletions apps/lenra/lib/lenra/kubernetes/api_services.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Lenra.Kubernetes.ApiServices do
alias Lenra.Apps.Deployment
alias Lenra.Kubernetes.StatusDynSup
alias Lenra.Repo
alias Ecto
require Logger

@doc """
Expand Down Expand Up @@ -44,44 +45,19 @@ defmodule Lenra.Kubernetes.ApiServices do

build_name = "build-#{service_name}-#{build_number}"

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 =
Jason.encode!(%{
apiVersion: "v1",
kind: "Secret",
type: "Opaque",
metadata: %{
name: build_name,
namespace: kubernetes_build_namespace
},
data: %{
APP_REPOSITORY: base64_repository,
REPOSITORY_BRANCH: base64_repository_branch,
CALLBACK_URL: base64_callback_url,
IMAGE_NAME: base64_image_name
}
})

secret_response =
Finch.build(:post, secrets_url, headers, secret_body)
|> Finch.request(PipelineHttp)
|> response(:secret)
create_k8s_secret(build_name, kubernetes_build_namespace, %{
APP_REPOSITORY: app_repository,
REPOSITORY_BRANCH: app_repository_branch || "",
CALLBACK_URL: "#{runner_callback_url}/runner/builds/#{build_id}?secret=#{runner_secret}",
IMAGE_NAME: Apps.image_name(service_name, build_number)
})

case secret_response do
{:ok, _} ->
{:ok} ->
Copy link
Collaborator Author

@jonas-martinez jonas-martinez Mar 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You changed this case but it causes an error when trying to deploy an application.

Here are the logs :

11:10:37.446 request_id=F7juFiPvtMtTt9AAAprh [info] POST /api/apps/389/builds
11:10:37.582 request_id=F7juFiPvtMtTt9AAAprh [info] Sent 500 in 135ms
11:10:37.583 [error] #PID<0.24822.0> running LenraWeb.Endpoint (connection #PID<0.24819.0>, stream id 3) terminated
Server: api.staging.lenra.io:80 (http)
Request: POST /api/apps/389/builds
** (exit) an exception was raised:
    ** (CaseClauseError) no case clause matching: {:ok, %{APP_REPOSITORY: "https://github.com/lenra-io/template-bun-js.git", CALLBACK_URL: "http://lenra-server.lenra.svc.cluster.local:4000/runner/builds/915?secret=**********", IMAGE_NAME: "registry.gitlab.com/lenra/platform/lenra-ci/staging/dd14cd88-07d9-4711-9d4c-1a1d9d02b8b4:2", REPOSITORY_BRANCH: "secret"}}
        (lenra 0.1.0) lib/lenra/kubernetes/api_services.ex:56: Lenra.Kubernetes.ApiServices.create_pipeline/6
        (lenra 0.1.0) lib/lenra/apps.ex:266: Lenra.Apps.trigger_pipeline/3
        (lenra 0.1.0) lib/lenra/apps.ex:240: Lenra.Apps.create_build_and_deploy/3
        (lenra_web 0.1.0) lib/lenra_web/controllers/build_controller.ex:23: LenraWeb.BuildsController.create/2
        (lenra_web 0.1.0) lib/lenra_web/controllers/build_controller.ex:1: LenraWeb.BuildsController.action/2
        (lenra_web 0.1.0) lib/lenra_web/controllers/build_controller.ex:1: LenraWeb.BuildsController.phoenix_controller_pipeline/2
        (phoenix 1.6.16) lib/phoenix/router.ex:354: Phoenix.Router.__call__/2
        (lenra_web 0.1.0) lib/lenra_web/endpoint.ex:1: LenraWeb.Endpoint.plug_builder_call/2

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shiipou I'm tagging you because I can't set this comment as blocking for the PR as I am the creator.

:ok

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

if retry < 1 do
create_pipeline(
service_name,
Expand Down Expand Up @@ -203,6 +179,11 @@ defmodule Lenra.Kubernetes.ApiServices do
{:ok, Jason.decode!(body)}
end

defp response({:ok, %Finch.Response{status: status_code, body: body}}, :secret)
when status_code in [404] do
{:error, :secret_not_found, Jason.decode!(body)}
end

defp response({:ok, %Finch.Response{status: status_code, body: body}}, :build)
when status_code in [200, 201, 202] do
%{"metadata" => %{"name" => name}} = Jason.decode!(body)
Expand All @@ -213,20 +194,225 @@ defmodule Lenra.Kubernetes.ApiServices do
raise "Kubernetes API could not be reached. It should not happen. #{reason}"
end

defp response(
{:ok,
%Finch.Response{
status: status_code
}},
_atom
)
defp response({:ok, %Finch.Response{status: status_code}}, _atom)
when status_code in [409] do
:secret_exist
{:error, :secret_exist}
end

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

{:error, :kubernetes_error}
end

defp get_k8s_secret(secret_name, namespace) do
kubernetes_api_url = Application.fetch_env!(:lenra, :kubernetes_api_url)
kubernetes_api_token = Application.fetch_env!(:lenra, :kubernetes_api_token)

secrets_url = "#{kubernetes_api_url}/api/v1/namespaces/#{namespace}/secrets/#{secret_name}"

headers = [
{"Authorization", "Bearer #{kubernetes_api_token}"},
{"content-type", "application/json"}
]

secret_response =
Finch.build(:get, secrets_url, headers)
|> Finch.request(PipelineHttp)
|> response(:secret)

case secret_response do
{:ok, body} ->
%{"data" => secret_data} = body
{:ok, Enum.into(Enum.map(secret_data, fn {key, value} -> {key, Base.decode64!(value)} end), %{})}

{:error, error, _reason} ->
{:error, error}
end
end

defp create_k8s_secret(secret_name, namespace, data) do
kubernetes_api_url = Application.fetch_env!(:lenra, :kubernetes_api_url)
kubernetes_api_token = Application.fetch_env!(:lenra, :kubernetes_api_token)

secrets_url = "#{kubernetes_api_url}/api/v1/namespaces/#{namespace}/secrets"

headers = [
{"Authorization", "Bearer #{kubernetes_api_token}"},
{"content-type", "application/json"}
]

secret_body =
Jason.encode!(%{
apiVersion: "v1",
kind: "Secret",
type: "Opaque",
metadata: %{
name: secret_name,
namespace: namespace
},
data: Enum.into(Enum.map(data, fn {key, value} -> {key, Base.encode64(value)} end), %{})
})

secret_response =
Finch.build(:post, secrets_url, headers, secret_body)
|> Finch.request(PipelineHttp)
|> response(:secret)

case secret_response do
{:ok, _} -> {:ok, data}
{:error, error} -> {:error, error}
end
end

defp update_k8s_secret(secret_name, namespace, secrets) do
kubernetes_api_url = Application.fetch_env!(:lenra, :kubernetes_api_url)
kubernetes_api_token = Application.fetch_env!(:lenra, :kubernetes_api_token)

secrets_url = "#{kubernetes_api_url}/api/v1/namespaces/#{namespace}/secrets/#{secret_name}"

headers = [
{"Authorization", "Bearer #{kubernetes_api_token}"},
{"content-type", "application/json"}
]

secret_body =
Jason.encode!(%{
apiVersion: "v1",
kind: "Secret",
metadata: %{
name: secret_name
},
data: Enum.into(Enum.map(secrets, fn {key, value} -> {key, Base.encode64(value)} end), %{})
})

secret_response =
Finch.build(:put, secrets_url, headers, secret_body)
|> Finch.request(PipelineHttp)
|> response(:secret)

case secret_response do
{:ok, _secret} -> {:ok}
_other -> {:secret_not_found}
end
end

defp delete_k8s_secret(secret_name, namespace) do
kubernetes_api_url = Application.fetch_env!(:lenra, :kubernetes_api_url)
kubernetes_api_token = Application.fetch_env!(:lenra, :kubernetes_api_token)

secrets_url = "#{kubernetes_api_url}/api/v1/namespaces/#{namespace}/secrets/#{secret_name}"

headers = [
{"Authorization", "Bearer #{kubernetes_api_token}"},
{"content-type", "application/json"}
]

secret_response =
Finch.build(:delete, secrets_url, headers)
|> Finch.request(PipelineHttp)
|> response(:secret)

case secret_response do
{:ok, _secret} -> {:ok}
_error -> {:secret_not_found}
end
end

def get_environment_secrets(service_name, env_id) do
secret_name = "#{service_name}-secret-#{env_id}"
kubernetes_apps_namespace = Application.fetch_env!(:lenra, :kubernetes_apps_namespace)

case get_k8s_secret(secret_name, kubernetes_apps_namespace) do
{:ok, secrets} -> {:ok, Enum.map(secrets, fn {key, _value} -> key end)}
{:error, :secret_not_found} -> {:error, :secret_not_found}
{:error, error} -> {:error, error}
end
end

def create_environment_secrets(service_name, env_id, secrets) do
secret_name = "#{service_name}-secret-#{env_id}"
kubernetes_apps_namespace = Application.fetch_env!(:lenra, :kubernetes_apps_namespace)

case create_k8s_secret(secret_name, kubernetes_apps_namespace, secrets) do
{:ok, secrets} ->
{:ok, partial_env} = Apps.fetch_env(env_id)
env = partial_env |> Repo.preload(deployment: [:build]) |> Repo.preload([:application])
build_number = env.deployment.build.build_number

Lenra.OpenfaasServices.update_secrets(
service_name,
build_number,
[secret_name]
)

{:ok, Enum.map(secrets, fn {key, _value} -> key end)}

{:error, :secret_exist} ->
{:error, :secret_exist}

_other ->
{:error, :unexpected_response}
end
end

def update_environment_secrets(service_name, env_id, secrets) do
secret_name = "#{service_name}-secret-#{env_id}"
kubernetes_apps_namespace = Application.fetch_env!(:lenra, :kubernetes_apps_namespace)

case get_k8s_secret(secret_name, kubernetes_apps_namespace) do
{:ok, current_secrets} ->
case update_k8s_secret(secret_name, kubernetes_apps_namespace, Map.merge(current_secrets, secrets)) do
{:ok} -> {:ok, Enum.map(secrets, fn {key, _value} -> key end)}
{:secret_not_found} -> {:error, :secret_not_found}
_other -> {:error, :unexpected_response}
end

error ->
error
end
end

def delete_environment_secrets(service_name, env_id, key) do
secret_name = "#{service_name}-secret-#{env_id}"
kubernetes_apps_namespace = Application.fetch_env!(:lenra, :kubernetes_apps_namespace)

case get_k8s_secret(secret_name, kubernetes_apps_namespace) do
{:ok, current_secrets} ->
case length(Map.keys(current_secrets)) do
len when len <= 1 ->
{:ok, partial_env} = Apps.fetch_env(env_id)

case partial_env
|> Repo.preload(deployment: [:build])
|> Repo.preload([:application]) do
%{application: app, deployment: %{build: build}} when not is_nil(build) ->
Lenra.OpenfaasServices.update_secrets(service_name, build.build_number, [])
# TODO: Return all other secrets
{:ok, []}

_other ->
{:error, :build_not_exist}
end

case delete_k8s_secret(secret_name, kubernetes_apps_namespace) do
{:ok} -> {:ok, []}
{:ok, _secret} -> {:ok, []}
{:secret_not_found} -> {:error, :secret_not_found}
# _other -> {:error, :unexpected_response}
end

_other ->
secrets = Map.drop(current_secrets, [key])
case update_k8s_secret(secret_name, kubernetes_apps_namespace, secrets) do
{:ok} -> {:ok, Enum.map(secrets, fn ({key, _value}) -> key end)}
{:secret_not_found} -> {:error, :secret_not_found}
# _other -> {:error, :unexpected_response}
end
end

error ->
error
end
end
end
Loading
Loading