From 2b4b6f4dec4a0bd4a232a9e90a81dbd74d61d7e3 Mon Sep 17 00:00:00 2001 From: Gagan Trivedi Date: Sat, 15 Aug 2026 16:03:47 +0530 Subject: [PATCH 1/2] feat: wire up the edge_control_plane private app Installs the app when the flagsmith-private wheel provides it, and mounts its two URL trees outside SaaS: proxy key management under organisations/{id}/edge-proxy/ and the proxy endpoints under api/v1/proxy/. --- api/app/settings/common.py | 6 ++++++ api/app/urls.py | 11 +++++++++++ api/pyproject.toml | 4 ++++ 3 files changed, 21 insertions(+) diff --git a/api/app/settings/common.py b/api/app/settings/common.py index e61b2164a316..cd1d21a4165e 100644 --- a/api/app/settings/common.py +++ b/api/app/settings/common.py @@ -1156,6 +1156,12 @@ "USER_FILTER_PARSER": "scim.filters.UserFilterQuery", } +EDGE_CONTROL_PLANE_INSTALLED = ( + importlib.util.find_spec("edge_control_plane") is not None +) +if EDGE_CONTROL_PLANE_INSTALLED: + INSTALLED_APPS.append("edge_control_plane") + DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Used to keep edge identities in sync by forwarding the http requests diff --git a/api/app/urls.py b/api/app/urls.py index 1b45a36d8732..4c061293c245 100644 --- a/api/app/urls.py +++ b/api/app/urls.py @@ -1,6 +1,7 @@ import importlib from common.core.urls import urlpatterns as core_urlpatterns +from common.core.utils import is_saas from django.conf import settings from django.contrib import admin from django.urls import include, path, re_path @@ -124,6 +125,16 @@ ), ] +# SaaS images ship the private wheel, so is_saas() is the gate, not module presence. +if settings.EDGE_CONTROL_PLANE_INSTALLED and not is_saas(): # pragma: no cover + urlpatterns += [ + path( + "api/v1/organisations//edge-proxy/", + include("edge_control_plane.management_urls"), + ), + path("api/v1/proxy/", include("edge_control_plane.urls")), + ] + if settings.WORKFLOWS_LOGIC_INSTALLED: # pragma: no cover workflow_views = importlib.import_module("workflows_logic.views") urlpatterns += [ diff --git a/api/pyproject.toml b/api/pyproject.toml index b1de5efeab8a..edbc82b94169 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -242,6 +242,10 @@ ignore_missing_imports = true module = ["rbac.*"] ignore_missing_imports = true +[[tool.mypy.overrides]] +module = ["edge_control_plane.*"] +ignore_missing_imports = true + [[tool.mypy.overrides]] module = ["saml.*"] ignore_missing_imports = true From 8732a970ba6b3b352762183fe939699491d62179 Mon Sep 17 00:00:00 2001 From: "flagsmith-engineering[bot]" Date: Wed, 16 Sep 2026 10:22:56 +0000 Subject: [PATCH 2/2] chore: Update documentation artefacts --- openapi.yaml | 319 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) diff --git a/openapi.yaml b/openapi.yaml index 484484a670be..e14d9a554745 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -9855,6 +9855,179 @@ paths: - Master API Key: [] tags: - Audit + '/api/v1/organisations/{organisation_pk}/edge-proxy/keys/': + get: + operationId: api_v1_organisations_edge_proxy_keys_list + parameters: + - name: organisation_pk + in: path + required: true + schema: + type: integer + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProxyKey' + security: + - tokenAuth: [] + - Master API Key: [] + tags: + - Organisations + post: + operationId: api_v1_organisations_edge_proxy_keys_create + parameters: + - name: organisation_pk + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyKey' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ProxyKey' + multipart/form-data: + schema: + $ref: '#/components/schemas/ProxyKey' + responses: + '201': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyKey' + security: + - tokenAuth: [] + - Master API Key: [] + tags: + - Organisations + '/api/v1/organisations/{organisation_pk}/edge-proxy/keys/{prefix}/': + get: + operationId: api_v1_organisations_edge_proxy_keys_retrieve + parameters: + - name: organisation_pk + in: path + required: true + schema: + type: integer + - name: prefix + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyKey' + security: + - tokenAuth: [] + - Master API Key: [] + tags: + - Organisations + put: + operationId: api_v1_organisations_edge_proxy_keys_update + parameters: + - name: organisation_pk + in: path + required: true + schema: + type: integer + - name: prefix + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyKey' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ProxyKey' + multipart/form-data: + schema: + $ref: '#/components/schemas/ProxyKey' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyKey' + security: + - tokenAuth: [] + - Master API Key: [] + tags: + - Organisations + patch: + operationId: api_v1_organisations_edge_proxy_keys_partial_update + parameters: + - name: organisation_pk + in: path + required: true + schema: + type: integer + - name: prefix + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedProxyKey' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedProxyKey' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedProxyKey' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ProxyKey' + security: + - tokenAuth: [] + - Master API Key: [] + tags: + - Organisations + delete: + operationId: api_v1_organisations_edge_proxy_keys_destroy + parameters: + - name: organisation_pk + in: path + required: true + schema: + type: integer + - name: prefix + in: path + required: true + schema: + type: string + responses: + '204': + description: No response body + security: + - tokenAuth: [] + - Master API Key: [] + tags: + - Organisations '/api/v1/organisations/{organisation_pk}/github/create-cleanup-issue/': post: operationId: api_v1_organisations_github_create_cleanup_issue_create @@ -18680,6 +18853,28 @@ paths: - Master API Key: [] tags: - Projects + /api/v1/proxy/config/: + get: + operationId: api_v1_proxy_config_list + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EdgeProxyEnvironment' + tags: + - Other + /api/v1/proxy/usage/: + post: + operationId: api_v1_proxy_usage_create + responses: + '200': + description: No response body + tags: + - Other '/api/v1/segments/get-by-uuid/{uuid}/': get: operationId: api_v1_segments_get_by_uuid_retrieve @@ -20589,6 +20784,48 @@ components: required: - multivariate_feature_option - percentage_allocation + EdgeProxyEnvironment: + type: object + properties: + id: + type: integer + readOnly: true + name: + type: string + maxLength: 2000 + client_side_key: + type: string + server_side_keys: + type: array + items: + $ref: '#/components/schemas/EdgeProxyServerSideKey' + updated_at: + description: 'Tracks changes to self and related entities, e.g. FeatureStates.' + type: string + format: date-time + project_id: + type: integer + readOnly: true + organisation_id: + type: integer + readOnly: true + required: + - client_side_key + - name + - server_side_keys + EdgeProxyServerSideKey: + type: object + properties: + key: + type: string + maxLength: 100 + active: + type: boolean + expires_at: + type: + - string + - 'null' + format: date-time EdgeV2MigrationStatusEnum: description: |- * `NOT_STARTED` - Not Started @@ -25590,6 +25827,47 @@ components: enforce_feature_owners: description: Require at least one user or group owner when creating a feature. type: boolean + PatchedProxyKey: + type: object + properties: + prefix: + type: string + readOnly: true + created: + type: string + format: date-time + readOnly: true + name: + description: A free-form name for the API key. Need not be unique. 50 characters max. + type: string + maxLength: 50 + revoked: + description: 'If the API key is revoked, clients cannot use it anymore. (This cannot be undone.)' + type: boolean + expiry_date: + description: 'Once API key expires, clients cannot use it anymore.' + type: + - string + - 'null' + format: date-time + title: Expires + key: + description: 'Only the key hash is stored, so the full key is returned on create and is unavailable from every other endpoint.' + type: string + readOnly: true + has_expired: + type: boolean + readOnly: true + created_by_user: + type: + - integer + - 'null' + readOnly: true + created_by_master_api_key: + type: + - string + - 'null' + readOnly: true PatchedReleasePipeline: type: object properties: @@ -27122,6 +27400,47 @@ components: type: boolean required: - name + ProxyKey: + type: object + properties: + prefix: + type: string + readOnly: true + created: + type: string + format: date-time + readOnly: true + name: + description: A free-form name for the API key. Need not be unique. 50 characters max. + type: string + maxLength: 50 + revoked: + description: 'If the API key is revoked, clients cannot use it anymore. (This cannot be undone.)' + type: boolean + expiry_date: + description: 'Once API key expires, clients cannot use it anymore.' + type: + - string + - 'null' + format: date-time + title: Expires + key: + description: 'Only the key hash is stored, so the full key is returned on create and is unavailable from every other endpoint.' + type: string + readOnly: true + has_expired: + type: boolean + readOnly: true + created_by_user: + type: + - integer + - 'null' + readOnly: true + created_by_master_api_key: + type: + - string + - 'null' + readOnly: true ReleasePipeline: type: object properties: