From 93ba413661d9c3a7b1227b065f282adf41339148 Mon Sep 17 00:00:00 2001 From: dushaniw Date: Wed, 14 Jan 2026 11:23:09 +0530 Subject: [PATCH 01/11] add initial data model changes. --- platform-api/src/internal/database/schema.sql | 13 ++++--- platform-api/src/internal/dto/api.go | 38 +++++++++++-------- platform-api/src/internal/model/api.go | 19 +++++----- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/platform-api/src/internal/database/schema.sql b/platform-api/src/internal/database/schema.sql index f48f6240c..f770f5571 100644 --- a/platform-api/src/internal/database/schema.sql +++ b/platform-api/src/internal/database/schema.sql @@ -51,9 +51,6 @@ CREATE TABLE IF NOT EXISTS apis ( lifecycle_status VARCHAR(20) DEFAULT 'CREATED', has_thumbnail BOOLEAN DEFAULT FALSE, is_default_version BOOLEAN DEFAULT FALSE, - is_revision BOOLEAN DEFAULT FALSE, - revisioned_api_id VARCHAR(40), - revision_id INTEGER DEFAULT 0, type VARCHAR(20) DEFAULT 'HTTP', transport VARCHAR(255), -- JSON array as TEXT security_enabled BOOLEAN, @@ -219,17 +216,21 @@ CREATE TABLE IF NOT EXISTS policies ( ); --- API Deployments table +-- API Deployments table (immutable deployment artifacts) CREATE TABLE IF NOT EXISTS api_deployments ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + deployment_id VARCHAR(40) PRIMARY KEY, api_uuid VARCHAR(40) NOT NULL, organization_uuid VARCHAR(40) NOT NULL, gateway_uuid VARCHAR(40) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'DEPLOYED', + base_deployment_id VARCHAR(40), -- Reference to the deployment used as base, NULL if based on "current" + content BLOB NOT NULL, -- Immutable deployment artifact (API spec + config) + metadata TEXT, -- JSON object for flexible key-value metadata created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (api_uuid) REFERENCES apis(uuid) ON DELETE CASCADE, FOREIGN KEY (organization_uuid) REFERENCES organizations(uuid) ON DELETE CASCADE, FOREIGN KEY (gateway_uuid) REFERENCES gateways(uuid) ON DELETE CASCADE, - UNIQUE(api_uuid, gateway_uuid) + CHECK (status IN ('DEPLOYED', 'UNDEPLOYED')) ); -- API Associations table (for both gateways and dev portals) diff --git a/platform-api/src/internal/dto/api.go b/platform-api/src/internal/dto/api.go index edfb49ecc..e649018d6 100644 --- a/platform-api/src/internal/dto/api.go +++ b/platform-api/src/internal/dto/api.go @@ -36,9 +36,6 @@ type API struct { LifeCycleStatus string `json:"lifeCycleStatus,omitempty" yaml:"lifeCycleStatus,omitempty"` HasThumbnail bool `json:"hasThumbnail,omitempty" yaml:"hasThumbnail,omitempty"` IsDefaultVersion bool `json:"isDefaultVersion,omitempty" yaml:"isDefaultVersion,omitempty"` - IsRevision bool `json:"isRevision,omitempty" yaml:"isRevision,omitempty"` - RevisionedAPIID string `json:"revisionedApiId,omitempty" yaml:"revisionedApiId,omitempty"` - RevisionID int `json:"revisionId,omitempty" yaml:"revisionId,omitempty"` Type string `json:"type,omitempty" yaml:"type,omitempty"` Transport []string `json:"transport,omitempty" yaml:"transport,omitempty"` MTLS *MTLSConfig `json:"mtls,omitempty" yaml:"mtls,omitempty"` @@ -164,18 +161,29 @@ type Policy struct { Params map[string]interface{} `json:"params,omitempty" yaml:"params,omitempty"` } -// APIRevisionDeployment represents an API revision deployment -type APIRevisionDeployment struct { - RevisionId string `json:"revisionId,omitempty" yaml:"revisionId,omitempty"` - GatewayID string `json:"gatewayId" yaml:"gatewayId"` - Status string `json:"status" yaml:"status"` - VHost string `json:"vhost" yaml:"vhost"` - DisplayOnDevportal bool `json:"displayOnDevportal" yaml:"displayOnDevportal"` - DeployedTime *string `json:"deployedTime,omitempty" yaml:"deployedTime,omitempty"` - SuccessDeployedTime *string `json:"successDeployedTime,omitempty" yaml:"successDeployedTime,omitempty"` - LiveGatewayCount int `json:"liveGatewayCount,omitempty" yaml:"liveGatewayCount,omitempty"` - DeployedGatewayCount int `json:"deployedGatewayCount,omitempty" yaml:"deployedGatewayCount,omitempty"` - FailedGatewayCount int `json:"failedGatewayCount,omitempty" yaml:"failedGatewayCount,omitempty"` +// DeployAPIRequest represents a request to deploy an API +type DeployAPIRequest struct { + Base string `json:"base" yaml:"base" binding:"required"` // "current" or a deploymentId + GatewayID string `json:"gatewayId" yaml:"gatewayId" binding:"required"` // Target gateway ID + Metadata map[string]interface{} `json:"metadata,omitempty" yaml:"metadata,omitempty"` // Flexible key-value metadata +} + +// DeploymentResponse represents a deployment artifact +type DeploymentResponse struct { + DeploymentID string `json:"deploymentId" yaml:"deploymentId"` + ApiID string `json:"apiId" yaml:"apiId"` + GatewayID string `json:"gatewayId" yaml:"gatewayId"` + Status string `json:"status" yaml:"status"` + BaseDeploymentID *string `json:"baseDeploymentId,omitempty" yaml:"baseDeploymentId,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty" yaml:"metadata,omitempty"` + CreatedAt time.Time `json:"createdAt" yaml:"createdAt"` +} + +// DeploymentListResponse represents a list of deployments +type DeploymentListResponse struct { + Count int `json:"count" yaml:"count"` + List []*DeploymentResponse `json:"list" yaml:"list"` + Pagination Pagination `json:"pagination" yaml:"pagination"` } // APIDeploymentYAML represents the API deployment YAML structure diff --git a/platform-api/src/internal/model/api.go b/platform-api/src/internal/model/api.go index 6077a87e7..6264a474f 100644 --- a/platform-api/src/internal/model/api.go +++ b/platform-api/src/internal/model/api.go @@ -37,9 +37,6 @@ type API struct { LifeCycleStatus string `json:"lifeCycleStatus,omitempty" db:"lifecycle_status"` HasThumbnail bool `json:"hasThumbnail,omitempty" db:"has_thumbnail"` IsDefaultVersion bool `json:"isDefaultVersion,omitempty" db:"is_default_version"` - IsRevision bool `json:"isRevision,omitempty" db:"is_revision"` - RevisionedAPIID string `json:"revisionedApiId,omitempty" db:"revisioned_api_id"` - RevisionID int `json:"revisionId,omitempty" db:"revision_id"` Type string `json:"type,omitempty" db:"type"` Transport []string `json:"transport,omitempty" db:"transport"` MTLS *MTLSConfig `json:"mtls,omitempty"` @@ -242,13 +239,17 @@ type Policy struct { Params map[string]interface{} `json:"params,omitempty"` } -// APIDeployment represents an API deployment record +// APIDeployment represents an immutable API deployment artifact type APIDeployment struct { - ID int `json:"id,omitempty" db:"id"` - ApiID string `json:"apiId" db:"api_uuid"` - OrganizationID string `json:"organizationId" db:"organization_uuid"` - GatewayID string `json:"gatewayId" db:"gateway_uuid"` - CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` + DeploymentID string `json:"deploymentId" db:"deployment_id"` + ApiID string `json:"apiId" db:"api_uuid"` + OrganizationID string `json:"organizationId" db:"organization_uuid"` + GatewayID string `json:"gatewayId" db:"gateway_uuid"` + Status string `json:"status" db:"status"` + BaseDeploymentID *string `json:"baseDeploymentId,omitempty" db:"base_deployment_id"` + Content []byte `json:"-" db:"content"` + Metadata map[string]interface{} `json:"metadata,omitempty" db:"metadata"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` } // TableName returns the table name for the APIDeployment model From 1c6e2a70c4e9fd8ed70618957402b66d0d71e39f Mon Sep 17 00:00:00 2001 From: dushaniw Date: Wed, 14 Jan 2026 12:31:09 +0530 Subject: [PATCH 02/11] add oas changes for deployment. --- platform-api/src/resources/openapi.yaml | 460 +++++++++++++++++------- 1 file changed, 335 insertions(+), 125 deletions(-) diff --git a/platform-api/src/resources/openapi.yaml b/platform-api/src/resources/openapi.yaml index c888bf53d..2a82f6970 100644 --- a/platform-api/src/resources/openapi.yaml +++ b/platform-api/src/resources/openapi.yaml @@ -519,45 +519,6 @@ paths: '500': $ref: '#/components/responses/InternalServerError' - /apis/{apiId}/deploy-revision: - post: - tags: - - API Revisions - summary: Deploy API Revision - description: | - Deploy a revision of an API. Access is validated against the organization - in the JWT token. - operationId: DeployAPIRevision - parameters: - - $ref: '#/components/parameters/apiID' - - $ref: '#/components/parameters/revisionId-Q' - requestBody: - description: Deployment object that needs to be added - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/APIRevisionDeployment' - responses: - 200: - description: | - Created. - Successful response with the newly deployed APIRevisionDeployment List object as the entity in the body. - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/APIRevisionDeployment' - 400: - $ref: '#/components/responses/BadRequest' - 401: - $ref: '#/components/responses/Unauthorized' - 404: - $ref: '#/components/responses/NotFound' - '500': - $ref: '#/components/responses/InternalServerError' /apis/{apiId}/gateways: get: @@ -807,6 +768,217 @@ paths: '500': $ref: '#/components/responses/InternalServerError' + /apis/{apiId}/deploy: + post: + summary: Deploy API to gateway + description: | + Creates an immutable deployment artifact for an API and deploys it to a specified gateway. + Each deployment targets a single gateway. The apiId parameter is the API handle (identifier), + not the UUID. The deployment becomes active immediately on the target gateway. + Access is validated against the organization in the JWT token. + operationId: DeployAPI + tags: + - API Deployments + parameters: + - $ref: '#/components/parameters/apiID' + requestBody: + description: Deployment request with gateway ID, base reference, and metadata + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeployAPIRequest' + responses: + '201': + description: API deployed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/DeploymentResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /apis/{apiId}/deployments: + get: + summary: Get deployments for an API + description: | + Retrieves all deployment artifacts for a specific API. The apiId parameter is the API handle (identifier), + not the UUID. Supports filtering by gateway UUID and deployment status. + Access is validated against the organization in the JWT token. + operationId: GetDeployments + tags: + - API Deployments + parameters: + - $ref: '#/components/parameters/apiID' + - $ref: '#/components/parameters/gatewayId-Q' + - $ref: '#/components/parameters/deploymentStatus-Q' + responses: + '200': + description: Deployments retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/DeploymentListResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /apis/{apiId}/deployments/{deploymentId}: + get: + summary: Get deployment by ID + description: | + Retrieves metadata for a specific deployment artifact including status, gateway association, + and timestamps. Access is validated against the organization in the JWT token. + operationId: GetDeployment + tags: + - API Deployments + parameters: + - $ref: '#/components/parameters/apiID' + - $ref: '#/components/parameters/deploymentId' + responses: + '200': + description: Deployment metadata retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/DeploymentResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + delete: + summary: Delete deployment + description: | + Deletes a deployment artifact. Deletion is only allowed when the deployment is in UNDEPLOYED status. + Access is validated against the organization in the JWT token. + operationId: DeleteDeployment + tags: + - API Deployments + parameters: + - $ref: '#/components/parameters/apiID' + - $ref: '#/components/parameters/deploymentId' + responses: + '204': + description: Deployment deleted successfully + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: Conflict. Deployment is currently DEPLOYED and cannot be deleted + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/InternalServerError' + + /apis/{apiId}/deployments/{deploymentId}/redeploy: + post: + summary: Redeploy deployment + description: | + Redeploys an existing deployment artifact. If a previous deployment exists on the same gateway, + it will be automatically undeployed first. Access is validated against the organization + in the JWT token. + operationId: RedeployDeployment + tags: + - API Deployments + parameters: + - $ref: '#/components/parameters/apiID' + - $ref: '#/components/parameters/deploymentId' + requestBody: + description: Redeploy request details + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RedeployRequest' + responses: + '200': + description: Deployment redeployed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/DeploymentResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /apis/{apiId}/deployments/{deploymentId}/undeploy: + post: + summary: Undeploy deployment + description: | + Undeploys an active deployment, stopping the API from being served. The deployment artifact + remains in the system with UNDEPLOYED status and can be redeployed later. Access is validated + against the organization in the JWT token. + operationId: UndeployDeployment + tags: + - API Deployments + parameters: + - $ref: '#/components/parameters/apiID' + - $ref: '#/components/parameters/deploymentId' + responses: + '200': + description: Deployment undeployed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/DeploymentResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /apis/{apiId}/deployments/{deploymentId}/content: + get: + summary: Get deployment artifact content + description: | + Retrieves the full content of a deployment artifact. The artifact contains the complete + API configuration and policies as they were at deployment time. Access is validated + against the organization in the JWT token. + operationId: GetDeploymentContent + tags: + - API Deployments + parameters: + - $ref: '#/components/parameters/apiID' + - $ref: '#/components/parameters/deploymentId' + responses: + '200': + description: Deployment content retrieved successfully + content: + application/json: + schema: + type: object + description: Raw deployment artifact content + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + /gateways: post: summary: Register a new gateway @@ -1728,17 +1900,6 @@ components: isDefaultVersion: type: boolean example: false - isRevision: - type: boolean - example: false - revisionedApiId: - type: string - description: UUID of the api artifact - readOnly: true - example: 01234567-0123-0123-0123-012345678901 - revisionId: - type: integer - example: 1 type: type: string description: The api creation type to be used. Accepted values are HTTP, @@ -2275,14 +2436,9 @@ components: type: object description: Details about API deployment to a specific gateway required: - - revisionId - status - deployedAt properties: - revisionId: - type: string - description: ID of the deployed API revision - example: "c26b2b9b-4632-4ca4-b6f3-521c8863990c" status: description: Current deployment status type: string @@ -2298,65 +2454,6 @@ components: description: Timestamp when the API was deployed example: "2025-10-15T11:00:00Z" - APIRevisionDeployment: - title: APIRevisionDeployment Info object with basic API deployment details - properties: - revisionId: - maxLength: 255 - minLength: 0 - type: string - example: c26b2b9b-4632-4ca4-b6f3-521c8863990c - gatewayId: - type: string - format: uuid - example: "987e6543-e21b-45d3-a789-426614174999" - status: - type: string - description: Current deployment status - example: CREATED - default: CREATED - enum: - - CREATED - - APPROVED - - REJECTED - vhost: - maxLength: 255 - minLength: 1 - # hostname regex as per RFC 1123 (http://tools.ietf.org/html/rfc1123) and appended * - pattern: '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$' - type: string - example: mg.wso2.com - displayOnDevportal: - type: boolean - example: true - default: true - deployedTime: - readOnly: true - type: string - format: date-time - successDeployedTime: - readOnly: true - type: string - format: date-time - liveGatewayCount: - readOnly: true - type: integer - description: | - The number of gateways that are currently live in the gateway environment - example: 1 - deployedGatewayCount: - readOnly: true - type: integer - description: | - The number of gateways in which the API revision is deployed successfully - example: 1 - failedGatewayCount: - readOnly: true - type: integer - description: | - The number of gateways where the API revision deployment has failed - example: 1 - CreateGatewayRequest: description: | Request body for creating a gateway. Organization ID is automatically extracted @@ -3547,6 +3644,99 @@ components: - code - message + DeployAPIRequest: + type: object + required: + - base + - gatewayId + properties: + base: + type: string + description: The source for the API definition. Can be "current" (latest working copy) or a deploymentId (existing deployment) + gatewayId: + type: string + format: uuid + description: The target gateway UUID for this deployment + metadata: + type: object + additionalProperties: true + description: Optional metadata for the deployment + + RedeployRequest: + type: object + required: + - gatewayId + properties: + gatewayId: + type: string + format: uuid + description: Gateway UUID to redeploy the API to + metadata: + type: object + additionalProperties: true + description: Optional metadata for the redeployment + + DeploymentResponse: + type: object + required: + - deploymentId + - apiId + - organizationId + - gatewayId + - status + - createdAt + properties: + deploymentId: + type: string + format: uuid + description: Unique identifier for the deployment + apiId: + type: string + format: uuid + description: UUID of the deployed API + organizationId: + type: string + format: uuid + description: UUID of the organization + gatewayId: + type: string + format: uuid + description: UUID of the gateway + status: + type: string + enum: + - DEPLOYED + - UNDEPLOYED + description: Current deployment status + baseDeploymentId: + type: string + format: uuid + nullable: true + description: UUID of the previous deployment (if redeployed) + metadata: + type: object + additionalProperties: true + description: Metadata associated with the deployment + createdAt: + type: string + format: date-time + description: Timestamp when the deployment was created + + DeploymentListResponse: + type: object + required: + - count + - deployments + properties: + count: + type: integer + description: Total number of deployments + deployments: + type: array + items: + $ref: '#/components/schemas/DeploymentResponse' + description: List of deployments + responses: Unauthorized: description: Unauthorized. Authentication credentials are missing or invalid. @@ -3633,15 +3823,6 @@ components: type: string example: my-api-handle - revisionId-Q: - name: revisionId - in: query - description: | - Revision ID of an API - schema: - type: string - format: uuid - example: crer4354-jui52345-245vd-93fvk-137063 projectId-Q: name: projectId @@ -3758,6 +3939,35 @@ components: format: uuid example: "123e4567-e89b-12d3-a456-426614174000" + deploymentId: + name: deploymentId + in: path + required: true + schema: + type: string + format: uuid + description: The UUID of the deployment + + apiId-Q: + name: apiId + in: query + required: false + schema: + type: string + format: uuid + description: Filter deployments by API UUID + + deploymentStatus-Q: + name: status + in: query + required: false + schema: + type: string + enum: + - DEPLOYED + - UNDEPLOYED + description: Filter deployments by status (DEPLOYED or UNDEPLOYED) + tags: - name: Health description: Health check endpoints @@ -3767,8 +3977,8 @@ tags: description: Project management operations - name: APIs description: API management operations - - name: API Revisions - description: API revision deployment operations + - name: API Deployments + description: API deployment artifact management and lifecycle operations - name: API Portal description: API portal publishing and unpublishing operations - name: DevPortals From 12fc18403f43533c8ea42c585352a52f84fc17f2 Mon Sep 17 00:00:00 2001 From: dushaniw Date: Wed, 14 Jan 2026 12:57:50 +0530 Subject: [PATCH 03/11] add unique constraint. --- platform-api/src/internal/database/schema.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platform-api/src/internal/database/schema.sql b/platform-api/src/internal/database/schema.sql index f770f5571..21b1f9ace 100644 --- a/platform-api/src/internal/database/schema.sql +++ b/platform-api/src/internal/database/schema.sql @@ -224,12 +224,13 @@ CREATE TABLE IF NOT EXISTS api_deployments ( gateway_uuid VARCHAR(40) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'DEPLOYED', base_deployment_id VARCHAR(40), -- Reference to the deployment used as base, NULL if based on "current" - content BLOB NOT NULL, -- Immutable deployment artifact (API spec + config) + content BLOB NOT NULL, -- Immutable deployment artifact metadata TEXT, -- JSON object for flexible key-value metadata created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (api_uuid) REFERENCES apis(uuid) ON DELETE CASCADE, FOREIGN KEY (organization_uuid) REFERENCES organizations(uuid) ON DELETE CASCADE, FOREIGN KEY (gateway_uuid) REFERENCES gateways(uuid) ON DELETE CASCADE, + UNIQUE(api_uuid, gateway_uuid, deployment_id, organization_uuid) CHECK (status IN ('DEPLOYED', 'UNDEPLOYED')) ); From 06fa711a44c7ab8338e9e78d9e72aee6fe42d497 Mon Sep 17 00:00:00 2001 From: dushaniw Date: Wed, 14 Jan 2026 13:00:51 +0530 Subject: [PATCH 04/11] remove deploy api revision handler method. --- platform-api/src/config/config.go | 8 ++++ platform-api/src/internal/handler/api.go | 60 ------------------------ 2 files changed, 8 insertions(+), 60 deletions(-) diff --git a/platform-api/src/config/config.go b/platform-api/src/config/config.go index 8f5dbfe7a..d84ad266d 100644 --- a/platform-api/src/config/config.go +++ b/platform-api/src/config/config.go @@ -44,6 +44,9 @@ type Server struct { // Default DevPortal configurations DefaultDevPortal DefaultDevPortal `envconfig:"DEFAULT_DEVPORTAL"` + + // Deployment configurations + Deployments Deployments `envconfig:"DEPLOYMENTS"` } // JWT holds JWT-specific configuration @@ -98,6 +101,11 @@ type DefaultDevPortal struct { SuperAdminRole string `envconfig:"SUPER_ADMIN_ROLE" default:"superAdmin"` } +// Deployments holds deployment-specific configuration +type Deployments struct { + MaxPerAPIGateway int `envconfig:"MAX_PER_API_GATEWAY" default:"20"` +} + // package-level variable and mutex for thread safety var ( processOnce sync.Once diff --git a/platform-api/src/internal/handler/api.go b/platform-api/src/internal/handler/api.go index bfe708e59..46eadebff 100644 --- a/platform-api/src/internal/handler/api.go +++ b/platform-api/src/internal/handler/api.go @@ -381,65 +381,6 @@ func (h *APIHandler) GetAPIGateways(c *gin.Context) { c.JSON(http.StatusOK, gateways) } -// DeployAPIRevision handles POST /api/v1/apis/:apiId/deploy-revision to deploy an API revision -func (h *APIHandler) DeployAPIRevision(c *gin.Context) { - orgId, exists := middleware.GetOrganizationFromContext(c) - if !exists { - c.JSON(http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) - return - } - - apiId := c.Param("apiId") - if apiId == "" { - c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) - return - } - - // Get optional revision ID from query parameter - revisionID := c.Query("revisionId") - - // Parse deployment request body - var deploymentRequests []dto.APIRevisionDeployment - if err := c.ShouldBindJSON(&deploymentRequests); err != nil { - c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - err.Error())) - return - } - - // Validate that we have at least one deployment request - if len(deploymentRequests) == 0 { - c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "At least one deployment configuration is required")) - return - } - - // Call service to deploy the API - deployments, err := h.apiService.DeployAPIRevisionByHandle(apiId, revisionID, deploymentRequests, orgId) - if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) - return - } - if strings.Contains(err.Error(), "invalid api deployment") { - c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid API deployment configuration")) - log.Printf("[ERROR] Failed to deploy API revision: apiUUID=%s revisionID=%s error=%v", - apiId, revisionID, err) - return - } - log.Printf("[ERROR] Failed to deploy API revision: apiUUID=%s revisionID=%s error=%v", - apiId, revisionID, err) - c.JSON(http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to deploy API revision")) - return - } - - c.JSON(http.StatusOK, deployments) -} - // PublishToDevPortal handles POST /api/v1/apis/:apiId/devportals/publish // // This endpoint publishes an API to a specific DevPortal with its metadata and OpenAPI definition. @@ -972,7 +913,6 @@ func (h *APIHandler) RegisterRoutes(r *gin.Engine) { apiGroup.PUT("/:apiId", h.UpdateAPI) apiGroup.DELETE("/:apiId", h.DeleteAPI) apiGroup.GET("/validate", h.ValidateAPI) - apiGroup.POST("/:apiId/deploy-revision", h.DeployAPIRevision) apiGroup.GET("/:apiId/gateways", h.GetAPIGateways) apiGroup.POST("/:apiId/gateways", h.AddGatewaysToAPI) apiGroup.POST("/:apiId/devportals/publish", h.PublishToDevPortal) From 36211b06c6f4ce2979354e0a11432d304bd5a4eb Mon Sep 17 00:00:00 2001 From: dushaniw Date: Wed, 14 Jan 2026 13:02:09 +0530 Subject: [PATCH 05/11] add deployment resources in handler. --- .../src/internal/handler/deployment.go | 346 ++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 platform-api/src/internal/handler/deployment.go diff --git a/platform-api/src/internal/handler/deployment.go b/platform-api/src/internal/handler/deployment.go new file mode 100644 index 000000000..fb27bd304 --- /dev/null +++ b/platform-api/src/internal/handler/deployment.go @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package handler + +import ( + "errors" + "log" + "net/http" + + "platform-api/src/internal/constants" + "platform-api/src/internal/dto" + "platform-api/src/internal/middleware" + "platform-api/src/internal/service" + "platform-api/src/internal/utils" + + "github.com/gin-gonic/gin" +) + +type DeploymentHandler struct { + deploymentService *service.DeploymentService +} + +func NewDeploymentHandler(deploymentService *service.DeploymentService) *DeploymentHandler { + return &DeploymentHandler{ + deploymentService: deploymentService, + } +} + +// DeployAPI handles POST /api/v1/apis/:apiId/deploy +// Creates a new immutable deployment artifact and deploys it to a gateway +func (h *DeploymentHandler) DeployAPI(c *gin.Context) { + orgId, exists := middleware.GetOrganizationFromContext(c) + if !exists { + c.JSON(http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", + "Organization claim not found in token")) + return + } + + apiId := c.Param("apiId") + if apiId == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API ID is required")) + return + } + + var req dto.DeployAPIRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + return + } + + // Validate required fields + if req.Base == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "base is required (use 'current' or a deploymentId)")) + return + } + if req.GatewayID == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "gatewayId is required")) + return + } + + deployment, err := h.deploymentService.DeployAPIByHandle(apiId, &req, orgId) + if err != nil { + if errors.Is(err, constants.ErrAPINotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "API not found")) + return + } + if errors.Is(err, constants.ErrGatewayNotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "Gateway not found")) + return + } + log.Printf("[ERROR] Failed to deploy API: apiId=%s error=%v", apiId, err) + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + err.Error())) + return + } + + c.JSON(http.StatusCreated, deployment) +} + +// RedeployDeployment handles POST /api/v1/apis/:apiId/deployments/:deploymentId/redeploy +// Re-deploys an existing undeployed deployment artifact to its original gateway +func (h *DeploymentHandler) RedeployDeployment(c *gin.Context) { + orgId, exists := middleware.GetOrganizationFromContext(c) + if !exists { + c.JSON(http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", + "Organization claim not found in token")) + return + } + + apiId := c.Param("apiId") + deploymentId := c.Param("deploymentId") + + if apiId == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API ID is required")) + return + } + if deploymentId == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Deployment ID is required")) + return + } + + deployment, err := h.deploymentService.RedeployDeploymentByHandle(apiId, deploymentId, orgId) + if err != nil { + if errors.Is(err, constants.ErrAPINotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "API not found")) + return + } + log.Printf("[ERROR] Failed to redeploy: apiId=%s deploymentId=%s error=%v", apiId, deploymentId, err) + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + return + } + + c.JSON(http.StatusOK, deployment) +} + +// UndeployDeployment handles POST /api/v1/apis/:apiId/deployments/:deploymentId/undeploy +// Undeploys an active deployment by changing its status to UNDEPLOYED +func (h *DeploymentHandler) UndeployDeployment(c *gin.Context) { + orgId, exists := middleware.GetOrganizationFromContext(c) + if !exists { + c.JSON(http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", + "Organization claim not found in token")) + return + } + + apiId := c.Param("apiId") + deploymentId := c.Param("deploymentId") + + if apiId == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API ID is required")) + return + } + if deploymentId == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Deployment ID is required")) + return + } + + err := h.deploymentService.UndeployDeploymentByHandle(apiId, deploymentId, orgId) + if err != nil { + if errors.Is(err, constants.ErrAPINotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "API not found")) + return + } + log.Printf("[ERROR] Failed to undeploy: apiId=%s deploymentId=%s error=%v", apiId, deploymentId, err) + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + return + } + + c.JSON(http.StatusNoContent, nil) +} + +// DeleteDeployment handles DELETE /api/v1/apis/:apiId/deployments/:deploymentId +// Permanently deletes an undeployed deployment artifact +func (h *DeploymentHandler) DeleteDeployment(c *gin.Context) { + orgId, exists := middleware.GetOrganizationFromContext(c) + if !exists { + c.JSON(http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", + "Organization claim not found in token")) + return + } + + apiId := c.Param("apiId") + deploymentId := c.Param("deploymentId") + + if apiId == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API ID is required")) + return + } + if deploymentId == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Deployment ID is required")) + return + } + + err := h.deploymentService.DeleteDeploymentByHandle(apiId, deploymentId, orgId) + if err != nil { + if errors.Is(err, constants.ErrAPINotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "API not found")) + return + } + log.Printf("[ERROR] Failed to delete deployment: apiId=%s deploymentId=%s error=%v", apiId, deploymentId, err) + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + return + } + + c.JSON(http.StatusNoContent, nil) +} + +// GetDeployment handles GET /api/v1/apis/:apiId/deployments/:deploymentId +// Retrieves metadata for a specific deployment artifact +func (h *DeploymentHandler) GetDeployment(c *gin.Context) { + orgId, exists := middleware.GetOrganizationFromContext(c) + if !exists { + c.JSON(http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", + "Organization claim not found in token")) + return + } + + apiId := c.Param("apiId") + deploymentId := c.Param("deploymentId") + + if apiId == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API ID is required")) + return + } + if deploymentId == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Deployment ID is required")) + return + } + + deployment, err := h.deploymentService.GetDeploymentByHandle(apiId, deploymentId, orgId) + if err != nil { + if errors.Is(err, constants.ErrAPINotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "API not found")) + return + } + log.Printf("[ERROR] Failed to get deployment: apiId=%s deploymentId=%s error=%v", apiId, deploymentId, err) + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "Deployment not found")) + return + } + + c.JSON(http.StatusOK, deployment) +} + +// GetDeployments handles GET /api/v1/apis/:apiId/deployments +// Retrieves all deployment records for an API with optional filters +func (h *DeploymentHandler) GetDeployments(c *gin.Context) { + orgId, exists := middleware.GetOrganizationFromContext(c) + if !exists { + c.JSON(http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", + "Organization claim not found in token")) + return + } + + apiId := c.Param("apiId") + if apiId == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API ID is required")) + return + } + + // Get optional query parameters + gatewayId := c.Query("gatewayId") + status := c.Query("status") + + deployments, err := h.deploymentService.GetDeploymentsByHandle(apiId, gatewayId, status, orgId) + if err != nil { + if errors.Is(err, constants.ErrAPINotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "API not found")) + return + } + log.Printf("[ERROR] Failed to get deployments: apiId=%s error=%v", apiId, err) + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to retrieve deployments")) + return + } + + c.JSON(http.StatusOK, deployments) +} + +// GetDeploymentContent handles GET /api/v1/apis/:apiId/deployments/:deploymentId/content +// Retrieves the immutable content blob for a deployment +func (h *DeploymentHandler) GetDeploymentContent(c *gin.Context) { + orgId, exists := middleware.GetOrganizationFromContext(c) + if !exists { + c.JSON(http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", + "Organization claim not found in token")) + return + } + + apiId := c.Param("apiId") + deploymentId := c.Param("deploymentId") + + if apiId == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API ID is required")) + return + } + if deploymentId == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Deployment ID is required")) + return + } + + content, err := h.deploymentService.GetDeploymentContentByHandle(apiId, deploymentId, orgId) + if err != nil { + if errors.Is(err, constants.ErrAPINotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "API not found")) + return + } + log.Printf("[ERROR] Failed to get deployment content: apiId=%s deploymentId=%s error=%v", apiId, deploymentId, err) + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "Deployment not found")) + return + } + + c.Data(http.StatusOK, "application/json", content) +} + +// RegisterRoutes registers all deployment-related routes +func (h *DeploymentHandler) RegisterRoutes(r *gin.Engine) { + apiGroup := r.Group("/api/v1/apis/:apiId") + { + apiGroup.POST("/deploy", h.DeployAPI) + apiGroup.GET("/deployments", h.GetDeployments) + apiGroup.GET("/deployments/:deploymentId", h.GetDeployment) + apiGroup.POST("/deployments/:deploymentId/redeploy", h.RedeployDeployment) + apiGroup.POST("/deployments/:deploymentId/undeploy", h.UndeployDeployment) + apiGroup.DELETE("/deployments/:deploymentId", h.DeleteDeployment) + apiGroup.GET("/deployments/:deploymentId/content", h.GetDeploymentContent) + } +} From 60f53168f8265ee75e816612a42b441616c157e3 Mon Sep 17 00:00:00 2001 From: dushaniw Date: Wed, 14 Jan 2026 14:11:44 +0530 Subject: [PATCH 06/11] add changes related api deployment. --- .../src/internal/model/gateway_event.go | 6 +- platform-api/src/internal/repository/api.go | 297 +++++++-- .../src/internal/repository/interfaces.go | 10 +- platform-api/src/internal/server/server.go | 3 + platform-api/src/internal/service/api.go | 185 ------ .../src/internal/service/deployment.go | 569 ++++++++++++++++++ .../src/internal/service/gateway_events.go | 73 +++ .../src/internal/service/gateway_internal.go | 6 +- platform-api/src/internal/utils/api.go | 11 - 9 files changed, 909 insertions(+), 251 deletions(-) create mode 100644 platform-api/src/internal/service/deployment.go diff --git a/platform-api/src/internal/model/gateway_event.go b/platform-api/src/internal/model/gateway_event.go index 4d4bbd684..8df5166d4 100644 --- a/platform-api/src/internal/model/gateway_event.go +++ b/platform-api/src/internal/model/gateway_event.go @@ -46,13 +46,13 @@ type GatewayEvent struct { } // APIDeploymentEvent contains payload data for "api.deployed" event type. -// This event is sent when an API revision is successfully deployed to a gateway. +// This event is sent when an API is successfully deployed to a gateway. type APIDeploymentEvent struct { // ApiId identifies the deployed API ApiId string `json:"apiId"` - // RevisionID identifies the specific API revision deployed - RevisionID string `json:"revisionId"` + // DeploymentID identifies the specific deployment artifact + DeploymentID string `json:"deploymentId"` // Vhost specifies the virtual host where the API is deployed Vhost string `json:"vhost"` diff --git a/platform-api/src/internal/repository/api.go b/platform-api/src/internal/repository/api.go index f10b0f543..69bf2a767 100644 --- a/platform-api/src/internal/repository/api.go +++ b/platform-api/src/internal/repository/api.go @@ -63,17 +63,16 @@ func (r *APIRepo) CreateAPI(api *model.API) error { // Insert main API record apiQuery := ` INSERT INTO apis (uuid, handle, name, description, context, version, provider, - project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, is_revision, - revisioned_api_id, revision_id, type, transport, security_enabled, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, + type, transport, security_enabled, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` securityEnabled := api.Security != nil && api.Security.Enabled _, err = tx.Exec(apiQuery, api.ID, api.Handle, api.Name, api.Description, api.Context, api.Version, api.Provider, api.ProjectID, api.OrganizationID, api.LifeCycleStatus, - api.HasThumbnail, api.IsDefaultVersion, api.IsRevision, api.RevisionedAPIID, - api.RevisionID, api.Type, string(transportJSON), securityEnabled, api.CreatedAt, api.UpdatedAt) + api.HasThumbnail, api.IsDefaultVersion, api.Type, string(transportJSON), securityEnabled, api.CreatedAt, api.UpdatedAt) if err != nil { return err } @@ -122,8 +121,8 @@ func (r *APIRepo) GetAPIByUUID(apiUUID, orgUUID string) (*model.API, error) { query := ` SELECT uuid, handle, name, description, context, version, provider, - project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, is_revision, - revisioned_api_id, revision_id, type, transport, security_enabled, created_at, updated_at + project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, + type, transport, security_enabled, created_at, updated_at FROM apis WHERE uuid = ? and organization_uuid = ? ` @@ -132,8 +131,7 @@ func (r *APIRepo) GetAPIByUUID(apiUUID, orgUUID string) (*model.API, error) { err := r.db.QueryRow(query, apiUUID, orgUUID).Scan( &api.ID, &api.Handle, &api.Name, &api.Description, &api.Context, &api.Version, &api.Provider, &api.ProjectID, &api.OrganizationID, &api.LifeCycleStatus, - &api.HasThumbnail, &api.IsDefaultVersion, &api.IsRevision, - &api.RevisionedAPIID, &api.RevisionID, &api.Type, &transportJSON, + &api.HasThumbnail, &api.IsDefaultVersion, &api.Type, &transportJSON, &securityEnabled, &api.CreatedAt, &api.UpdatedAt) if err != nil { @@ -179,8 +177,8 @@ func (r *APIRepo) GetAPIMetadataByHandle(handle, orgUUID string) (*model.APIMeta func (r *APIRepo) GetAPIsByProjectUUID(projectUUID, orgUUID string) ([]*model.API, error) { query := ` SELECT uuid, handle, name, description, context, version, provider, - project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, is_revision, - revisioned_api_id, revision_id, type, transport, security_enabled, created_at, updated_at + project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, + type, transport, security_enabled, created_at, updated_at FROM apis WHERE project_uuid = ? AND organization_uuid = ? ORDER BY created_at DESC ` @@ -199,8 +197,7 @@ func (r *APIRepo) GetAPIsByProjectUUID(projectUUID, orgUUID string) ([]*model.AP err := rows.Scan(&api.ID, &api.Handle, &api.Name, &api.Description, &api.Context, &api.Version, &api.Provider, &api.ProjectID, &api.OrganizationID, &api.LifeCycleStatus, &api.HasThumbnail, &api.IsDefaultVersion, - &api.IsRevision, &api.RevisionedAPIID, &api.RevisionID, &api.Type, - &transportJSON, &securityEnabled, &api.CreatedAt, &api.UpdatedAt) + &api.Type, &transportJSON, &securityEnabled, &api.CreatedAt, &api.UpdatedAt) if err != nil { return nil, err } @@ -230,8 +227,8 @@ func (r *APIRepo) GetAPIsByOrganizationUUID(orgUUID string, projectUUID *string) // Filter by specific project within the organization query = ` SELECT uuid, handle, name, description, context, version, provider, - project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, is_revision, - revisioned_api_id, revision_id, type, transport, security_enabled, created_at, updated_at + project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, + type, transport, security_enabled, created_at, updated_at FROM apis WHERE organization_uuid = ? AND project_uuid = ? ORDER BY created_at DESC @@ -241,8 +238,8 @@ func (r *APIRepo) GetAPIsByOrganizationUUID(orgUUID string, projectUUID *string) // Get all APIs for the organization query = ` SELECT uuid, handle, name, description, context, version, provider, - project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, is_revision, - revisioned_api_id, revision_id, type, transport, security_enabled, created_at, updated_at + project_uuid, organization_uuid, lifecycle_status, has_thumbnail, is_default_version, + type, transport, security_enabled, created_at, updated_at FROM apis WHERE organization_uuid = ? ORDER BY created_at DESC @@ -265,8 +262,7 @@ func (r *APIRepo) GetAPIsByOrganizationUUID(orgUUID string, projectUUID *string) err := rows.Scan(&api.ID, &api.Handle, &api.Name, &api.Description, &api.Context, &api.Version, &api.Provider, &api.ProjectID, &api.OrganizationID, &api.LifeCycleStatus, &api.HasThumbnail, &api.IsDefaultVersion, - &api.IsRevision, &api.RevisionedAPIID, &api.RevisionID, &api.Type, - &transportJSON, &securityEnabled, &api.CreatedAt, &api.UpdatedAt) + &api.Type, &transportJSON, &securityEnabled, &api.CreatedAt, &api.UpdatedAt) if err != nil { return nil, err } @@ -369,14 +365,12 @@ func (r *APIRepo) UpdateAPI(api *model.API) error { query := ` UPDATE apis SET description = ?, provider = ?, lifecycle_status = ?, has_thumbnail = ?, - is_default_version = ?, is_revision = ?, revisioned_api_id = ?, - revision_id = ?, type = ?, transport = ?, security_enabled = ?, updated_at = ? + is_default_version = ?, type = ?, transport = ?, security_enabled = ?, updated_at = ? WHERE uuid = ? ` _, err = tx.Exec(query, api.Description, api.Provider, api.LifeCycleStatus, - api.HasThumbnail, api.IsDefaultVersion, api.IsRevision, - api.RevisionedAPIID, api.RevisionID, api.Type, string(transportJSON), + api.HasThumbnail, api.IsDefaultVersion, api.Type, string(transportJSON), securityEnabled, api.UpdatedAt, api.ID) if err != nil { return err @@ -970,55 +964,248 @@ func (r *APIRepo) deleteAPIConfigurations(tx *sql.Tx, apiId string) error { // CreateDeployment inserts a new API deployment record func (r *APIRepo) CreateDeployment(deployment *model.APIDeployment) error { + // Generate UUID for deployment if not already set + if deployment.DeploymentID == "" { + deployment.DeploymentID = uuid.New().String() + } deployment.CreatedAt = time.Now() query := ` - INSERT INTO api_deployments (api_uuid, organization_uuid, gateway_uuid, created_at) - VALUES (?, ?, ?, ?) + INSERT INTO api_deployments (deployment_id, api_uuid, organization_uuid, gateway_uuid, status, base_deployment_id, content, metadata, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ` + + var baseDeploymentID interface{} + if deployment.BaseDeploymentID != nil { + baseDeploymentID = *deployment.BaseDeploymentID + } else { + baseDeploymentID = nil + } + + var metadataJSON string + if deployment.Metadata != nil { + metadataBytes, _ := json.Marshal(deployment.Metadata) + metadataJSON = string(metadataBytes) + } + + _, err := r.db.Exec(query, deployment.DeploymentID, deployment.ApiID, deployment.OrganizationID, + deployment.GatewayID, deployment.Status, baseDeploymentID, deployment.Content, metadataJSON, deployment.CreatedAt) + + return err +} + +// GetDeploymentsByAPIUUID retrieves all deployment records for an API +func (r *APIRepo) GetDeploymentsByAPIUUID(apiUUID, orgUUID string, gatewayID, status *string) ([]*model.APIDeployment, error) { + var query string + var args []interface{} + + if gatewayID != nil && status != nil { + query = ` + SELECT deployment_id, api_uuid, organization_uuid, gateway_uuid, status, base_deployment_id, content, metadata, created_at + FROM api_deployments + WHERE api_uuid = ? AND organization_uuid = ? AND gateway_uuid = ? AND status = ? + ORDER BY created_at DESC + ` + args = []interface{}{apiUUID, orgUUID, *gatewayID, *status} + } else if gatewayID != nil { + query = ` + SELECT deployment_id, api_uuid, organization_uuid, gateway_uuid, status, base_deployment_id, content, metadata, created_at + FROM api_deployments + WHERE api_uuid = ? AND organization_uuid = ? AND gateway_uuid = ? + ORDER BY created_at DESC + ` + args = []interface{}{apiUUID, orgUUID, *gatewayID} + } else if status != nil { + query = ` + SELECT deployment_id, api_uuid, organization_uuid, gateway_uuid, status, base_deployment_id, content, metadata, created_at + FROM api_deployments + WHERE api_uuid = ? AND organization_uuid = ? AND status = ? + ORDER BY created_at DESC + ` + args = []interface{}{apiUUID, orgUUID, *status} + } else { + query = ` + SELECT deployment_id, api_uuid, organization_uuid, gateway_uuid, status, base_deployment_id, content, metadata, created_at + FROM api_deployments + WHERE api_uuid = ? AND organization_uuid = ? + ORDER BY created_at DESC + ` + args = []interface{}{apiUUID, orgUUID} + } + + rows, err := r.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var deployments []*model.APIDeployment + for rows.Next() { + deployment := &model.APIDeployment{} + var baseDeploymentID sql.NullString + var metadataJSON string + + err := rows.Scan(&deployment.DeploymentID, &deployment.ApiID, &deployment.OrganizationID, + &deployment.GatewayID, &deployment.Status, &baseDeploymentID, &deployment.Content, &metadataJSON, &deployment.CreatedAt) + if err != nil { + return nil, err + } + + if baseDeploymentID.Valid { + deployment.BaseDeploymentID = &baseDeploymentID.String + } + + if metadataJSON != "" { + var metadata map[string]interface{} + if err := json.Unmarshal([]byte(metadataJSON), &metadata); err == nil { + deployment.Metadata = metadata + } + } + + deployments = append(deployments, deployment) + } + + return deployments, rows.Err() +} + +// GetDeploymentByID retrieves a specific deployment by deployment ID for a given API +func (r *APIRepo) GetDeploymentByID(deploymentID, apiID, orgID string) (*model.APIDeployment, error) { + deployment := &model.APIDeployment{} + + query := ` + SELECT deployment_id, api_uuid, organization_uuid, gateway_uuid, status, base_deployment_id, content, metadata, created_at + FROM api_deployments + WHERE deployment_id = ? AND api_uuid = ? AND organization_uuid = ? ` - result, err := r.db.Exec(query, deployment.ApiID, deployment.OrganizationID, - deployment.GatewayID, deployment.CreatedAt) + var baseDeploymentID sql.NullString + var metadataJSON string + + err := r.db.QueryRow(query, deploymentID, apiID, orgID).Scan( + &deployment.DeploymentID, &deployment.ApiID, &deployment.OrganizationID, + &deployment.GatewayID, &deployment.Status, &baseDeploymentID, &deployment.Content, &metadataJSON, &deployment.CreatedAt) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + + if baseDeploymentID.Valid { + deployment.BaseDeploymentID = &baseDeploymentID.String + } + + if metadataJSON != "" { + var metadata map[string]interface{} + if err := json.Unmarshal([]byte(metadataJSON), &metadata); err == nil { + deployment.Metadata = metadata + } + } + + return deployment, nil +} + +// GetDeploymentContent retrieves the content blob for a deployment +func (r *APIRepo) GetDeploymentContent(deploymentID, apiID, orgID string) ([]byte, error) { + var content []byte + + query := `SELECT content FROM api_deployments WHERE deployment_id = ? AND api_uuid = ? AND organization_uuid = ?` + + err := r.db.QueryRow(query, deploymentID, apiID, orgID).Scan(&content) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, errors.New("deployment not found") + } + return nil, err + } + + return content, nil +} + +// UpdateDeploymentStatus updates the status of a deployment +func (r *APIRepo) UpdateDeploymentStatus(deploymentID, apiID, status, orgID string) error { + query := ` + UPDATE api_deployments + SET status = ? + WHERE deployment_id = ? AND api_uuid = ? AND organization_uuid = ? + ` + + result, err := r.db.Exec(query, status, deploymentID, apiID, orgID) if err != nil { return err } - id, err := result.LastInsertId() + rowsAffected, err := result.RowsAffected() if err != nil { return err } - deployment.ID = int(id) + if rowsAffected == 0 { + return errors.New("deployment not found") + } + return nil } -// GetDeploymentsByAPIUUID retrieves all deployment records for an API -func (r *APIRepo) GetDeploymentsByAPIUUID(apiUUID, orgUUID string) ([]*model.APIDeployment, error) { +// DeleteDeployment deletes a deployment record +func (r *APIRepo) DeleteDeployment(deploymentID, apiID, orgID string) error { + query := `DELETE FROM api_deployments WHERE deployment_id = ? AND api_uuid = ? AND organization_uuid = ?` + + result, err := r.db.Exec(query, deploymentID, apiID, orgID) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + + if rowsAffected == 0 { + return errors.New("deployment not found") + } + + return nil +} + +// GetActiveDeploymentByGateway retrieves the currently deployed artifact for an API on a gateway +func (r *APIRepo) GetActiveDeploymentByGateway(apiUUID, gatewayID, orgID string) (*model.APIDeployment, error) { + deployment := &model.APIDeployment{} + query := ` - SELECT id, api_uuid, organization_uuid, gateway_uuid, created_at + SELECT deployment_id, api_uuid, organization_uuid, gateway_uuid, status, base_deployment_id, content, metadata, created_at FROM api_deployments - WHERE api_uuid = ? AND organization_uuid = ? - ORDER BY created_at DESC + WHERE api_uuid = ? AND gateway_uuid = ? AND organization_uuid = ? AND status = 'DEPLOYED' + LIMIT 1 ` - rows, err := r.db.Query(query, apiUUID, orgUUID) + var baseDeploymentID sql.NullString + var metadataJSON string + + err := r.db.QueryRow(query, apiUUID, gatewayID, orgID).Scan( + &deployment.DeploymentID, &deployment.ApiID, &deployment.OrganizationID, + &deployment.GatewayID, &deployment.Status, &baseDeploymentID, &deployment.Content, &metadataJSON, &deployment.CreatedAt) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } return nil, err } - defer rows.Close() - var deployments []*model.APIDeployment - for rows.Next() { - var deployment model.APIDeployment - err := rows.Scan(&deployment.ID, &deployment.ApiID, &deployment.OrganizationID, - &deployment.GatewayID, &deployment.CreatedAt) - if err != nil { - return nil, err + if baseDeploymentID.Valid { + deployment.BaseDeploymentID = &baseDeploymentID.String + } + + if metadataJSON != "" { + var metadata map[string]interface{} + if err := json.Unmarshal([]byte(metadataJSON), &metadata); err == nil { + deployment.Metadata = metadata } - deployments = append(deployments, &deployment) } - return deployments, rows.Err() + return deployment, nil } // CreateAPIAssociation creates an association between an API and resource (e.g., gateway or dev portal) @@ -1098,10 +1285,10 @@ func (r *APIRepo) GetAPIGatewaysWithDetails(apiUUID, orgUUID string) ([]*model.A g.updated_at, aa.created_at as associated_at, aa.updated_at as association_updated_at, - CASE WHEN ad.id IS NOT NULL THEN 1 ELSE 0 END as is_deployed, + CASE WHEN ad.deployment_id IS NOT NULL THEN 1 ELSE 0 END as is_deployed, ad.created_at as deployed_at FROM gateways g - INNER JOIN api_associations aa ON g.uuid = aa.resource_uuid AND association_type = 'gateway' + INNER JOIN api_associations aa ON g.uuid = aa.resource_uuid AND aa.association_type = 'gateway' LEFT JOIN api_deployments ad ON g.uuid = ad.gateway_uuid AND ad.api_uuid = ? WHERE aa.api_uuid = ? AND g.organization_uuid = ? ORDER BY aa.created_at DESC @@ -1165,3 +1352,19 @@ func (r *APIRepo) CheckAPIExistsByNameAndVersionInOrganization(name, version, or return count > 0, nil } + +// CountDeploymentsByAPIAndGateway returns the number of deployments for a specific API-Gateway combination +func (r *APIRepo) CountDeploymentsByAPIAndGateway(apiID, gatewayID, orgID string) (int, error) { + var count int + err := r.db.QueryRow(` + SELECT COUNT(*) + FROM api_deployments + WHERE api_uuid = ? AND gateway_uuid = ? AND organization_uuid = ? + `, apiID, gatewayID, orgID).Scan(&count) + + if err != nil { + return 0, err + } + + return count, nil +} diff --git a/platform-api/src/internal/repository/interfaces.go b/platform-api/src/internal/repository/interfaces.go index 6837632b6..c5c66b58d 100644 --- a/platform-api/src/internal/repository/interfaces.go +++ b/platform-api/src/internal/repository/interfaces.go @@ -54,8 +54,16 @@ type APIRepository interface { GetDeployedAPIsByGatewayUUID(gatewayUUID, orgUUID string) ([]*model.API, error) UpdateAPI(api *model.API) error DeleteAPI(apiUUID, orgUUID string) error + + // Deployment artifact methods (immutable deployments) CreateDeployment(deployment *model.APIDeployment) error - GetDeploymentsByAPIUUID(apiUUID, orgUUID string) ([]*model.APIDeployment, error) + GetDeploymentByID(deploymentID, apiUUID, orgUUID string) (*model.APIDeployment, error) + GetDeploymentsByAPIUUID(apiUUID, orgUUID string, gatewayID *string, status *string) ([]*model.APIDeployment, error) + GetDeploymentContent(deploymentID, apiUUID, orgUUID string) ([]byte, error) + UpdateDeploymentStatus(deploymentID, apiUUID, status, orgUUID string) error + DeleteDeployment(deploymentID, apiUUID, orgUUID string) error + GetActiveDeploymentByGateway(apiUUID, gatewayID, orgUUID string) (*model.APIDeployment, error) + CountDeploymentsByAPIAndGateway(apiUUID, gatewayID, orgUUID string) (int, error) // API-Gateway association methods GetAPIGatewaysWithDetails(apiUUID, orgUUID string) ([]*model.APIGatewayWithDetails, error) diff --git a/platform-api/src/internal/server/server.go b/platform-api/src/internal/server/server.go index cd3295dd2..1f86c7839 100644 --- a/platform-api/src/internal/server/server.go +++ b/platform-api/src/internal/server/server.go @@ -100,6 +100,7 @@ func StartPlatformAPIServer(cfg *config.Server) (*Server, error) { gatewayService := service.NewGatewayService(gatewayRepo, orgRepo, apiRepo) internalGatewayService := service.NewGatewayInternalAPIService(apiRepo, gatewayRepo, orgRepo, projectRepo, upstreamService) gitService := service.NewGitService() + deploymentService := service.NewDeploymentService(apiRepo, gatewayRepo, backendServiceRepo, orgRepo, gatewayEventsService, apiUtil, cfg) // Initialize handlers orgHandler := handler.NewOrganizationHandler(orgService) @@ -110,6 +111,7 @@ func StartPlatformAPIServer(cfg *config.Server) (*Server, error) { wsHandler := handler.NewWebSocketHandler(wsManager, gatewayService, cfg.WebSocket.RateLimitPerMin) internalGatewayHandler := handler.NewGatewayInternalAPIHandler(gatewayService, internalGatewayService) gitHandler := handler.NewGitHandler(gitService) + deploymentHandler := handler.NewDeploymentHandler(deploymentService) // Setup router router := gin.Default() @@ -140,6 +142,7 @@ func StartPlatformAPIServer(cfg *config.Server) (*Server, error) { wsHandler.RegisterRoutes(router) internalGatewayHandler.RegisterRoutes(router) gitHandler.RegisterRoutes(router) + deploymentHandler.RegisterRoutes(router) log.Printf("[INFO] WebSocket manager initialized: maxConnections=%d heartbeatTimeout=%ds rateLimitPerMin=%d", cfg.WebSocket.MaxConnections, cfg.WebSocket.ConnectionTimeout, cfg.WebSocket.RateLimitPerMin) diff --git a/platform-api/src/internal/service/api.go b/platform-api/src/internal/service/api.go index b23d299ea..e2d3a66ea 100644 --- a/platform-api/src/internal/service/api.go +++ b/platform-api/src/internal/service/api.go @@ -136,9 +136,6 @@ func (s *APIService) CreateAPI(req *CreateAPIRequest, orgUUID string) (*dto.API, LifeCycleStatus: req.LifeCycleStatus, HasThumbnail: req.HasThumbnail, IsDefaultVersion: req.IsDefaultVersion, - IsRevision: req.IsRevision, - RevisionedAPIID: req.RevisionedAPIID, - RevisionID: req.RevisionID, Type: req.Type, Transport: req.Transport, MTLS: req.MTLS, @@ -382,16 +379,6 @@ func (s *APIService) GetAPIGatewaysByHandle(handle, orgId string) (*dto.APIGatew return s.GetAPIGateways(apiUUID, orgId) } -// DeployAPIRevisionByHandle deploys an API revision identified by handle -func (s *APIService) DeployAPIRevisionByHandle(handle string, revisionID string, - deploymentRequests []dto.APIRevisionDeployment, orgId string) ([]*dto.APIRevisionDeployment, error) { - apiUUID, err := s.getAPIUUIDByHandle(handle, orgId) - if err != nil { - return nil, err - } - return s.DeployAPIRevision(apiUUID, revisionID, deploymentRequests, orgId) -} - // PublishAPIToDevPortalByHandle publishes an API identified by handle to a DevPortal func (s *APIService) PublishAPIToDevPortalByHandle(handle string, req *dto.PublishToDevPortalRequest, orgID string) error { apiUUID, err := s.getAPIUUIDByHandle(handle, orgID) @@ -419,120 +406,6 @@ func (s *APIService) GetAPIPublicationsByHandle(handle, orgID string) (*dto.APID return s.GetAPIPublications(apiUUID, orgID) } -// DeployAPIRevision deploys an API revision and generates deployment YAML -func (s *APIService) DeployAPIRevision(apiUUID string, revisionID string, - deploymentRequests []dto.APIRevisionDeployment, orgUUID string) ([]*dto.APIRevisionDeployment, error) { - if apiUUID == "" { - return nil, errors.New("api id is required") - } - - // Get the API from database - apiModel, err := s.apiRepo.GetAPIByUUID(apiUUID, orgUUID) - if err != nil { - return nil, err - } - if apiModel == nil { - return nil, constants.ErrAPINotFound - } - if apiModel.OrganizationID != orgUUID { - return nil, constants.ErrAPINotFound - } - - // Get existing associations to check which gateways need association - existingAssociations, err := s.apiRepo.GetAPIAssociations(apiUUID, constants.AssociationTypeGateway, orgUUID) - if err != nil { - return nil, fmt.Errorf("failed to check existing API-gateway associations: %w", err) - } - - // Create a map of existing gateway associations for quick lookup - existingGatewayIds := make(map[string]bool) - for _, assoc := range existingAssociations { - existingGatewayIds[assoc.ResourceID] = true - } - - // Process deployment requests and create deployment responses - var deployments []*dto.APIRevisionDeployment - currentTime := time.Now().Format(time.RFC3339) - - for _, deploymentReq := range deploymentRequests { - // Validate deployment request - if err := s.validateDeploymentRequest(&deploymentReq, apiModel, orgUUID); err != nil { - return nil, fmt.Errorf("invalid api deployment: %w", err) - } - - // If gateway is not associated with the API, create the association - if !existingGatewayIds[deploymentReq.GatewayID] { - log.Printf("[INFO] Creating API-gateway association: apiUUID=%s gatewayId=%s", - apiUUID, deploymentReq.GatewayID) - - association := &model.APIAssociation{ - ApiID: apiUUID, - OrganizationID: orgUUID, - ResourceID: deploymentReq.GatewayID, - AssociationType: constants.AssociationTypeGateway, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - if err := s.apiRepo.CreateAPIAssociation(association); err != nil { - return nil, fmt.Errorf("failed to create API-gateway association for gateway %s: %w", - deploymentReq.GatewayID, err) - } - - // Add to the map to avoid duplicate creation in the same request - existingGatewayIds[deploymentReq.GatewayID] = true - log.Printf("[INFO] Created API-gateway association: apiUUID=%s gatewayId=%s associationId=%d", - apiUUID, deploymentReq.GatewayID, association.ID) - } - - deployment := &dto.APIRevisionDeployment{ - RevisionId: revisionID, // Optional, can be empty - GatewayID: deploymentReq.GatewayID, - Status: "CREATED", // Default status for new deployments - VHost: deploymentReq.VHost, - DisplayOnDevportal: deploymentReq.DisplayOnDevportal, - DeployedTime: ¤tTime, - SuccessDeployedTime: ¤tTime, - } - - deployments = append(deployments, deployment) - - // Create deployment record in the database - deploymentRecord := &model.APIDeployment{ - ApiID: apiUUID, - OrganizationID: orgUUID, - GatewayID: deployment.GatewayID, - } - - if err := s.apiRepo.CreateDeployment(deploymentRecord); err != nil { - log.Printf("[ERROR] Failed to create deployment record: apiUUID=%s gatewayID=%s error=%v", - apiUUID, deployment.GatewayID, err) - } else { - log.Printf("[INFO] Created deployment record: apiUUID=%s gatewayID=%s deploymentId=%d", - apiUUID, deployment.GatewayID, deploymentRecord.ID) - } - - // Send deployment event to gateway via WebSocket - deploymentEvent := &model.APIDeploymentEvent{ - ApiId: apiUUID, - RevisionID: revisionID, - Vhost: deployment.VHost, - Environment: "production", // Default environment - } - - // Broadcast deployment event to target gateway - if s.gatewayEventsService != nil { - if err := s.gatewayEventsService.BroadcastDeploymentEvent(deployment.GatewayID, deploymentEvent); err != nil { - log.Printf("[WARN] Failed to broadcast deployment event: apiUUID=%s gatewayID=%s error=%v", - apiUUID, deployment.GatewayID, err) - // Continue execution - event delivery failure doesn't fail the deployment - } - } - } - - return deployments, nil -} - // AddGatewaysToAPI associates multiple gateways with an API func (s *APIService) AddGatewaysToAPI(apiUUID string, gatewayIds []string, orgUUID string) (*dto.APIGatewayListResponse, error) { // Validate that the API exists and belongs to the organization @@ -664,38 +537,6 @@ func (s *APIService) GetAPIGateways(apiUUID, orgUUID string) (*dto.APIGatewayLis return listResponse, nil } -// validateDeploymentRequest validates the deployment request -func (s *APIService) validateDeploymentRequest(req *dto.APIRevisionDeployment, api *model.API, orgUUID string) error { - if req.GatewayID == "" { - return errors.New("gateway Id is required") - } - if req.VHost == "" { - return errors.New("vhost is required") - } - // TODO - vHost validation - gateway, err := s.gatewayRepo.GetByUUID(req.GatewayID) - if err != nil { - return fmt.Errorf("failed to get gateway: %w", err) - } - if gateway == nil { - return fmt.Errorf("failed to get gateway: %w", err) - } - if gateway.OrganizationID != orgUUID { - return fmt.Errorf("failed to get gateway: %w", err) - } - - // Validate that the API has at least one backend service attached - backendServices, err := s.backendServiceRepo.GetBackendServicesByAPIID(api.ID) - if err != nil { - return fmt.Errorf("failed to get backend services for API: %w", err) - } - if len(backendServices) == 0 { - return errors.New("API must have at least one backend service attached before deployment") - } - - return nil -} - // createDefaultDevPortalAssociation creates an association between the API and the default DevPortal func (s *APIService) createDefaultDevPortalAssociation(apiId, orgId string) error { // Get default DevPortal for the organization @@ -825,15 +666,6 @@ func (s *APIService) applyAPIUpdates(existingAPIModel *model.API, req *UpdateAPI if req.IsDefaultVersion != nil { existingAPI.IsDefaultVersion = *req.IsDefaultVersion } - if req.IsRevision != nil { - existingAPI.IsRevision = *req.IsRevision - } - if req.RevisionedAPIID != nil { - existingAPI.RevisionedAPIID = *req.RevisionedAPIID - } - if req.RevisionID != nil { - existingAPI.RevisionID = *req.RevisionID - } if req.Type != nil { existingAPI.Type = *req.Type } @@ -996,9 +828,6 @@ type CreateAPIRequest struct { LifeCycleStatus string `json:"lifeCycleStatus,omitempty"` HasThumbnail bool `json:"hasThumbnail,omitempty"` IsDefaultVersion bool `json:"isDefaultVersion,omitempty"` - IsRevision bool `json:"isRevision,omitempty"` - RevisionedAPIID string `json:"revisionedApiId,omitempty"` - RevisionID int `json:"revisionId,omitempty"` Type string `json:"type,omitempty"` Transport []string `json:"transport,omitempty"` MTLS *dto.MTLSConfig `json:"mtls,omitempty"` @@ -1018,9 +847,6 @@ type UpdateAPIRequest struct { LifeCycleStatus *string `json:"lifeCycleStatus,omitempty"` HasThumbnail *bool `json:"hasThumbnail,omitempty"` IsDefaultVersion *bool `json:"isDefaultVersion,omitempty"` - IsRevision *bool `json:"isRevision,omitempty"` - RevisionedAPIID *string `json:"revisionedApiId,omitempty"` - RevisionID *int `json:"revisionId,omitempty"` Type *string `json:"type,omitempty"` Transport *[]string `json:"transport,omitempty"` MTLS *dto.MTLSConfig `json:"mtls,omitempty"` @@ -1140,9 +966,6 @@ func (s *APIService) ImportAPIProject(req *dto.ImportAPIProjectRequest, orgUUID LifeCycleStatus: apiData.LifeCycleStatus, HasThumbnail: apiData.HasThumbnail, IsDefaultVersion: apiData.IsDefaultVersion, - IsRevision: apiData.IsRevision, - RevisionedAPIID: apiData.RevisionedAPIID, - RevisionID: apiData.RevisionID, Type: apiData.Type, Transport: apiData.Transport, MTLS: apiData.MTLS, @@ -1198,14 +1021,6 @@ func (s *APIService) mergeAPIData(artifact *dto.APIYAMLData, userAPIData *dto.AP // Handle boolean fields apiDTO.HasThumbnail = userAPIData.HasThumbnail apiDTO.IsDefaultVersion = userAPIData.IsDefaultVersion - apiDTO.IsRevision = userAPIData.IsRevision - - if userAPIData.RevisionedAPIID != "" { - apiDTO.RevisionedAPIID = userAPIData.RevisionedAPIID - } - if userAPIData.RevisionID != 0 { - apiDTO.RevisionID = userAPIData.RevisionID - } return apiDTO } diff --git a/platform-api/src/internal/service/deployment.go b/platform-api/src/internal/service/deployment.go new file mode 100644 index 000000000..5ec0c9dd6 --- /dev/null +++ b/platform-api/src/internal/service/deployment.go @@ -0,0 +1,569 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package service + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "time" + + "platform-api/src/config" + "platform-api/src/internal/constants" + "platform-api/src/internal/dto" + "platform-api/src/internal/model" + "platform-api/src/internal/repository" + "platform-api/src/internal/utils" + + "github.com/google/uuid" +) + +// DeploymentService handles business logic for API deployment operations +type DeploymentService struct { + apiRepo repository.APIRepository + gatewayRepo repository.GatewayRepository + backendServiceRepo repository.BackendServiceRepository + orgRepo repository.OrganizationRepository + gatewayEventsService *GatewayEventsService + apiUtil *utils.APIUtil + cfg *config.Server +} + +// NewDeploymentService creates a new deployment service +func NewDeploymentService( + apiRepo repository.APIRepository, + gatewayRepo repository.GatewayRepository, + backendServiceRepo repository.BackendServiceRepository, + orgRepo repository.OrganizationRepository, + gatewayEventsService *GatewayEventsService, + apiUtil *utils.APIUtil, + cfg *config.Server, +) *DeploymentService { + return &DeploymentService{ + apiRepo: apiRepo, + gatewayRepo: gatewayRepo, + backendServiceRepo: backendServiceRepo, + orgRepo: orgRepo, + gatewayEventsService: gatewayEventsService, + apiUtil: apiUtil, + cfg: cfg, + } +} + +// DeployAPI creates a new immutable deployment artifact and deploys it to a gateway +func (s *DeploymentService) DeployAPI(apiUUID string, req *dto.DeployAPIRequest, orgUUID string) (*dto.DeploymentResponse, error) { + // Validate request + if req.Base == "" { + return nil, errors.New("base is required") + } + if req.GatewayID == "" { + return nil, errors.New("gatewayId is required") + } + + // Validate gateway exists and belongs to organization + gateway, err := s.gatewayRepo.GetByUUID(req.GatewayID) + if err != nil { + return nil, fmt.Errorf("failed to get gateway: %w", err) + } + if gateway == nil || gateway.OrganizationID != orgUUID { + return nil, constants.ErrGatewayNotFound + } + + // Get API + apiModel, err := s.apiRepo.GetAPIByUUID(apiUUID, orgUUID) + if err != nil { + return nil, err + } + if apiModel == nil { + return nil, constants.ErrAPINotFound + } + + // Check deployment limits + apiDeploymentCount, err := s.apiRepo.CountDeploymentsByAPIAndGateway(apiUUID, req.GatewayID, orgUUID) + if err != nil { + return nil, fmt.Errorf("failed to check deployment count: %w", err) + } + if apiDeploymentCount >= s.cfg.Deployments.MaxPerAPIGateway { + return nil, fmt.Errorf("deployment limit exceeded: maximum %d deployments allowed per API-Gateway combination", s.cfg.Deployments.MaxPerAPIGateway) + } + + // Validate API has backend services attached + backendServices, err := s.backendServiceRepo.GetBackendServicesByAPIID(apiUUID) + if err != nil { + return nil, fmt.Errorf("failed to get backend services: %w", err) + } + if len(backendServices) == 0 { + return nil, errors.New("API must have at least one backend service attached before deployment") + } + + var baseDeploymentID *string + var apiContent *dto.API + + // Determine the source: "current" or existing deployment + if req.Base == "current" { + // Use current API state + apiContent = s.apiUtil.ModelToDTO(apiModel) + } else { + // Use existing deployment as base + baseDeployment, err := s.apiRepo.GetDeploymentByID(req.Base, apiUUID, orgUUID) + if err != nil { + return nil, fmt.Errorf("failed to get base deployment: %w", err) + } + if baseDeployment == nil { + return nil, errors.New("base deployment not found") + } + + // Deserialize content from base deployment + content, err := s.apiRepo.GetDeploymentContent(req.Base, apiUUID, orgUUID) + if err != nil { + return nil, fmt.Errorf("failed to get deployment content: %w", err) + } + + var deploymentContent dto.API + if err := json.Unmarshal(content, &deploymentContent); err != nil { + return nil, fmt.Errorf("failed to unmarshal deployment content: %w", err) + } + + apiContent = &deploymentContent + baseDeploymentID = &req.Base + } + + // Create immutable deployment artifact content (serialize API + metadata) + contentBytes, err := json.Marshal(apiContent) + if err != nil { + return nil, fmt.Errorf("failed to marshal API content: %w", err) + } + + // Generate deployment ID + deploymentID := uuid.New().String() + + // Check if there's an existing active deployment on this gateway + existingDeployment, err := s.apiRepo.GetActiveDeploymentByGateway(apiUUID, req.GatewayID, orgUUID) + if err != nil { + return nil, fmt.Errorf("failed to check existing deployments: %w", err) + } + + // If exists, undeploy it + if existingDeployment != nil { + if err := s.apiRepo.UpdateDeploymentStatus(existingDeployment.DeploymentID, apiUUID, "UNDEPLOYED", orgUUID); err != nil { + log.Printf("[WARN] Failed to undeploy existing deployment %s: %v", existingDeployment.DeploymentID, err) + } + } + + // Create new deployment record + deployment := &model.APIDeployment{ + DeploymentID: deploymentID, + ApiID: apiUUID, + OrganizationID: orgUUID, + GatewayID: req.GatewayID, + Status: "DEPLOYED", + BaseDeploymentID: baseDeploymentID, + Content: contentBytes, + Metadata: req.Metadata, + CreatedAt: time.Now(), + } + + if err := s.apiRepo.CreateDeployment(deployment); err != nil { + return nil, fmt.Errorf("failed to create deployment: %w", err) + } + + // Ensure API-Gateway association exists + if err := s.ensureAPIGatewayAssociation(apiUUID, req.GatewayID, orgUUID); err != nil { + log.Printf("[WARN] Failed to ensure API-gateway association: %v", err) + } + + // Send deployment event to gateway + if s.gatewayEventsService != nil { + deploymentEvent := &model.APIDeploymentEvent{ + ApiId: apiUUID, + DeploymentID: deploymentID, + Vhost: gateway.Vhost, + Environment: "production", + } + + if err := s.gatewayEventsService.BroadcastDeploymentEvent(req.GatewayID, deploymentEvent); err != nil { + log.Printf("[WARN] Failed to broadcast deployment event: %v", err) + } + } + + // Return deployment response + return &dto.DeploymentResponse{ + DeploymentID: deployment.DeploymentID, + ApiID: deployment.ApiID, + GatewayID: deployment.GatewayID, + Status: deployment.Status, + BaseDeploymentID: deployment.BaseDeploymentID, + Metadata: deployment.Metadata, + CreatedAt: deployment.CreatedAt, + }, nil +} + +// RedeployDeployment re-deploys an existing undeployed deployment artifact +func (s *DeploymentService) RedeployDeployment(apiUUID, deploymentID, orgUUID string) (*dto.DeploymentResponse, error) { + // Get the deployment + deployment, err := s.apiRepo.GetDeploymentByID(deploymentID, apiUUID, orgUUID) + if err != nil { + return nil, err + } + if deployment == nil { + return nil, errors.New("deployment not found") + } + if deployment.Status == "DEPLOYED" { + return nil, errors.New("deployment is already active") + } + + // Check if there's an existing active deployment on this gateway + existingDeployment, err := s.apiRepo.GetActiveDeploymentByGateway(apiUUID, deployment.GatewayID, orgUUID) + if err != nil { + return nil, fmt.Errorf("failed to check existing deployments: %w", err) + } + + // If exists, undeploy it + if existingDeployment != nil { + if err := s.apiRepo.UpdateDeploymentStatus(existingDeployment.DeploymentID, apiUUID, "UNDEPLOYED", orgUUID); err != nil { + log.Printf("[WARN] Failed to undeploy existing deployment %s: %v", existingDeployment.DeploymentID, err) + } + } + + // Update status to DEPLOYED + if err := s.apiRepo.UpdateDeploymentStatus(deploymentID, apiUUID, "DEPLOYED", orgUUID); err != nil { + return nil, fmt.Errorf("failed to update deployment status: %w", err) + } + + // Send deployment event to gateway + if s.gatewayEventsService != nil { + gateway, _ := s.gatewayRepo.GetByUUID(deployment.GatewayID) + vhost := "" + if gateway != nil { + vhost = gateway.Vhost + } + + deploymentEvent := &model.APIDeploymentEvent{ + ApiId: apiUUID, + DeploymentID: deploymentID, + Vhost: vhost, + Environment: "production", + } + + if err := s.gatewayEventsService.BroadcastDeploymentEvent(deployment.GatewayID, deploymentEvent); err != nil { + log.Printf("[WARN] Failed to broadcast deployment event: %v", err) + } + } + + deployment.Status = "DEPLOYED" + + return &dto.DeploymentResponse{ + DeploymentID: deployment.DeploymentID, + ApiID: deployment.ApiID, + GatewayID: deployment.GatewayID, + Status: deployment.Status, + BaseDeploymentID: deployment.BaseDeploymentID, + Metadata: deployment.Metadata, + CreatedAt: deployment.CreatedAt, + }, nil +} + +// UndeployDeployment undeploys an active deployment +func (s *DeploymentService) UndeployDeployment(apiUUID, deploymentID, orgUUID string) error { + // Get the deployment + deployment, err := s.apiRepo.GetDeploymentByID(deploymentID, apiUUID, orgUUID) + if err != nil { + return err + } + if deployment == nil { + return errors.New("deployment not found") + } + if deployment.Status != "DEPLOYED" { + return errors.New("deployment is not currently active") + } + + // Update status to UNDEPLOYED + if err := s.apiRepo.UpdateDeploymentStatus(deploymentID, apiUUID, "UNDEPLOYED", orgUUID); err != nil { + return fmt.Errorf("failed to update deployment status: %w", err) + } + + // Send undeployment event to gateway + if s.gatewayEventsService != nil { + gateway, _ := s.gatewayRepo.GetByUUID(deployment.GatewayID) + vhost := "" + if gateway != nil { + vhost = gateway.Vhost + } + + undeploymentEvent := &model.APIUndeploymentEvent{ + ApiId: apiUUID, + Vhost: vhost, + Environment: "production", + } + + if err := s.gatewayEventsService.BroadcastUndeploymentEvent(deployment.GatewayID, undeploymentEvent); err != nil { + log.Printf("[WARN] Failed to broadcast undeployment event: %v", err) + } + } + + return nil +} + +// DeleteDeployment permanently deletes an undeployed deployment artifact +func (s *DeploymentService) DeleteDeployment(apiUUID, deploymentID, orgUUID string) error { + // Get the deployment + deployment, err := s.apiRepo.GetDeploymentByID(deploymentID, apiUUID, orgUUID) + if err != nil { + return err + } + if deployment == nil { + return errors.New("deployment not found") + } + if deployment.Status == "DEPLOYED" { + return errors.New("cannot delete an active deployment - undeploy it first") + } + + // Delete the deployment + if err := s.apiRepo.DeleteDeployment(deploymentID, apiUUID, orgUUID); err != nil { + return fmt.Errorf("failed to delete deployment: %w", err) + } + + return nil +} + +// GetDeployments retrieves all deployments for an API with optional filters +func (s *DeploymentService) GetDeployments(apiUUID, orgUUID string, gatewayID *string, status *string) (*dto.DeploymentListResponse, error) { + // Verify API exists + apiModel, err := s.apiRepo.GetAPIByUUID(apiUUID, orgUUID) + if err != nil { + return nil, err + } + if apiModel == nil { + return nil, constants.ErrAPINotFound + } + + // Get deployments + deployments, err := s.apiRepo.GetDeploymentsByAPIUUID(apiUUID, orgUUID, gatewayID, status) + if err != nil { + return nil, err + } + + // Convert to DTOs + var deploymentDTOs []*dto.DeploymentResponse + for _, d := range deployments { + deploymentDTOs = append(deploymentDTOs, &dto.DeploymentResponse{ + DeploymentID: d.DeploymentID, + ApiID: d.ApiID, + GatewayID: d.GatewayID, + Status: d.Status, + BaseDeploymentID: d.BaseDeploymentID, + Metadata: d.Metadata, + CreatedAt: d.CreatedAt, + }) + } + + return &dto.DeploymentListResponse{ + Count: len(deploymentDTOs), + List: deploymentDTOs, + Pagination: dto.Pagination{ + Offset: 0, + Limit: len(deploymentDTOs), + Total: len(deploymentDTOs), + }, + }, nil +} + +// GetDeployment retrieves a specific deployment by ID +func (s *DeploymentService) GetDeployment(apiUUID, deploymentID, orgUUID string) (*dto.DeploymentResponse, error) { + // Verify API exists + apiModel, err := s.apiRepo.GetAPIByUUID(apiUUID, orgUUID) + if err != nil { + return nil, err + } + if apiModel == nil { + return nil, constants.ErrAPINotFound + } + + // Get deployment - apiId is validated at repository level + deployment, err := s.apiRepo.GetDeploymentByID(deploymentID, apiUUID, orgUUID) + if err != nil { + return nil, err + } + if deployment == nil { + return nil, errors.New("deployment not found") + } + + // Convert to DTO + return &dto.DeploymentResponse{ + DeploymentID: deployment.DeploymentID, + ApiID: deployment.ApiID, + GatewayID: deployment.GatewayID, + Status: deployment.Status, + BaseDeploymentID: deployment.BaseDeploymentID, + Metadata: deployment.Metadata, + CreatedAt: deployment.CreatedAt, + }, nil +} + +// GetDeploymentContent retrieves the immutable content of a deployment +func (s *DeploymentService) GetDeploymentContent(apiUUID, deploymentID, orgUUID string) ([]byte, error) { + // Verify deployment exists and belongs to the API + deployment, err := s.apiRepo.GetDeploymentByID(deploymentID, apiUUID, orgUUID) + if err != nil { + return nil, err + } + if deployment == nil { + return nil, errors.New("deployment not found") + } + + // Get content - apiId is validated at repository level + content, err := s.apiRepo.GetDeploymentContent(deploymentID, apiUUID, orgUUID) + if err != nil { + return nil, fmt.Errorf("failed to get deployment content: %w", err) + } + + return content, nil +} + +// ensureAPIGatewayAssociation ensures an association exists between API and gateway +func (s *DeploymentService) ensureAPIGatewayAssociation(apiUUID, gatewayID, orgUUID string) error { + // Check if association already exists + associations, err := s.apiRepo.GetAPIAssociations(apiUUID, constants.AssociationTypeGateway, orgUUID) + if err != nil { + return err + } + + for _, assoc := range associations { + if assoc.ResourceID == gatewayID { + // Association already exists + return nil + } + } + + // Create new association + association := &model.APIAssociation{ + ApiID: apiUUID, + OrganizationID: orgUUID, + ResourceID: gatewayID, + AssociationType: constants.AssociationTypeGateway, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + return s.apiRepo.CreateAPIAssociation(association) +} + +// DeployAPIByHandle creates a new immutable deployment artifact using API handle +func (s *DeploymentService) DeployAPIByHandle(apiHandle string, req *dto.DeployAPIRequest, orgUUID string) (*dto.DeploymentResponse, error) { + // Convert API handle to UUID + apiUUID, err := s.getAPIUUIDByHandle(apiHandle, orgUUID) + if err != nil { + return nil, err + } + + return s.DeployAPI(apiUUID, req, orgUUID) +} + +// getAPIUUIDByHandle retrieves the internal UUID for an API by its handle +func (s *DeploymentService) getAPIUUIDByHandle(handle, orgUUID string) (string, error) { + if handle == "" { + return "", errors.New("API handle is required") + } + + metadata, err := s.apiRepo.GetAPIMetadataByHandle(handle, orgUUID) + if err != nil { + return "", err + } + if metadata == nil { + return "", constants.ErrAPINotFound + } + + return metadata.ID, nil +} + +// GetDeploymentByHandle retrieves a single deployment using API handle +func (s *DeploymentService) GetDeploymentByHandle(apiHandle, deploymentID, orgUUID string) (*dto.DeploymentResponse, error) { + // Convert API handle to UUID + apiUUID, err := s.getAPIUUIDByHandle(apiHandle, orgUUID) + if err != nil { + return nil, err + } + + return s.GetDeployment(deploymentID, apiUUID, orgUUID) +} + +// GetDeploymentsByHandle retrieves deployments for an API using handle +func (s *DeploymentService) GetDeploymentsByHandle(apiHandle, gatewayID, status, orgUUID string) (*dto.DeploymentListResponse, error) { + // Convert API handle to UUID + apiUUID, err := s.getAPIUUIDByHandle(apiHandle, orgUUID) + if err != nil { + return nil, err + } + + // Convert empty strings to nil for optional parameters + var gatewayIdPtr *string + var statusPtr *string + if gatewayID != "" { + gatewayIdPtr = &gatewayID + } + if status != "" { + statusPtr = &status + } + + return s.GetDeployments(apiUUID, orgUUID, gatewayIdPtr, statusPtr) +} + +// RedeployDeploymentByHandle redeploys an existing deployment using API handle +func (s *DeploymentService) RedeployDeploymentByHandle(apiHandle, deploymentID, orgUUID string) (*dto.DeploymentResponse, error) { + // Convert API handle to UUID + apiUUID, err := s.getAPIUUIDByHandle(apiHandle, orgUUID) + if err != nil { + return nil, err + } + + return s.RedeployDeployment(deploymentID, apiUUID, orgUUID) +} + +// UndeployDeploymentByHandle undeploys a deployment using API handle +func (s *DeploymentService) UndeployDeploymentByHandle(apiHandle, deploymentID, orgUUID string) error { + // Convert API handle to UUID + apiUUID, err := s.getAPIUUIDByHandle(apiHandle, orgUUID) + if err != nil { + return err + } + + return s.UndeployDeployment(deploymentID, apiUUID, orgUUID) +} + +// DeleteDeploymentByHandle deletes a deployment using API handle +func (s *DeploymentService) DeleteDeploymentByHandle(apiHandle, deploymentID, orgUUID string) error { + // Convert API handle to UUID + apiUUID, err := s.getAPIUUIDByHandle(apiHandle, orgUUID) + if err != nil { + return err + } + + return s.DeleteDeployment(deploymentID, apiUUID, orgUUID) +} + +// GetDeploymentContentByHandle retrieves deployment artifact content using API handle +func (s *DeploymentService) GetDeploymentContentByHandle(apiHandle, deploymentID, orgUUID string) ([]byte, error) { + // Convert API handle to UUID + apiUUID, err := s.getAPIUUIDByHandle(apiHandle, orgUUID) + if err != nil { + return nil, err + } + + return s.GetDeploymentContent(deploymentID, apiUUID, orgUUID) +} diff --git a/platform-api/src/internal/service/gateway_events.go b/platform-api/src/internal/service/gateway_events.go index 8d28c1ea0..555a0da56 100644 --- a/platform-api/src/internal/service/gateway_events.go +++ b/platform-api/src/internal/service/gateway_events.go @@ -134,3 +134,76 @@ func (s *GatewayEventsService) BroadcastDeploymentEvent(gatewayID string, deploy // Partial success is still considered success (some instances received the event) return nil } + +// BroadcastUndeploymentEvent sends an API undeployment event to target gateway +func (s *GatewayEventsService) BroadcastUndeploymentEvent(gatewayID string, undeployment *model.APIUndeploymentEvent) error { + // Create correlation ID for tracing + correlationID := uuid.New().String() + + // Serialize payload + payloadJSON, err := json.Marshal(undeployment) + if err != nil { + log.Printf("[ERROR] Failed to serialize undeployment event: gatewayID=%s error=%v", gatewayID, err) + return fmt.Errorf("failed to serialize undeployment event: %w", err) + } + + // Validate payload size + if len(payloadJSON) > MaxEventPayloadSize { + err := fmt.Errorf("event payload exceeds maximum size: %d bytes (limit: %d bytes)", len(payloadJSON), MaxEventPayloadSize) + log.Printf("[ERROR] Payload size validation failed: gatewayID=%s size=%d error=%v", gatewayID, len(payloadJSON), err) + return err + } + + // Create gateway event DTO with undeployment type + eventDTO := dto.GatewayEventDTO{ + Type: "api.undeployed", + Payload: undeployment, + Timestamp: time.Now().Format(time.RFC3339), + CorrelationID: correlationID, + } + + // Serialize complete event + eventJSON, err := json.Marshal(eventDTO) + if err != nil { + log.Printf("[ERROR] Failed to marshal undeployment event DTO: gatewayID=%s correlationId=%s error=%v", gatewayID, correlationID, err) + return fmt.Errorf("failed to marshal event: %w", err) + } + + // Get all connections for this gateway + connections := s.manager.GetConnections(gatewayID) + if len(connections) == 0 { + log.Printf("[WARN] No active connections for gateway: gatewayID=%s correlationId=%s", gatewayID, correlationID) + return fmt.Errorf("no active connections for gateway: %s", gatewayID) + } + + // Broadcast to all connections + successCount := 0 + failureCount := 0 + var lastError error + + for _, conn := range connections { + err := conn.Send(eventJSON) + if err != nil { + failureCount++ + lastError = err + log.Printf("[ERROR] Failed to send undeployment event: gatewayID=%s connectionID=%s correlationId=%s error=%v", + gatewayID, conn.ConnectionID, correlationID, err) + conn.DeliveryStats.IncrementFailed(fmt.Sprintf("send error: %v", err)) + } else { + successCount++ + log.Printf("[INFO] Undeployment event sent: gatewayID=%s connectionID=%s correlationId=%s type=%s", + gatewayID, conn.ConnectionID, correlationID, eventDTO.Type) + conn.DeliveryStats.IncrementTotalSent() + } + } + + // Log broadcast summary + log.Printf("[INFO] Undeployment broadcast summary: gatewayID=%s correlationId=%s total=%d success=%d failed=%d", + gatewayID, correlationID, len(connections), successCount, failureCount) + + if successCount == 0 { + return fmt.Errorf("failed to deliver undeployment event to any connection: %w", lastError) + } + + return nil +} diff --git a/platform-api/src/internal/service/gateway_internal.go b/platform-api/src/internal/service/gateway_internal.go index b87a69802..5cf86f02d 100644 --- a/platform-api/src/internal/service/gateway_internal.go +++ b/platform-api/src/internal/service/gateway_internal.go @@ -177,8 +177,6 @@ func (s *GatewayInternalAPIService) CreateGatewayAPIDeployment(apiHandle, orgID, Type: "HTTP", Transport: []string{"http", "https"}, IsDefaultVersion: false, - IsRevision: false, - RevisionID: 0, Operations: operations, CreatedAt: now, UpdatedAt: now, @@ -210,7 +208,7 @@ func (s *GatewayInternalAPIService) CreateGatewayAPIDeployment(apiHandle, orgID, } // Check if deployment already exists - existingDeployments, err := s.apiRepo.GetDeploymentsByAPIUUID(apiUUID, orgID) + existingDeployments, err := s.apiRepo.GetDeploymentsByAPIUUID(apiUUID, orgID, nil, nil) if err != nil { return nil, fmt.Errorf("failed to check existing deployments: %w", err) } @@ -267,7 +265,7 @@ func (s *GatewayInternalAPIService) CreateGatewayAPIDeployment(apiHandle, orgID, return &dto.GatewayAPIDeploymentResponse{ APIId: apiUUID, - DeploymentId: int64(deployment.ID), + DeploymentId: 0, // Legacy field, no longer used with new deployment model Message: "API deployment registered successfully", Created: apiCreated, }, nil diff --git a/platform-api/src/internal/utils/api.go b/platform-api/src/internal/utils/api.go index ea6f1116f..8fce5c56b 100644 --- a/platform-api/src/internal/utils/api.go +++ b/platform-api/src/internal/utils/api.go @@ -60,9 +60,6 @@ func (u *APIUtil) DTOToModel(dto *dto.API) *model.API { LifeCycleStatus: dto.LifeCycleStatus, HasThumbnail: dto.HasThumbnail, IsDefaultVersion: dto.IsDefaultVersion, - IsRevision: dto.IsRevision, - RevisionedAPIID: dto.RevisionedAPIID, - RevisionID: dto.RevisionID, Type: dto.Type, Transport: dto.Transport, MTLS: u.MTLSDTOToModel(dto.MTLS), @@ -96,9 +93,6 @@ func (u *APIUtil) ModelToDTO(model *model.API) *dto.API { LifeCycleStatus: model.LifeCycleStatus, HasThumbnail: model.HasThumbnail, IsDefaultVersion: model.IsDefaultVersion, - IsRevision: model.IsRevision, - RevisionedAPIID: model.RevisionedAPIID, - RevisionID: model.RevisionID, Type: model.Type, Transport: model.Transport, MTLS: u.MTLSModelToDTO(model.MTLS), @@ -1150,8 +1144,6 @@ func (u *APIUtil) APIYAMLDataToDTO(yamlData *dto.APIYAMLData) *dto.API { Transport: []string{"http", "https"}, HasThumbnail: false, IsDefaultVersion: false, - IsRevision: false, - RevisionID: 0, // Fields that need to be set by caller: // - ProjectID (required) @@ -1743,9 +1735,6 @@ func (u *APIUtil) MergeAPIDetails(userAPI *dto.API, extractedAPI *dto.API) *dto. // Copy boolean fields from user input merged.HasThumbnail = userAPI.HasThumbnail merged.IsDefaultVersion = userAPI.IsDefaultVersion - merged.IsRevision = userAPI.IsRevision - merged.RevisionedAPIID = userAPI.RevisionedAPIID - merged.RevisionID = userAPI.RevisionID return merged } From 0687b37891b6affbc08c8b6e51ab1ca79520f269 Mon Sep 17 00:00:00 2001 From: dushaniw Date: Wed, 14 Jan 2026 16:28:31 +0530 Subject: [PATCH 07/11] retrieve api deployment by gateway and api. --- platform-api/src/internal/constants/error.go | 5 +++ .../src/internal/handler/deployment.go | 39 +++++++++++++++---- .../src/internal/handler/gateway_internal.go | 11 +++++- .../src/internal/service/gateway_internal.go | 30 ++++++++++++++ 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/platform-api/src/internal/constants/error.go b/platform-api/src/internal/constants/error.go index e5508ff44..d61143144 100644 --- a/platform-api/src/internal/constants/error.go +++ b/platform-api/src/internal/constants/error.go @@ -63,6 +63,11 @@ var ( ErrGatewayHasAssociatedAPIs = errors.New("cannot delete gateway: it has associated APIs. Please remove all API associations before deleting the gateway") ) +var ( + ErrDeploymentNotFound = errors.New("deployment not found") + ErrDeploymentNotActive = errors.New("no active deployment found for this API on the gateway") +) + var ( ErrApiPortalSync = errors.New("failed to synchronize with dev portal") ) diff --git a/platform-api/src/internal/handler/deployment.go b/platform-api/src/internal/handler/deployment.go index fb27bd304..a390d667a 100644 --- a/platform-api/src/internal/handler/deployment.go +++ b/platform-api/src/internal/handler/deployment.go @@ -128,8 +128,13 @@ func (h *DeploymentHandler) RedeployDeployment(c *gin.Context) { "API not found")) return } + if errors.Is(err, constants.ErrDeploymentNotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "Deployment not found")) + return + } log.Printf("[ERROR] Failed to redeploy: apiId=%s deploymentId=%s error=%v", apiId, deploymentId, err) - c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", err.Error())) return } @@ -167,8 +172,13 @@ func (h *DeploymentHandler) UndeployDeployment(c *gin.Context) { "API not found")) return } + if errors.Is(err, constants.ErrDeploymentNotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "Deployment not found")) + return + } log.Printf("[ERROR] Failed to undeploy: apiId=%s deploymentId=%s error=%v", apiId, deploymentId, err) - c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", err.Error())) return } @@ -206,8 +216,13 @@ func (h *DeploymentHandler) DeleteDeployment(c *gin.Context) { "API not found")) return } + if errors.Is(err, constants.ErrDeploymentNotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "Deployment not found")) + return + } log.Printf("[ERROR] Failed to delete deployment: apiId=%s deploymentId=%s error=%v", apiId, deploymentId, err) - c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", err.Error())) return } @@ -245,8 +260,13 @@ func (h *DeploymentHandler) GetDeployment(c *gin.Context) { "API not found")) return } + if errors.Is(err, constants.ErrDeploymentNotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "Deployment not found")) + return + } log.Printf("[ERROR] Failed to get deployment: apiId=%s deploymentId=%s error=%v", apiId, deploymentId, err) - c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Deployment not found")) return } @@ -322,12 +342,15 @@ func (h *DeploymentHandler) GetDeploymentContent(c *gin.Context) { "API not found")) return } + if errors.Is(err, constants.ErrDeploymentNotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "Deployment not found")) + return + } log.Printf("[ERROR] Failed to get deployment content: apiId=%s deploymentId=%s error=%v", apiId, deploymentId, err) - c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) - return + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to retrieve deployment content")) } - c.Data(http.StatusOK, "application/json", content) } diff --git a/platform-api/src/internal/handler/gateway_internal.go b/platform-api/src/internal/handler/gateway_internal.go index 7a48c4f4f..35af15b1e 100644 --- a/platform-api/src/internal/handler/gateway_internal.go +++ b/platform-api/src/internal/handler/gateway_internal.go @@ -26,8 +26,9 @@ import ( "platform-api/src/internal/dto" "platform-api/src/internal/utils" - "github.com/gin-gonic/gin" "platform-api/src/internal/service" + + "github.com/gin-gonic/gin" ) type GatewayInternalAPIHandler struct { @@ -121,6 +122,7 @@ func (h *GatewayInternalAPIHandler) GetAPI(c *gin.Context) { } orgID := gateway.OrganizationID + gatewayID := gateway.ID apiID := c.Param("apiId") if apiID == "" { c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", @@ -128,8 +130,13 @@ func (h *GatewayInternalAPIHandler) GetAPI(c *gin.Context) { return } - api, err := h.gatewayInternalService.GetAPIByUUID(apiID, orgID) + api, err := h.gatewayInternalService.GetActiveDeploymentByGateway(apiID, orgID, gatewayID) if err != nil { + if errors.Is(err, constants.ErrDeploymentNotActive) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "No active deployment found for this API on this gateway")) + return + } if errors.Is(err, constants.ErrAPINotFound) { c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "API not found")) diff --git a/platform-api/src/internal/service/gateway_internal.go b/platform-api/src/internal/service/gateway_internal.go index 5cf86f02d..b232c9148 100644 --- a/platform-api/src/internal/service/gateway_internal.go +++ b/platform-api/src/internal/service/gateway_internal.go @@ -18,6 +18,7 @@ package service import ( + "encoding/json" "fmt" "platform-api/src/internal/constants" "platform-api/src/internal/dto" @@ -95,6 +96,35 @@ func (s *GatewayInternalAPIService) GetAPIByUUID(apiId, orgId string) (map[strin return apiYamlMap, nil } +// GetActiveDeploymentByGateway retrieves the currently deployed API artifact for a specific gateway +func (s *GatewayInternalAPIService) GetActiveDeploymentByGateway(apiID, orgID, gatewayID string) (map[string]string, error) { + // Get the active deployment for this API on this gateway + deployment, err := s.apiRepo.GetActiveDeploymentByGateway(apiID, gatewayID, orgID) + if err != nil { + return nil, fmt.Errorf("failed to get deployment: %w", err) + } + if deployment == nil { + return nil, constants.ErrDeploymentNotActive + } + + // Parse the deployment content (which is the serialized API definition) + var apiDTO *dto.API + if err := json.Unmarshal(deployment.Content, &apiDTO); err != nil { + return nil, fmt.Errorf("failed to unmarshal deployment content: %w", err) + } + + // Generate deployment YAML from the artifact + apiYaml, err := s.apiUtil.GenerateAPIDeploymentYAML(apiDTO) + if err != nil { + return nil, fmt.Errorf("failed to generate API YAML: %w", err) + } + + apiYamlMap := map[string]string{ + apiDTO.ID: apiYaml, + } + return apiYamlMap, nil +} + // CreateGatewayAPIDeployment handles the registration of an API deployment from a gateway func (s *GatewayInternalAPIService) CreateGatewayAPIDeployment(apiHandle, orgID, gatewayID string, notification dto.APIDeploymentNotification, revisionID *string) (*dto.GatewayAPIDeploymentResponse, error) { From 1ba969d89b30f23bec5e1975e407820caf930ddf Mon Sep 17 00:00:00 2001 From: dushaniw Date: Wed, 14 Jan 2026 17:11:09 +0530 Subject: [PATCH 08/11] move deployment generation to deployment time. --- .../src/internal/service/deployment.go | 31 +++++++------------ .../src/internal/service/gateway_internal.go | 16 ++-------- 2 files changed, 15 insertions(+), 32 deletions(-) diff --git a/platform-api/src/internal/service/deployment.go b/platform-api/src/internal/service/deployment.go index 5ec0c9dd6..16cd9a856 100644 --- a/platform-api/src/internal/service/deployment.go +++ b/platform-api/src/internal/service/deployment.go @@ -18,7 +18,6 @@ package service import ( - "encoding/json" "errors" "fmt" "log" @@ -114,11 +113,21 @@ func (s *DeploymentService) DeployAPI(apiUUID string, req *dto.DeployAPIRequest, var baseDeploymentID *string var apiContent *dto.API + var contentBytes []byte // Determine the source: "current" or existing deployment if req.Base == "current" { // Use current API state apiContent = s.apiUtil.ModelToDTO(apiModel) + + // Generate API deployment YAML for storage + apiYaml, err := s.apiUtil.GenerateAPIDeploymentYAML(apiContent) + if err != nil { + return nil, fmt.Errorf("failed to generate API deployment YAML: %w", err) + } + + // Create immutable deployment artifact content (store as YAML bytes) + contentBytes = []byte(apiYaml) } else { // Use existing deployment as base baseDeployment, err := s.apiRepo.GetDeploymentByID(req.Base, apiUUID, orgUUID) @@ -129,27 +138,11 @@ func (s *DeploymentService) DeployAPI(apiUUID string, req *dto.DeployAPIRequest, return nil, errors.New("base deployment not found") } - // Deserialize content from base deployment - content, err := s.apiRepo.GetDeploymentContent(req.Base, apiUUID, orgUUID) - if err != nil { - return nil, fmt.Errorf("failed to get deployment content: %w", err) - } - - var deploymentContent dto.API - if err := json.Unmarshal(content, &deploymentContent); err != nil { - return nil, fmt.Errorf("failed to unmarshal deployment content: %w", err) - } - - apiContent = &deploymentContent + // Deployment content is already stored as YAML, reuse it directly + contentBytes = baseDeployment.Content baseDeploymentID = &req.Base } - // Create immutable deployment artifact content (serialize API + metadata) - contentBytes, err := json.Marshal(apiContent) - if err != nil { - return nil, fmt.Errorf("failed to marshal API content: %w", err) - } - // Generate deployment ID deploymentID := uuid.New().String() diff --git a/platform-api/src/internal/service/gateway_internal.go b/platform-api/src/internal/service/gateway_internal.go index b232c9148..2ec117829 100644 --- a/platform-api/src/internal/service/gateway_internal.go +++ b/platform-api/src/internal/service/gateway_internal.go @@ -18,7 +18,6 @@ package service import ( - "encoding/json" "fmt" "platform-api/src/internal/constants" "platform-api/src/internal/dto" @@ -107,20 +106,11 @@ func (s *GatewayInternalAPIService) GetActiveDeploymentByGateway(apiID, orgID, g return nil, constants.ErrDeploymentNotActive } - // Parse the deployment content (which is the serialized API definition) - var apiDTO *dto.API - if err := json.Unmarshal(deployment.Content, &apiDTO); err != nil { - return nil, fmt.Errorf("failed to unmarshal deployment content: %w", err) - } - - // Generate deployment YAML from the artifact - apiYaml, err := s.apiUtil.GenerateAPIDeploymentYAML(apiDTO) - if err != nil { - return nil, fmt.Errorf("failed to generate API YAML: %w", err) - } + // Deployment content is already stored as YAML, so return it directly + apiYaml := string(deployment.Content) apiYamlMap := map[string]string{ - apiDTO.ID: apiYaml, + apiID: apiYaml, } return apiYamlMap, nil } From 55481d55e5ac8a84fe980bbdd5fb1500f3bd00c9 Mon Sep 17 00:00:00 2001 From: dushaniw Date: Fri, 16 Jan 2026 10:35:58 +0530 Subject: [PATCH 09/11] add example. --- platform-api/src/resources/openapi.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/platform-api/src/resources/openapi.yaml b/platform-api/src/resources/openapi.yaml index 132784dc0..8a6fdae50 100644 --- a/platform-api/src/resources/openapi.yaml +++ b/platform-api/src/resources/openapi.yaml @@ -3673,6 +3673,7 @@ components: base: type: string description: The source for the API definition. Can be "current" (latest working copy) or a deploymentId (existing deployment) + example: "current" gatewayId: type: string format: uuid From f9e51461d7369135b061c32dedb946e4c7681b6a Mon Sep 17 00:00:00 2001 From: dushaniw Date: Fri, 16 Jan 2026 15:29:20 +0530 Subject: [PATCH 10/11] fix bug in deployment and api ids. --- platform-api/src/internal/service/deployment.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/platform-api/src/internal/service/deployment.go b/platform-api/src/internal/service/deployment.go index 16cd9a856..c56c4c693 100644 --- a/platform-api/src/internal/service/deployment.go +++ b/platform-api/src/internal/service/deployment.go @@ -493,7 +493,7 @@ func (s *DeploymentService) GetDeploymentByHandle(apiHandle, deploymentID, orgUU return nil, err } - return s.GetDeployment(deploymentID, apiUUID, orgUUID) + return s.GetDeployment(apiUUID, deploymentID, orgUUID) } // GetDeploymentsByHandle retrieves deployments for an API using handle @@ -525,7 +525,7 @@ func (s *DeploymentService) RedeployDeploymentByHandle(apiHandle, deploymentID, return nil, err } - return s.RedeployDeployment(deploymentID, apiUUID, orgUUID) + return s.RedeployDeployment(apiUUID, deploymentID, orgUUID) } // UndeployDeploymentByHandle undeploys a deployment using API handle @@ -536,7 +536,7 @@ func (s *DeploymentService) UndeployDeploymentByHandle(apiHandle, deploymentID, return err } - return s.UndeployDeployment(deploymentID, apiUUID, orgUUID) + return s.UndeployDeployment(apiUUID, deploymentID, orgUUID) } // DeleteDeploymentByHandle deletes a deployment using API handle @@ -547,7 +547,7 @@ func (s *DeploymentService) DeleteDeploymentByHandle(apiHandle, deploymentID, or return err } - return s.DeleteDeployment(deploymentID, apiUUID, orgUUID) + return s.DeleteDeployment(apiUUID, deploymentID, orgUUID) } // GetDeploymentContentByHandle retrieves deployment artifact content using API handle From 0ef8547f66346bd1b75d405dcc13293e7b17e32c Mon Sep 17 00:00:00 2001 From: dushaniw Date: Fri, 16 Jan 2026 16:14:38 +0530 Subject: [PATCH 11/11] fix issues in deployments. --- platform-api/src/internal/repository/api.go | 40 +++++++++++++++++++ .../src/internal/repository/interfaces.go | 1 + .../src/internal/service/deployment.go | 26 +++++++++++- 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/platform-api/src/internal/repository/api.go b/platform-api/src/internal/repository/api.go index 63dac3520..609f41ff6 100644 --- a/platform-api/src/internal/repository/api.go +++ b/platform-api/src/internal/repository/api.go @@ -1207,6 +1207,46 @@ func (r *APIRepo) GetActiveDeploymentByGateway(apiUUID, gatewayID, orgID string) return deployment, nil } +// GetOldestUndeployedDeploymentByGateway implements [APIRepository]. +func (r *APIRepo) GetOldestUndeployedDeploymentByGateway(apiUUID string, gatewayID string, orgUUID string) (*model.APIDeployment, error) { + deployment := &model.APIDeployment{} + + // SQL query to select the oldest undeployed deployment + query := ` + SELECT deployment_id, api_uuid, organization_uuid, gateway_uuid, status, base_deployment_id, content, metadata, created_at + FROM api_deployments + WHERE api_uuid = ? AND gateway_uuid = ? AND organization_uuid = ? AND status != 'DEPLOYED' + ORDER BY created_at ASC + LIMIT 1 + ` + var baseDeploymentID sql.NullString + var metadataJSON string + + err := r.db.QueryRow(query, apiUUID, gatewayID, orgUUID).Scan( + &deployment.DeploymentID, &deployment.ApiID, &deployment.OrganizationID, + &deployment.GatewayID, &deployment.Status, &baseDeploymentID, &deployment.Content, &metadataJSON, &deployment.CreatedAt) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + + if baseDeploymentID.Valid { + deployment.BaseDeploymentID = &baseDeploymentID.String + } + + if metadataJSON != "" { + var metadata map[string]interface{} + if err := json.Unmarshal([]byte(metadataJSON), &metadata); err == nil { + deployment.Metadata = metadata + } + } + + return deployment, nil +} + // CreateAPIAssociation creates an association between an API and resource (e.g., gateway or dev portal) func (r *APIRepo) CreateAPIAssociation(association *model.APIAssociation) error { query := ` diff --git a/platform-api/src/internal/repository/interfaces.go b/platform-api/src/internal/repository/interfaces.go index 48b1df8b0..ab7ff674b 100644 --- a/platform-api/src/internal/repository/interfaces.go +++ b/platform-api/src/internal/repository/interfaces.go @@ -64,6 +64,7 @@ type APIRepository interface { DeleteDeployment(deploymentID, apiUUID, orgUUID string) error GetActiveDeploymentByGateway(apiUUID, gatewayID, orgUUID string) (*model.APIDeployment, error) CountDeploymentsByAPIAndGateway(apiUUID, gatewayID, orgUUID string) (int, error) + GetOldestUndeployedDeploymentByGateway(apiUUID, gatewayID, orgUUID string) (*model.APIDeployment, error) // API-Gateway association methods GetAPIGatewaysWithDetails(apiUUID, orgUUID string) ([]*model.APIGatewayWithDetails, error) diff --git a/platform-api/src/internal/service/deployment.go b/platform-api/src/internal/service/deployment.go index c56c4c693..8b76a55ca 100644 --- a/platform-api/src/internal/service/deployment.go +++ b/platform-api/src/internal/service/deployment.go @@ -99,9 +99,18 @@ func (s *DeploymentService) DeployAPI(apiUUID string, req *dto.DeployAPIRequest, return nil, fmt.Errorf("failed to check deployment count: %w", err) } if apiDeploymentCount >= s.cfg.Deployments.MaxPerAPIGateway { - return nil, fmt.Errorf("deployment limit exceeded: maximum %d deployments allowed per API-Gateway combination", s.cfg.Deployments.MaxPerAPIGateway) + // Delete oldest deployment in UNDEPLOYED state to make room + oldestDeployment, err := s.apiRepo.GetOldestUndeployedDeploymentByGateway(apiUUID, req.GatewayID, orgUUID) + if err != nil { + return nil, fmt.Errorf("failed to get oldest undeployed deployment: %w", err) + } + if oldestDeployment != nil { + if err := s.apiRepo.DeleteDeployment(oldestDeployment.DeploymentID, apiUUID, orgUUID); err != nil { + return nil, fmt.Errorf("failed to delete oldest undeployed deployment: %w", err) + } + log.Printf("[INFO] Deleted oldest undeployed deployment %s to make room for new deployment", oldestDeployment.DeploymentID) + } } - // Validate API has backend services attached backendServices, err := s.backendServiceRepo.GetBackendServicesByAPIID(apiUUID) if err != nil { @@ -159,6 +168,19 @@ func (s *DeploymentService) DeployAPI(apiUUID string, req *dto.DeployAPIRequest, } } + // Send undeployment event to gateway + if s.gatewayEventsService != nil { + undeploymentEvent := &model.APIUndeploymentEvent{ + ApiId: apiUUID, + Vhost: gateway.Vhost, + Environment: "production", + } + + if err := s.gatewayEventsService.BroadcastUndeploymentEvent(req.GatewayID, undeploymentEvent); err != nil { + log.Printf("[WARN] Failed to broadcast undeployment event: %v", err) + } + } + // Create new deployment record deployment := &model.APIDeployment{ DeploymentID: deploymentID,