diff --git a/.vscode/postman/lenra_server.postman_collection.json b/.vscode/postman/lenra_server.postman_collection.json new file mode 100644 index 00000000..b38eff50 --- /dev/null +++ b/.vscode/postman/lenra_server.postman_collection.json @@ -0,0 +1,1161 @@ +{ + "info": { + "_postman_id": "0b9bcb53-2d64-45b9-89af-66f48aff3c1c", + "name": "Lenra Server", + "description": "This document will show you how to authentificate (Oauth2) and interact with the Lenra server API using Postman's VSCode extension.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "6750074" + }, + "item": [ + { + "name": "Authentification", + "item": [ + { + "name": "login_with_authorization_code", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Has access_token\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData.access_token, \"Request didn't reply an access token\").to.exist\r", + " pm.collectionVariables.set(\"lenra_access_token\", jsonData.access_token);\r", + "});\r", + "\r", + " \r", + "\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "name": "cache-control", + "sortOrder": 2, + "infoTitle": "We recommend using this header", + "info": "Postman added \"Cache-Control: no-cache\" as a precautionary measure to prevent the server from returning stale response when one makes repeated requests.\n\nYou can remove this header in the app settings or enter a new one with a different value.", + "allowedToToggle": false, + "disableEdit": true, + "previewSettingsLink": "Go to settings", + "key": "Cache-Control", + "value": "no-cache", + "system": true, + "type": "text" + }, + { + "name": "postman-token", + "sortOrder": 3, + "infoTitle": "We recommend using this header", + "info": "The Postman-Token header appends a random UUID to every outgoing request. Postman adds this header for API developers to better debug requests sent and to ensure separate requests appear distinct to the receiving server.\n\nYou can remove this header in the app settings.", + "allowedToToggle": false, + "disableEdit": true, + "previewSettingsLink": "Go to settings", + "key": "Postman-Token", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "content-type", + "sortOrder": 4, + "infoTitle": "This header was automatically added", + "info": "The Content-Type header is added to help the server identify the media type of the request body that is present in this request.\n\nUse the request body tab to control the value or to remove this header.", + "allowedToToggle": true, + "disableEdit": true, + "previewSettingsLink": "Go to body", + "key": "Content-Type", + "value": "application/x-www-form-urlencoded", + "system": true, + "type": "text" + }, + { + "name": "content-length", + "sortOrder": 5, + "infoTitle": "This header was automatically added", + "info": "The Content-Length header was added to indicate to the server the size of the request body that is added to this request. Server uses this value to parse the request body accurately.\n\nYou can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "previewSettingsLink": "Go to body", + "key": "Content-Length", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "host", + "sortOrder": 6, + "infoTitle": "We recommend using this header", + "info": "The Host header is added to identify the domain name for which a request is being sent to the server. This header is implicitly sent by every HTTP client.\n\nYou can remove the header or enter a new one with a different value. It is most likely that without this header, your request will return an HTTP 400 error.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Host", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "user-agent", + "sortOrder": 7, + "infoTitle": "We recommend using this header", + "info": "The User-Agent header is added to help the server identify Postman as the HTTP requesting application or client.\n\nIt is recommended that this header be sent, but you can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "key": "User-Agent", + "value": "PostmanRuntime/7.32.1", + "system": true, + "type": "text" + }, + { + "name": "accept", + "sortOrder": 8, + "infoTitle": "We recommend using this header", + "info": "The \"Accept: */*\" header is added to tell the server that Postman can understand and process all forms of response content types.\n\nIt is recommended that this header be sent, but you can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Accept", + "value": "*/*", + "system": true, + "type": "text" + }, + { + "name": "accept-encoding", + "sortOrder": 9, + "infoTitle": "We recommend using this header", + "info": "The Accept-Encoding header is added to indicate to the server that Postman HTTP client supports a defined list of content-encoding or compression algorithms as response.\n\nYou can remove the header or enter a new one with a different value. Doing that may not accurately render the response within the app.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Accept-Encoding", + "value": "gzip, deflate, br", + "system": true, + "type": "text" + }, + { + "name": "connection", + "sortOrder": 10, + "infoTitle": "We recommend using this header", + "info": "Postman added the Connection header to indicate the server to keep the underlying network connection open once the current response is received. This allows Postman to reuse the same connection for faster response times in subsequent requests to the same server.\n\nYou can remove this header or enter a new one with a different value, such as `Connection: Close` to control this behaviour.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Connection", + "value": "keep-alive", + "system": true, + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/x-www-form-urlencoded", + "type": "text", + "id": 0 + } + ], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "grant_type", + "value": "authorization_code", + "type": "text" + }, + { + "key": "code", + "value": "{{hydra_auth_code}}", + "type": "text" + }, + { + "key": "client_id", + "value": "{{lenra_client_id}}", + "type": "text" + }, + { + "key": "redirect_uri", + "value": "{{hydra_redirect_url}}", + "type": "text" + }, + { + "key": "state", + "value": "p1z8pm12ov", + "type": "text" + } + ] + }, + "url": { + "raw": "{{hydra_endpoint}}/oauth2/token", + "host": [ + "{{hydra_endpoint}}" + ], + "path": [ + "oauth2", + "token" + ] + }, + "description": "# Authentification\n\nThe" + }, + "response": [] + } + ], + "description": "To get your authentification token, you'll need to get you're authentification code from the OAuth process.\n\nTo get it, you'll need to register a new Oauth client for the [oauthdebugger](https://oauthdebugger.com/), go to your terminal in the Lenra Server project directory and run the following command :\n\n``` bash\n# Run the local dependencies to have hydra and postgres running\n~ $ docker compose up -d\n ✔ Network server_default Created 0.1s \n ✔ Container lenra-mongo Started 0.1s \n ✔ Container server-hydra-migrate-1 Started 0.1s \n ✔ Container lenra-postgres Started 0.1s \n ✔ Container server-hydra-1 Started 0.0s \n# Add a new Oauth2 Client for oauthdebugger.com\n~ $ mix create_oauth2_client backoffice --redirect-uri https://oauthdebugger.com/debug\n09:42:09.243 [debug] Create hydra client %{allowed_cors_origins: [\"http://localhost:10000\"], client_name: \"Lenra Backoffice\", redirect_uris: [\"https://oauthdebugger.com/debug\"], scope: \"profile store manage:account manage:apps\", skip_consent: false, token_endpoint_auth_method: \"none\"}\nOauth client backoffice :\n%{\n \"allowed_cors_origins\" => [\"http://localhost:10000\"],\n \"client_id\" => \"5fbecdf6-ad85-4aea-870e-26a45fe90791\",\n \"client_name\" => \"Lenra Backoffice\",\n \"metadata\" => %{},\n \"redirect_uris\" => [\"https://oauthdebugger.com/debug\"],\n \"scope\" => \"profile store manage:account manage:apps\"\n}\n\n ```\n\nJust copy the \\`client_id\\` and go to [oauthdebugger](https://oauthdebugger.com/), fill the form with following informations :\n\n- Authorize URI: [http://localhost:4444/oauth2/auth](http://localhost:4444/oauth2/auth)\n- Redirect URI: [https://oauthdebugger.com/debug](https://oauthdebugger.com/debug)\n- Client ID: 5fbecdf6-ad85-4aea-870e-26a45fe907917\n- Scope: profile store manage:account manage:apps\n- Response type: code\n- Response mode: form_post\n \n\nLet everything else in the default value.\n\nStart the server with the following command :\n\n``` bash\n~ $ mix phx.server\n\n ```\n\nAnd then you can change the LenraClientAuthCode in the Globals Environment from the Postman's extension tabs." + }, + { + "name": "Apps", + "item": [ + { + "name": "current_user_list_app", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Has some apps\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.be.an(\"array\")\r", + " const last_app = jsonData.last()\r", + " pm.expect(last_app).not.to.be.an(\"undefined\");\r", + " pm.collectionVariables.set(\"lenra_latest_created_app\", last_app.id);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "name": "cache-control", + "sortOrder": 2, + "infoTitle": "We recommend using this header", + "info": "Postman added \"Cache-Control: no-cache\" as a precautionary measure to prevent the server from returning stale response when one makes repeated requests.\n\nYou can remove this header in the app settings or enter a new one with a different value.", + "allowedToToggle": false, + "disableEdit": true, + "previewSettingsLink": "Go to settings", + "key": "Cache-Control", + "value": "no-cache", + "system": true, + "type": "text" + }, + { + "name": "postman-token", + "sortOrder": 3, + "infoTitle": "We recommend using this header", + "info": "The Postman-Token header appends a random UUID to every outgoing request. Postman adds this header for API developers to better debug requests sent and to ensure separate requests appear distinct to the receiving server.\n\nYou can remove this header in the app settings.", + "allowedToToggle": false, + "disableEdit": true, + "previewSettingsLink": "Go to settings", + "key": "Postman-Token", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "host", + "sortOrder": 6, + "infoTitle": "We recommend using this header", + "info": "The Host header is added to identify the domain name for which a request is being sent to the server. This header is implicitly sent by every HTTP client.\n\nYou can remove the header or enter a new one with a different value. It is most likely that without this header, your request will return an HTTP 400 error.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Host", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "user-agent", + "sortOrder": 7, + "infoTitle": "We recommend using this header", + "info": "The User-Agent header is added to help the server identify Postman as the HTTP requesting application or client.\n\nIt is recommended that this header be sent, but you can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "key": "User-Agent", + "value": "PostmanRuntime/7.32.1", + "system": true, + "type": "text" + }, + { + "name": "accept", + "sortOrder": 8, + "infoTitle": "We recommend using this header", + "info": "The \"Accept: */*\" header is added to tell the server that Postman can understand and process all forms of response content types.\n\nIt is recommended that this header be sent, but you can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Accept", + "value": "*/*", + "system": true, + "type": "text" + }, + { + "name": "accept-encoding", + "sortOrder": 9, + "infoTitle": "We recommend using this header", + "info": "The Accept-Encoding header is added to indicate to the server that Postman HTTP client supports a defined list of content-encoding or compression algorithms as response.\n\nYou can remove the header or enter a new one with a different value. Doing that may not accurately render the response within the app.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Accept-Encoding", + "value": "gzip, deflate, br", + "system": true, + "type": "text" + }, + { + "name": "connection", + "sortOrder": 10, + "infoTitle": "We recommend using this header", + "info": "Postman added the Connection header to indicate the server to keep the underlying network connection open once the current response is received. This allows Postman to reuse the same connection for faster response times in subsequent requests to the same server.\n\nYou can remove this header or enter a new one with a different value, such as `Connection: Close` to control this behaviour.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Connection", + "value": "keep-alive", + "system": true, + "type": "text" + }, + { + "key": "Authorization", + "value": "Bearer {{lenra_access_token}}", + "type": "text", + "id": 0 + } + ], + "url": { + "raw": "{{lenra_endpoint}}/api/me/apps", + "host": [ + "{{lenra_endpoint}}" + ], + "path": [ + "api", + "me", + "apps" + ] + } + }, + "response": [] + }, + { + "name": "list_environments", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Has some envs\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.be.an(\"array\")\r", + " const last_env = jsonData.last()\r", + " pm.expect(last_env).not.to.be.an(\"undefined\");\r", + " pm.collectionVariables.set(\"lenra_latest_created_env\", last_env.id);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "name": "cache-control", + "sortOrder": 2, + "infoTitle": "We recommend using this header", + "info": "Postman added \"Cache-Control: no-cache\" as a precautionary measure to prevent the server from returning stale response when one makes repeated requests.\n\nYou can remove this header in the app settings or enter a new one with a different value.", + "allowedToToggle": false, + "disableEdit": true, + "previewSettingsLink": "Go to settings", + "key": "Cache-Control", + "value": "no-cache", + "system": true, + "type": "text" + }, + { + "name": "postman-token", + "sortOrder": 3, + "infoTitle": "We recommend using this header", + "info": "The Postman-Token header appends a random UUID to every outgoing request. Postman adds this header for API developers to better debug requests sent and to ensure separate requests appear distinct to the receiving server.\n\nYou can remove this header in the app settings.", + "allowedToToggle": false, + "disableEdit": true, + "previewSettingsLink": "Go to settings", + "key": "Postman-Token", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "host", + "sortOrder": 6, + "infoTitle": "We recommend using this header", + "info": "The Host header is added to identify the domain name for which a request is being sent to the server. This header is implicitly sent by every HTTP client.\n\nYou can remove the header or enter a new one with a different value. It is most likely that without this header, your request will return an HTTP 400 error.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Host", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "user-agent", + "sortOrder": 7, + "infoTitle": "We recommend using this header", + "info": "The User-Agent header is added to help the server identify Postman as the HTTP requesting application or client.\n\nIt is recommended that this header be sent, but you can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "key": "User-Agent", + "value": "PostmanRuntime/7.32.1", + "system": true, + "type": "text" + }, + { + "name": "accept", + "sortOrder": 8, + "infoTitle": "We recommend using this header", + "info": "The \"Accept: */*\" header is added to tell the server that Postman can understand and process all forms of response content types.\n\nIt is recommended that this header be sent, but you can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Accept", + "value": "*/*", + "system": true, + "type": "text" + }, + { + "name": "accept-encoding", + "sortOrder": 9, + "infoTitle": "We recommend using this header", + "info": "The Accept-Encoding header is added to indicate to the server that Postman HTTP client supports a defined list of content-encoding or compression algorithms as response.\n\nYou can remove the header or enter a new one with a different value. Doing that may not accurately render the response within the app.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Accept-Encoding", + "value": "gzip, deflate, br", + "system": true, + "type": "text" + }, + { + "name": "connection", + "sortOrder": 10, + "infoTitle": "We recommend using this header", + "info": "Postman added the Connection header to indicate the server to keep the underlying network connection open once the current response is received. This allows Postman to reuse the same connection for faster response times in subsequent requests to the same server.\n\nYou can remove this header or enter a new one with a different value, such as `Connection: Close` to control this behaviour.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Connection", + "value": "keep-alive", + "system": true, + "type": "text" + }, + { + "key": "Authorization", + "value": "Bearer {{lenra_access_token}}", + "type": "text", + "id": 0 + } + ], + "url": { + "raw": "{{lenra_endpoint}}/api/apps/:app_id/environments", + "host": [ + "{{lenra_endpoint}}" + ], + "path": [ + "api", + "apps", + ":app_id", + "environments" + ], + "variable": [ + { + "id": 0, + "key": "app_id", + "value": "{{lenra_latest_created_app}}", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "list_environments_secrets", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Has some secrets\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.be.an(\"array\")\r", + " const last_secret = jsonData.last()\r", + " pm.expect(last_secret).not.to.be.an(\"undefined\");\r", + " pm.collectionVariables.set(\"lenra_latest_created_secret\", last_secret.id);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "name": "cache-control", + "sortOrder": 2, + "infoTitle": "We recommend using this header", + "info": "Postman added \"Cache-Control: no-cache\" as a precautionary measure to prevent the server from returning stale response when one makes repeated requests.\n\nYou can remove this header in the app settings or enter a new one with a different value.", + "allowedToToggle": false, + "disableEdit": true, + "previewSettingsLink": "Go to settings", + "key": "Cache-Control", + "value": "no-cache", + "system": true, + "type": "text" + }, + { + "name": "postman-token", + "sortOrder": 3, + "infoTitle": "We recommend using this header", + "info": "The Postman-Token header appends a random UUID to every outgoing request. Postman adds this header for API developers to better debug requests sent and to ensure separate requests appear distinct to the receiving server.\n\nYou can remove this header in the app settings.", + "allowedToToggle": false, + "disableEdit": true, + "previewSettingsLink": "Go to settings", + "key": "Postman-Token", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "host", + "sortOrder": 6, + "infoTitle": "We recommend using this header", + "info": "The Host header is added to identify the domain name for which a request is being sent to the server. This header is implicitly sent by every HTTP client.\n\nYou can remove the header or enter a new one with a different value. It is most likely that without this header, your request will return an HTTP 400 error.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Host", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "user-agent", + "sortOrder": 7, + "infoTitle": "We recommend using this header", + "info": "The User-Agent header is added to help the server identify Postman as the HTTP requesting application or client.\n\nIt is recommended that this header be sent, but you can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "key": "User-Agent", + "value": "PostmanRuntime/7.32.1", + "system": true, + "type": "text" + }, + { + "name": "accept", + "sortOrder": 8, + "infoTitle": "We recommend using this header", + "info": "The \"Accept: */*\" header is added to tell the server that Postman can understand and process all forms of response content types.\n\nIt is recommended that this header be sent, but you can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Accept", + "value": "*/*", + "system": true, + "type": "text" + }, + { + "name": "accept-encoding", + "sortOrder": 9, + "infoTitle": "We recommend using this header", + "info": "The Accept-Encoding header is added to indicate to the server that Postman HTTP client supports a defined list of content-encoding or compression algorithms as response.\n\nYou can remove the header or enter a new one with a different value. Doing that may not accurately render the response within the app.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Accept-Encoding", + "value": "gzip, deflate, br", + "system": true, + "type": "text" + }, + { + "name": "connection", + "sortOrder": 10, + "infoTitle": "We recommend using this header", + "info": "Postman added the Connection header to indicate the server to keep the underlying network connection open once the current response is received. This allows Postman to reuse the same connection for faster response times in subsequent requests to the same server.\n\nYou can remove this header or enter a new one with a different value, such as `Connection: Close` to control this behaviour.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Connection", + "value": "keep-alive", + "system": true, + "type": "text" + }, + { + "key": "Authorization", + "value": "Bearer {{lenra_access_token}}", + "type": "text", + "id": 0 + } + ], + "url": { + "raw": "{{lenra_endpoint}}/api/apps/:app_id/environments/:env_id/secrets", + "host": [ + "{{lenra_endpoint}}" + ], + "path": [ + "api", + "apps", + ":app_id", + "environments", + ":env_id", + "secrets" + ], + "variable": [ + { + "id": 0, + "key": "app_id", + "value": "{{lenra_latest_created_app}}", + "type": "string" + }, + { + "key": "env_id", + "value": "{{lenra_latest_created_env}}", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "add_environments_secrets", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Secret is created\", function () {\r", + " var jsonData = pm.response.json();\r", + " const {id, key} = jsonData\r", + " pm.expect(id).to.be.a(\"number\")\r", + " pm.collectionVariables.set(\"lenra_latest_created_secret\", id);\r", + " pm.collectionVariables.set(\"lenra_latest_created_secret_key\", key);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "name": "cache-control", + "sortOrder": 2, + "infoTitle": "We recommend using this header", + "info": "Postman added \"Cache-Control: no-cache\" as a precautionary measure to prevent the server from returning stale response when one makes repeated requests.\n\nYou can remove this header in the app settings or enter a new one with a different value.", + "allowedToToggle": false, + "disableEdit": true, + "previewSettingsLink": "Go to settings", + "key": "Cache-Control", + "value": "no-cache", + "system": true, + "type": "text" + }, + { + "name": "postman-token", + "sortOrder": 3, + "infoTitle": "We recommend using this header", + "info": "The Postman-Token header appends a random UUID to every outgoing request. Postman adds this header for API developers to better debug requests sent and to ensure separate requests appear distinct to the receiving server.\n\nYou can remove this header in the app settings.", + "allowedToToggle": false, + "disableEdit": true, + "previewSettingsLink": "Go to settings", + "key": "Postman-Token", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "host", + "sortOrder": 6, + "infoTitle": "We recommend using this header", + "info": "The Host header is added to identify the domain name for which a request is being sent to the server. This header is implicitly sent by every HTTP client.\n\nYou can remove the header or enter a new one with a different value. It is most likely that without this header, your request will return an HTTP 400 error.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Host", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "user-agent", + "sortOrder": 7, + "infoTitle": "We recommend using this header", + "info": "The User-Agent header is added to help the server identify Postman as the HTTP requesting application or client.\n\nIt is recommended that this header be sent, but you can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "key": "User-Agent", + "value": "PostmanRuntime/7.32.1", + "system": true, + "type": "text" + }, + { + "name": "accept", + "sortOrder": 8, + "infoTitle": "We recommend using this header", + "info": "The \"Accept: */*\" header is added to tell the server that Postman can understand and process all forms of response content types.\n\nIt is recommended that this header be sent, but you can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Accept", + "value": "*/*", + "system": true, + "type": "text" + }, + { + "name": "accept-encoding", + "sortOrder": 9, + "infoTitle": "We recommend using this header", + "info": "The Accept-Encoding header is added to indicate to the server that Postman HTTP client supports a defined list of content-encoding or compression algorithms as response.\n\nYou can remove the header or enter a new one with a different value. Doing that may not accurately render the response within the app.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Accept-Encoding", + "value": "gzip, deflate, br", + "system": true, + "type": "text" + }, + { + "name": "connection", + "sortOrder": 10, + "infoTitle": "We recommend using this header", + "info": "Postman added the Connection header to indicate the server to keep the underlying network connection open once the current response is received. This allows Postman to reuse the same connection for faster response times in subsequent requests to the same server.\n\nYou can remove this header or enter a new one with a different value, such as `Connection: Close` to control this behaviour.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Connection", + "value": "keep-alive", + "system": true, + "type": "text" + }, + { + "key": "Authorization", + "value": "Bearer {{lenra_access_token}}", + "type": "text", + "id": 0 + } + ], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "key", + "value": "test", + "type": "text" + }, + { + "key": "value", + "value": "test", + "type": "text" + } + ] + }, + "url": { + "raw": "{{lenra_endpoint}}/api/apps/:app_id/environments/:env_id/secrets", + "host": [ + "{{lenra_endpoint}}" + ], + "path": [ + "api", + "apps", + ":app_id", + "environments", + ":env_id", + "secrets" + ], + "variable": [ + { + "id": 0, + "key": "app_id", + "value": "{{lenra_latest_created_app}}", + "type": "string" + }, + { + "key": "env_id", + "value": "{{lenra_latest_created_env}}", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "update_environments_secrets", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Secret is created\", function () {\r", + " var jsonData = pm.response.json();\r", + " const id = jsonData.id\r", + " pm.expect(id).to.be.a(\"number\")\r", + " pm.collectionVariables.set(\"lenra_latest_created_secret\", id);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "name": "cache-control", + "sortOrder": 2, + "infoTitle": "We recommend using this header", + "info": "Postman added \"Cache-Control: no-cache\" as a precautionary measure to prevent the server from returning stale response when one makes repeated requests.\n\nYou can remove this header in the app settings or enter a new one with a different value.", + "allowedToToggle": false, + "disableEdit": true, + "previewSettingsLink": "Go to settings", + "key": "Cache-Control", + "value": "no-cache", + "system": true, + "type": "text" + }, + { + "name": "postman-token", + "sortOrder": 3, + "infoTitle": "We recommend using this header", + "info": "The Postman-Token header appends a random UUID to every outgoing request. Postman adds this header for API developers to better debug requests sent and to ensure separate requests appear distinct to the receiving server.\n\nYou can remove this header in the app settings.", + "allowedToToggle": false, + "disableEdit": true, + "previewSettingsLink": "Go to settings", + "key": "Postman-Token", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "host", + "sortOrder": 6, + "infoTitle": "We recommend using this header", + "info": "The Host header is added to identify the domain name for which a request is being sent to the server. This header is implicitly sent by every HTTP client.\n\nYou can remove the header or enter a new one with a different value. It is most likely that without this header, your request will return an HTTP 400 error.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Host", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "user-agent", + "sortOrder": 7, + "infoTitle": "We recommend using this header", + "info": "The User-Agent header is added to help the server identify Postman as the HTTP requesting application or client.\n\nIt is recommended that this header be sent, but you can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "key": "User-Agent", + "value": "PostmanRuntime/7.32.1", + "system": true, + "type": "text" + }, + { + "name": "accept", + "sortOrder": 8, + "infoTitle": "We recommend using this header", + "info": "The \"Accept: */*\" header is added to tell the server that Postman can understand and process all forms of response content types.\n\nIt is recommended that this header be sent, but you can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Accept", + "value": "*/*", + "system": true, + "type": "text" + }, + { + "name": "accept-encoding", + "sortOrder": 9, + "infoTitle": "We recommend using this header", + "info": "The Accept-Encoding header is added to indicate to the server that Postman HTTP client supports a defined list of content-encoding or compression algorithms as response.\n\nYou can remove the header or enter a new one with a different value. Doing that may not accurately render the response within the app.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Accept-Encoding", + "value": "gzip, deflate, br", + "system": true, + "type": "text" + }, + { + "name": "connection", + "sortOrder": 10, + "infoTitle": "We recommend using this header", + "info": "Postman added the Connection header to indicate the server to keep the underlying network connection open once the current response is received. This allows Postman to reuse the same connection for faster response times in subsequent requests to the same server.\n\nYou can remove this header or enter a new one with a different value, such as `Connection: Close` to control this behaviour.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Connection", + "value": "keep-alive", + "system": true, + "type": "text" + }, + { + "key": "Authorization", + "value": "Bearer {{lenra_access_token}}", + "type": "text", + "id": 0 + } + ], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "key", + "value": "test", + "type": "text" + }, + { + "key": "value", + "value": "test", + "type": "text" + } + ] + }, + "url": { + "raw": "{{lenra_endpoint}}/api/apps/:app_id/environments/:env_id/secrets/:key", + "host": [ + "{{lenra_endpoint}}" + ], + "path": [ + "api", + "apps", + ":app_id", + "environments", + ":env_id", + "secrets", + ":key" + ], + "variable": [ + { + "id": 0, + "key": "app_id", + "value": "{{lenra_latest_created_app}}", + "type": "string" + }, + { + "key": "env_id", + "value": "{{lenra_latest_created_env}}", + "type": "string" + }, + { + "key": "key", + "value": "{{lenra_latest_created_secret_key}}", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "current_dev_list_app", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Has some apps\", function () {\r", + " var jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.be.an(\"array\")\r", + " const last_app = jsonData.last()\r", + " pm.expect(last_app).not.to.be.an(\"undefined\");\r", + " pm.collectionVariables.set(\"lenra_latest_created_app\", last_app.id);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "name": "cache-control", + "sortOrder": 2, + "infoTitle": "We recommend using this header", + "info": "Postman added \"Cache-Control: no-cache\" as a precautionary measure to prevent the server from returning stale response when one makes repeated requests.\n\nYou can remove this header in the app settings or enter a new one with a different value.", + "allowedToToggle": false, + "disableEdit": true, + "previewSettingsLink": "Go to settings", + "key": "Cache-Control", + "value": "no-cache", + "system": true, + "type": "text" + }, + { + "name": "postman-token", + "sortOrder": 3, + "infoTitle": "We recommend using this header", + "info": "The Postman-Token header appends a random UUID to every outgoing request. Postman adds this header for API developers to better debug requests sent and to ensure separate requests appear distinct to the receiving server.\n\nYou can remove this header in the app settings.", + "allowedToToggle": false, + "disableEdit": true, + "previewSettingsLink": "Go to settings", + "key": "Postman-Token", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "host", + "sortOrder": 6, + "infoTitle": "We recommend using this header", + "info": "The Host header is added to identify the domain name for which a request is being sent to the server. This header is implicitly sent by every HTTP client.\n\nYou can remove the header or enter a new one with a different value. It is most likely that without this header, your request will return an HTTP 400 error.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Host", + "value": "", + "system": true, + "type": "text" + }, + { + "name": "user-agent", + "sortOrder": 7, + "infoTitle": "We recommend using this header", + "info": "The User-Agent header is added to help the server identify Postman as the HTTP requesting application or client.\n\nIt is recommended that this header be sent, but you can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "key": "User-Agent", + "value": "PostmanRuntime/7.32.1", + "system": true, + "type": "text" + }, + { + "name": "accept", + "sortOrder": 8, + "infoTitle": "We recommend using this header", + "info": "The \"Accept: */*\" header is added to tell the server that Postman can understand and process all forms of response content types.\n\nIt is recommended that this header be sent, but you can remove the header or enter a new one with a different value.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Accept", + "value": "*/*", + "system": true, + "type": "text" + }, + { + "name": "accept-encoding", + "sortOrder": 9, + "infoTitle": "We recommend using this header", + "info": "The Accept-Encoding header is added to indicate to the server that Postman HTTP client supports a defined list of content-encoding or compression algorithms as response.\n\nYou can remove the header or enter a new one with a different value. Doing that may not accurately render the response within the app.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Accept-Encoding", + "value": "gzip, deflate, br", + "system": true, + "type": "text" + }, + { + "name": "connection", + "sortOrder": 10, + "infoTitle": "We recommend using this header", + "info": "Postman added the Connection header to indicate the server to keep the underlying network connection open once the current response is received. This allows Postman to reuse the same connection for faster response times in subsequent requests to the same server.\n\nYou can remove this header or enter a new one with a different value, such as `Connection: Close` to control this behaviour.", + "allowedToToggle": true, + "disableEdit": true, + "key": "Connection", + "value": "keep-alive", + "system": true, + "type": "text" + }, + { + "key": "Authorization", + "value": "Bearer {{lenra_access_token}}", + "type": "text", + "id": 0 + } + ], + "url": { + "raw": "{{lenra_endpoint}}/api/me/apps", + "host": [ + "{{lenra_endpoint}}" + ], + "path": [ + "api", + "me", + "apps" + ] + } + }, + "response": [] + } + ] + } + ], + "variable": [ + { + "key": "lenra_endpoint", + "value": "http://localhost:4000", + "type": "string" + }, + { + "key": "hydra_endpoint", + "value": "http://localhost:4444", + "type": "string" + }, + { + "key": "hydra_auth_code", + "value": "", + "type": "string" + }, + { + "key": "lenra_client_id", + "value": "", + "type": "string" + }, + { + "key": "hydra_redirect_url", + "value": "https://oauthdebugger.com/debug", + "type": "string" + }, + { + "key": "hydra_client_scope", + "value": "profile store manage:account manage:apps", + "type": "string" + }, + { + "key": "lenra_access_token", + "value": "", + "type": "string" + }, + { + "key": "lenra_latest_created_app", + "value": "", + "type": "default" + }, + { + "key": "lenra_latest_created_env", + "value": "", + "type": "default" + }, + { + "key": "lenra-intra", + "value": "", + "type": "string" + } + ] +} diff --git a/.vscode/postman/local.postman_environment.json b/.vscode/postman/local.postman_environment.json new file mode 100644 index 00000000..663bf03a --- /dev/null +++ b/.vscode/postman/local.postman_environment.json @@ -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" +} \ No newline at end of file diff --git a/.vscode/postman/staging.postman_environment.json b/.vscode/postman/staging.postman_environment.json new file mode 100644 index 00000000..d2909760 --- /dev/null +++ b/.vscode/postman/staging.postman_environment.json @@ -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" +} \ No newline at end of file diff --git a/apps/lenra/lib/lenra/errors/business_error.ex b/apps/lenra/lib/lenra/errors/business_error.ex index 7f487378..43438ea3 100644 --- a/apps/lenra/lib/lenra/errors/business_error.ex +++ b/apps/lenra/lib/lenra/errors/business_error.ex @@ -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 diff --git a/apps/lenra/lib/lenra/kubernetes/api_services.ex b/apps/lenra/lib/lenra/kubernetes/api_services.ex index cf223036..8651e556 100644 --- a/apps/lenra/lib/lenra/kubernetes/api_services.ex +++ b/apps/lenra/lib/lenra/kubernetes/api_services.ex @@ -4,6 +4,8 @@ defmodule Lenra.Kubernetes.ApiServices do Curently only support the request to create a new pipeline. """ + alias ApplicationRunner.Errors.TechnicalError + alias Ecto alias Lenra.Apps alias Lenra.Apps.Build alias Lenra.Apps.Deployment @@ -44,44 +46,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, _data} -> :ok - :secret_exist -> - Finch.build(:delete, secrets_url <> "/#{build_name}", headers) - |> Finch.request(PipelineHttp) - |> response(:secret) - + {:error, :secret_exist} -> if retry < 1 do create_pipeline( service_name, @@ -203,6 +180,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) @@ -213,15 +195,9 @@ 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 @@ -229,4 +205,215 @@ defmodule Lenra.Kubernetes.ApiServices do {: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) + + if kubernetes_api_url == nil || kubernetes_api_token == nil do + TechnicalError.error_500_tuple("Kubernetes configured improperly.") + else + 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 + 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, secret} + _other -> {:error, :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, _secret} + _error -> {: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) + + with {:ok, secrets} <- get_k8s_secret(secret_name, kubernetes_apps_namespace) do + {:ok, Enum.map(secrets, fn {key, _value} -> key end)} + 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) + + with {:ok, current_secrets} <- get_k8s_secret(secret_name, kubernetes_apps_namespace) do + case update_k8s_secret(secret_name, kubernetes_apps_namespace, Map.merge(current_secrets, secrets)) do + {:ok, _secret} -> {:ok, Enum.map(secrets, fn {key, _value} -> key end)} + {:error, :secret_not_found} -> {:error, :secret_not_found} + end + 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) + + with {:ok, current_secrets} <- get_k8s_secret(secret_name, kubernetes_apps_namespace) do + case length(Map.keys(current_secrets)) do + len when len <= 1 -> + handle_delete_when_one_or_less_secrets(env_id, secret_name, kubernetes_apps_namespace, service_name) + + _other -> + handle_delete_when_multiple_secrets(current_secrets, key, secret_name, kubernetes_apps_namespace) + end + end + end + + defp handle_delete_when_one_or_less_secrets(env_id, secret_name, kubernetes_apps_namespace, service_name) do + {:ok, partial_env} = Apps.fetch_env(env_id) + + case partial_env + |> Repo.preload(deployment: [:build]) + |> Repo.preload([:application]) do + %{application: _app, deployment: %{build: nil}} -> + {:error, :build_not_exist} + + %{application: _app, deployment: %{build: build}} -> + Lenra.OpenfaasServices.update_secrets(service_name, build.build_number, []) + # TODO: Return all other secrets + {:ok, []} + + _other -> + {:error, :build_not_exist} + end + + with {:ok, _secret} <- delete_k8s_secret(secret_name, kubernetes_apps_namespace) do + {:ok, []} + end + end + + defp handle_delete_when_multiple_secrets(current_secrets, key, secret_name, kubernetes_apps_namespace) do + secrets = Map.drop(current_secrets, [key]) + + with {:ok, _secret} <- update_k8s_secret(secret_name, kubernetes_apps_namespace, secrets) do + {:ok, Enum.map(secrets, fn {key, _value} -> key end)} + end + end end diff --git a/apps/lenra/lib/lenra/services/openfaas_services.ex b/apps/lenra/lib/lenra/services/openfaas_services.ex index 531b4541..8ac81e7c 100644 --- a/apps/lenra/lib/lenra/services/openfaas_services.ex +++ b/apps/lenra/lib/lenra/services/openfaas_services.ex @@ -6,6 +6,7 @@ defmodule Lenra.OpenfaasServices do alias ApplicationRunner.ApplicationServices alias Lenra.Apps alias Lenra.Errors.TechnicalError + alias Lenra.Subscriptions alias LenraCommon.Errors require Logger @@ -50,6 +51,46 @@ defmodule Lenra.OpenfaasServices do |> response(:deploy_status) end + def update_secrets(service_name, build_number, secrets \\ []) do + {base_url, headers} = get_http_context() + function_name = get_function_name(service_name, build_number) + url = "#{base_url}/system/function/#{function_name}" + + function = + case Finch.build( + :get, + url, + headers + ) + |> Finch.request(FaasHttp, receive_timeout: 1000) + |> response(:get_function) do + {:ok, result} -> result + _other -> nil + end + + if function != nil do + url = "#{base_url}/system/functions" + + body = + function + |> Map.merge(%{ + "secrets" => secrets + }) + |> Jason.encode!() + + Finch.build( + :put, + url, + headers, + body + ) + |> Finch.request(FaasHttp, receive_timeout: 1000) + |> response(:deploy_status) + else + TechnicalError.openfaas_not_reachable_tuple() + end + end + def delete_app_openfaas(service_name, build_number) do {base_url, headers} = get_http_context() @@ -79,6 +120,14 @@ defmodule Lenra.OpenfaasServices do available_replicas != 0 && available_replicas != nil end + defp response( + {:ok, %Finch.Response{status: status_code, body: body}}, + :get_function + ) + when status_code in [200, 202] do + Jason.decode!(body) + end + defp response({:ok, %Finch.Response{status: status_code}}, :delete_app) when status_code in [200, 202] do {:ok, status_code} diff --git a/apps/lenra_web/lib/lenra_web/controllers/environment_controller.ex b/apps/lenra_web/lib/lenra_web/controllers/environment_controller.ex index 2bed3b06..865983dc 100644 --- a/apps/lenra_web/lib/lenra_web/controllers/environment_controller.ex +++ b/apps/lenra_web/lib/lenra_web/controllers/environment_controller.ex @@ -6,7 +6,8 @@ defmodule LenraWeb.EnvsController do alias Lenra.Apps alias Lenra.Errors.BusinessError - alias alias Lenra.Subscriptions + alias Lenra.Kubernetes.ApiServices + alias Lenra.Subscriptions alias Lenra.Subscriptions.Subscription defp get_app_and_allow(conn, %{"app_id" => app_id_str}) do @@ -43,7 +44,7 @@ defmodule LenraWeb.EnvsController do end end - def update(conn, %{"env_id" => env_id, "is_public" => true} = params) do + def update(conn, %{"env_id" => _env_id, "is_public" => true} = params) do with {:ok, app, env} <- get_app_env_and_allow(conn, params), %Subscription{} = _subscription <- Subscriptions.get_subscription_by_app_id(app.id), {:ok, %{updated_env: env}} <- Apps.update_env(env, params) do @@ -55,13 +56,91 @@ defmodule LenraWeb.EnvsController do end end - def update(conn, %{"env_id" => env_id} = params) do + def update(conn, %{"env_id" => _env_id} = params) do with {:ok, _app, env} <- get_app_env_and_allow(conn, params), {:ok, %{updated_env: env}} <- Apps.update_env(env, params) do conn |> reply(env) end end + + def list_secrets(conn, %{"env_id" => env_id} = params) do + with {:ok, app} <- get_app_and_allow(conn, params), + {:ok, environment} <- Apps.fetch_env(env_id) do + case ApiServices.get_environment_secrets(app.service_name, environment.id) do + {:ok, secrets} -> conn |> reply(secrets) + {:error, :secret_not_found} -> conn |> reply([]) + # {:error, :kubernetes_error} -> BusinessError.kubernetes_unexpected_response_tuple() + {:error, error} -> BusinessError.api_return_unexpected_response_tuple(error) + end + end + end + + def create_secret(conn, %{"env_id" => env_id, "key" => key, "value" => value} = params) do + with {:ok, app} <- get_app_and_allow(conn, params), + {:ok, environment} <- Apps.fetch_env(env_id) do + case ApiServices.get_environment_secrets(app.service_name, environment.id) do + {:ok, secrets} -> + handle_secret_found(conn, secrets, app, environment, key, value) + + {:error, :secret_not_found} -> + handle_secret_not_found(conn, app, environment, key, value) + end + end + end + + defp handle_secret_found(conn, secrets, app, environment, key, value) do + case Enum.any?(secrets, fn s -> s == key end) do + false -> + case ApiServices.update_environment_secrets(app.service_name, environment.id, %{key => value}) do + {:ok, updated_secrets} -> + conn |> reply(updated_secrets) + + # Should never happen + {:error, :secret_not_found} -> + BusinessError.env_secret_not_found_tuple() + end + + true -> + BusinessError.env_secret_already_exist_tuple() + end + end + + defp handle_secret_not_found(conn, app, environment, key, value) do + case ApiServices.create_environment_secrets(app.service_name, environment.id, %{key => value}) do + {:ok, secrets} -> conn |> reply(secrets) + # This should never happen + {:error, :secret_exist} -> BusinessError.env_secret_already_exist_tuple() + # {:error, :kubernetes_error} -> BusinessError.kubernetes_unexpected_response_tuple() + {:error, :unexpected_response} -> BusinessError.api_return_unexpected_response_tuple() + end + end + + def update_secret(conn, %{"env_id" => env_id, "key" => key, "value" => value} = params) do + with {:ok, app} <- get_app_and_allow(conn, params), + {:ok, environment} <- Apps.fetch_env(env_id) do + case ApiServices.update_environment_secrets(app.service_name, environment.id, %{key => value}) do + {:ok, secrets} -> + conn |> reply(secrets) + + {:error, :secret_not_found} -> + BusinessError.env_secret_not_found_tuple() + end + end + end + + def delete_secret(conn, %{"env_id" => env_id, "key" => key} = params) do + with {:ok, app} <- get_app_and_allow(conn, params), + {:ok, environment} <- Apps.fetch_env(env_id) do + case ApiServices.delete_environment_secrets(app.service_name, environment.id, key) do + {:ok, secrets} -> + conn |> reply(secrets) + + {:error, :secret_not_found} -> + BusinessError.env_secret_not_found_tuple() + end + end + end end defmodule LenraWeb.EnvsController.Policy do @@ -82,6 +161,11 @@ defmodule LenraWeb.EnvsController.Policy do def authorize(:update, %App{id: app_id}, %Subscription{application_id: app_id}), do: true + def authorize(:list_secrets, %User{id: user_id}, %App{creator_id: user_id}), do: true + def authorize(:create_secret, %User{id: user_id}, %App{creator_id: user_id}), do: true + def authorize(:update_secret, %User{id: user_id}, %App{creator_id: user_id}), do: true + def authorize(:delete_secret, %User{id: user_id}, %App{creator_id: user_id}), do: true + # credo:disable-for-next-line Credo.Check.Readability.StrictModuleLayout use LenraWeb.Policy.Default end diff --git a/apps/lenra_web/lib/lenra_web/router.ex b/apps/lenra_web/lib/lenra_web/router.ex index 5c89346f..d50afef8 100644 --- a/apps/lenra_web/lib/lenra_web/router.ex +++ b/apps/lenra_web/lib/lenra_web/router.ex @@ -113,6 +113,12 @@ defmodule LenraWeb.Router do resources("/:app_id/environments", EnvsController, only: [:index, :create]) patch("/:app_id/environments/:env_id", EnvsController, :update) + # Environment's Secrets + get("/:app_id/environments/:env_id/secrets", EnvsController, :list_secrets) + post("/:app_id/environments/:env_id/secrets", EnvsController, :create_secret) + put("/:app_id/environments/:env_id/secrets/:key", EnvsController, :update_secret) + delete("/:app_id/environments/:env_id/secrets/:key", EnvsController, :delete_secret) + # Invitations to env resources("/:app_id/environments/:env_id/invitations", UserEnvironmentAccessController, only: [:index, :create]) diff --git a/config/config.exs b/config/config.exs index 55e4ff69..b80655a6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -122,7 +122,8 @@ config :application_runner, ApplicationRunner.Scheduler, storage: ApplicationRun # additional_env_modules: {LenraWeb.ApplicationRunnerAdapter, :additional_env_modules} config :lenra, - kubernetes_build_namespace: System.get_env("KUBERNETES_BUILD_NAMESPACE", "lenra-build") + kubernetes_build_namespace: System.get_env("KUBERNETES_BUILD_NAMESPACE", "lenra-build"), + kubernetes_apps_namespace: System.get_env("KUBERNETES_APPS_NAMESPACE", "openfaas-fn") config :argon2_elixir, t_cost: 8, diff --git a/config/dev.exs b/config/dev.exs index ba44506b..1b0d98bf 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -97,6 +97,7 @@ config :lenra, kubernetes_api_url: System.get_env("KUBERNETES_API_URL"), kubernetes_api_cert: System.get_env("KUBERNETES_API_CERT"), kubernetes_api_token: System.get_env("KUBERNETES_API_TOKEN", ""), + kubernetes_apps_namespace: System.get_env("KUBERNETES_APPS_NAMESPACE", "openfaas-fn"), kubernetes_build_namespace: System.get_env("KUBERNETES_BUILD_NAMESPACE", "lenra_build"), kubernetes_build_scripts: System.get_env("KUBERNETES_BUILD_SCRIPTS", "lenra_build"), kubernetes_build_secret: System.get_env("KUBERNETES_BUILD_SECRET", "lenra_build"), diff --git a/config/runtime.exs b/config/runtime.exs index 6c8f25ac..c3a92ca6 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -58,6 +58,7 @@ if config_env() == :prod do ) |> File.read!() |> String.trim(), + kubernetes_apps_namespace: System.get_env("KUBERNETES_APPS_NAMESPACE", "openfaas-fn"), kubernetes_build_namespace: System.get_env("KUBERNETES_BUILD_NAMESPACE", "lenra-build"), kubernetes_build_scripts: System.get_env("KUBERNETES_BUILD_SCRIPTS", "lenra-build-scripts"), kubernetes_build_secret: System.get_env("KUBERNETES_BUILD_SECRET", "lenra-build-secret"), diff --git a/libs/application_runner/lib/services/application_services.ex b/libs/application_runner/lib/services/application_services.ex index 20febb86..62db68a4 100644 --- a/libs/application_runner/lib/services/application_services.ex +++ b/libs/application_runner/lib/services/application_services.ex @@ -172,10 +172,14 @@ defmodule ApplicationRunner.ApplicationServices do @spec generate_function_object(String.t(), String.t(), map()) :: map() def generate_function_object(function_name, image_name, labels) do + # secret_name = '#{app.service_name}-secret-#{env.id}' + %{ "image" => image_name, "service" => function_name, + # , "secrets" => Application.fetch_env!(:application_runner, :faas_secrets), + # "environments" => ApiServices.get_environment_secrets(app.service_name, env.id), "requests" => %{ "cpu" => Application.fetch_env!(:application_runner, :faas_request_cpu), "memory" => Application.fetch_env!(:application_runner, :faas_request_memory) diff --git a/libs/application_runner/priv/components-api b/libs/application_runner/priv/components-api index 87c0e492..c6584e75 160000 --- a/libs/application_runner/priv/components-api +++ b/libs/application_runner/priv/components-api @@ -1 +1 @@ -Subproject commit 87c0e492eda38e38ce900d4dc6cd21c270f6a9d6 +Subproject commit c6584e757ecf0b7cbf4cccc5475768c4e4ab78c3