From 348b121b2e91d43621eb9e5562bd75f00ae34bb8 Mon Sep 17 00:00:00 2001 From: thivindu Date: Thu, 13 Nov 2025 09:44:12 +0530 Subject: [PATCH 1/8] Implement import API project from Github repo --- platform-api/src/internal/constants/error.go | 8 ++ platform-api/src/internal/dto/api-project.go | 50 +++++++ platform-api/src/internal/handler/api.go | 122 ++++++++++++++++++ platform-api/src/internal/service/api.go | 119 +++++++++++++++++ platform-api/src/internal/service/git.go | 83 ++++++++++++ .../internal/service/git_bitbucket_client.go | 32 +++++ .../src/internal/service/git_github_client.go | 68 ++++++++++ .../src/internal/service/git_gitlab_client.go | 6 + .../src/internal/service/git_provider.go | 1 + platform-api/src/internal/utils/api.go | 84 ++++++++++++ platform-api/src/resources/openapi.yaml | 72 +++++++++++ 11 files changed, 645 insertions(+) create mode 100644 platform-api/src/internal/dto/api-project.go diff --git a/platform-api/src/internal/constants/error.go b/platform-api/src/internal/constants/error.go index b5acdc6f..596bb17e 100644 --- a/platform-api/src/internal/constants/error.go +++ b/platform-api/src/internal/constants/error.go @@ -83,4 +83,12 @@ var ( ErrAPIPublicationNotFound = errors.New("api publication not found") ErrAPIPublicationInProgress = errors.New("api publication is currently in progress") ErrAPIAlreadyPublished = errors.New("api is already published to devportal") + + // API Project Import errors + ErrAPIProjectNotFound = errors.New("api project not found") + ErrMalformedAPIProject = errors.New("malformed api project") + ErrInvalidAPIProject = errors.New("invalid api project") + ErrConfigFileNotFound = errors.New("config.yaml file not found") + ErrOpenAPIFileNotFound = errors.New("openapi file not found") + ErrWSO2ArtifactNotFound = errors.New("wso2 artifact file not found") ) diff --git a/platform-api/src/internal/dto/api-project.go b/platform-api/src/internal/dto/api-project.go new file mode 100644 index 00000000..60a47906 --- /dev/null +++ b/platform-api/src/internal/dto/api-project.go @@ -0,0 +1,50 @@ +/* + * 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 dto + +// ImportAPIProjectRequest represents the request payload for importing an API project from Git +type ImportAPIProjectRequest struct { + RepoURL string `json:"repoUrl" binding:"required"` + Provider string `json:"provider,omitempty"` // Optional: "github", "gitlab", "bitbucket", etc. + Branch string `json:"branch" binding:"required"` + Path string `json:"path" binding:"required"` + API API `json:"api" binding:"required"` +} + +// APIProjectConfig represents the structure of config.yaml file in .api-platform directory +type APIProjectConfig struct { + Version string `yaml:"version"` + APIs []APIConfigEntry `yaml:"apis"` + SpectralRulesets []SpectralRuleset `yaml:"spectralRulesets,omitempty"` +} + +// APIConfigEntry represents an API entry in the config.yaml +type APIConfigEntry struct { + OpenAPI string `yaml:"openapi"` + WSO2Artifact string `yaml:"wso2Artifact"` + Documentation string `yaml:"documentation,omitempty"` + Tests string `yaml:"tests,omitempty"` +} + +// SpectralRuleset represents a spectral ruleset configuration +type SpectralRuleset struct { + Name string `yaml:"name"` + SourceFolder string `yaml:"sourceFolder"` + FileName string `yaml:"fileName"` + RulesetContentPath string `yaml:"rulesetContentPath"` +} diff --git a/platform-api/src/internal/handler/api.go b/platform-api/src/internal/handler/api.go index 3c89bfdd..ad47b8a5 100644 --- a/platform-api/src/internal/handler/api.go +++ b/platform-api/src/internal/handler/api.go @@ -610,6 +610,124 @@ func (h *APIHandler) GetAPIPublications(c *gin.Context) { c.JSON(http.StatusOK, response) } +// ImportAPIProject handles POST /api/v1/import/api-project +func (h *APIHandler) ImportAPIProject(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 + } + + var req dto.ImportAPIProjectRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + return + } + + // Validate required fields + if req.RepoURL == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Repository URL is required")) + return + } + if req.Branch == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Branch is required")) + return + } + if req.Path == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Path is required")) + return + } + if req.API.Name == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API name is required")) + return + } + if req.API.Context == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API context is required")) + return + } + if req.API.Version == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API version is required")) + return + } + if req.API.ProjectID == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Project ID is required")) + return + } + + // Create Git service + gitService := service.NewGitService() + + // Import API project + api, err := h.apiService.ImportAPIProject(&req, orgId, gitService) + if err != nil { + if errors.Is(err, constants.ErrAPIProjectNotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "API Project Not Found", + "API project not found: .api-platform directory not found")) + return + } + if errors.Is(err, constants.ErrMalformedAPIProject) { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Malformed API Project", + "Malformed API project: config.yaml is missing or invalid")) + return + } + if errors.Is(err, constants.ErrInvalidAPIProject) { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Invalid API Project", + "Invalid API project: referenced files not found")) + return + } + if errors.Is(err, constants.ErrConfigFileNotFound) { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Config File Not Found", + "config.yaml file not found in .api-platform directory")) + return + } + if errors.Is(err, constants.ErrOpenAPIFileNotFound) { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "OpenAPI File Not Found", + "OpenAPI file not found")) + return + } + if errors.Is(err, constants.ErrWSO2ArtifactNotFound) { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "WSO2 Artifact Not Found", + "WSO2 artifact file not found")) + return + } + if errors.Is(err, constants.ErrAPIAlreadyExists) { + c.JSON(http.StatusConflict, utils.NewErrorResponse(409, "Conflict", + "API already exists in the project")) + return + } + if errors.Is(err, constants.ErrProjectNotFound) { + c.JSON(http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "Project not found")) + return + } + if errors.Is(err, constants.ErrInvalidAPIName) { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Invalid API name format")) + return + } + if errors.Is(err, constants.ErrInvalidAPIContext) { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Invalid API context format")) + return + } + + log.Printf("Failed to import API project: %v", err) + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to import API project")) + return + } + + c.JSON(http.StatusCreated, api) +} + // RegisterRoutes registers all API routes func (h *APIHandler) RegisterRoutes(r *gin.Engine) { // API routes @@ -627,4 +745,8 @@ func (h *APIHandler) RegisterRoutes(r *gin.Engine) { apiGroup.POST("/:apiId/devportals/unpublish", h.UnpublishFromDevPortal) apiGroup.GET("/:apiId/publications", h.GetAPIPublications) } + importGroup := r.Group("/api/v1/import") + { + importGroup.POST("/api-project", h.ImportAPIProject) + } } diff --git a/platform-api/src/internal/service/api.go b/platform-api/src/internal/service/api.go index 288e017c..67cc5ec0 100644 --- a/platform-api/src/internal/service/api.go +++ b/platform-api/src/internal/service/api.go @@ -161,6 +161,9 @@ func (s *APIService) CreateAPI(req *CreateAPIRequest, orgId string) (*dto.API, e return nil, fmt.Errorf("failed to create api: %w", err) } + api.CreatedAt = time.Now() + api.UpdatedAt = time.Now() + // Associate backend services with the API for i, backendServiceUUID := range backendServiceIdList { isDefault := i == 0 // First backend service is default @@ -944,6 +947,122 @@ func (s *APIService) generateDefaultOperations() []dto.Operation { } } +// ImportAPIProject imports an API project from a Git repository +func (s *APIService) ImportAPIProject(req *dto.ImportAPIProjectRequest, orgId string, gitService GitService) (*dto.API, error) { + // 1. Validate if there is a .api-platform directory with config.yaml + config, err := gitService.ValidateAPIProject(req.RepoURL, req.Branch, req.Path) + if err != nil { + if strings.Contains(err.Error(), "api project not found") { + return nil, constants.ErrAPIProjectNotFound + } + if strings.Contains(err.Error(), "malformed api project") { + return nil, constants.ErrMalformedAPIProject + } + if strings.Contains(err.Error(), "invalid api project") { + return nil, constants.ErrInvalidAPIProject + } + return nil, err + } + + // For now, we'll process the first API in the config (can be extended later for multiple APIs) + if len(config.APIs) == 0 { + return nil, constants.ErrMalformedAPIProject + } + + apiConfig := config.APIs[0] + + // 5. Fetch the WSO2 artifact file content + wso2ArtifactPath := req.Path + "/" + apiConfig.WSO2Artifact + artifactData, err := gitService.FetchWSO2Artifact(req.RepoURL, req.Branch, wso2ArtifactPath) + if err != nil { + return nil, constants.ErrWSO2ArtifactNotFound + } + + // 6. Create API with details from WSO2 artifact, overwritten by request details + apiData := s.mergeAPIData(&artifactData.Data, &req.API) + + // 7. Create API using the existing CreateAPI flow + createReq := &CreateAPIRequest{ + Name: apiData.Name, + DisplayName: apiData.DisplayName, + Description: apiData.Description, + Context: apiData.Context, + Version: apiData.Version, + Provider: apiData.Provider, + ProjectID: apiData.ProjectID, + 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, + Security: apiData.Security, + CORS: apiData.CORS, + BackendServices: apiData.BackendServices, + APIRateLimiting: apiData.APIRateLimiting, + Operations: apiData.Operations, + } + + return s.CreateAPI(createReq, orgId) +} + +// mergeAPIData merges WSO2 artifact data with user-provided API data (user data takes precedence) +func (s *APIService) mergeAPIData(artifact *dto.APIYAMLData2, userAPIData *dto.API) *dto.API { + apiDTO := s.apiUtil.APIYAMLData2ToDTO(artifact) + + // Overwrite with user-provided data (if not empty) + if userAPIData.Name != "" { + apiDTO.Name = userAPIData.Name + } + if userAPIData.DisplayName != "" { + apiDTO.DisplayName = userAPIData.DisplayName + } + if userAPIData.Description != "" { + apiDTO.Description = userAPIData.Description + } + if userAPIData.Context != "" { + apiDTO.Context = userAPIData.Context + } + if userAPIData.Version != "" { + apiDTO.Version = userAPIData.Version + } + if userAPIData.Provider != "" { + apiDTO.Provider = userAPIData.Provider + } + if userAPIData.ProjectID != "" { + apiDTO.ProjectID = userAPIData.ProjectID + } + if userAPIData.LifeCycleStatus != "" { + apiDTO.LifeCycleStatus = userAPIData.LifeCycleStatus + } + if userAPIData.Type != "" { + apiDTO.Type = userAPIData.Type + } + if len(userAPIData.Transport) > 0 { + apiDTO.Transport = userAPIData.Transport + } + if userAPIData.BackendServices != nil && len(userAPIData.BackendServices) > 0 { + apiDTO.BackendServices = userAPIData.BackendServices + } + + // 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 +} + // PublishAPIToDevPortal publishes an API to a specific DevPortal func (s *APIService) PublishAPIToDevPortal(apiID string, req *dto.PublishToDevPortalRequest, orgID string) (*dto.PublishToDevPortalResponse, error) { // Get the API diff --git a/platform-api/src/internal/service/git.go b/platform-api/src/internal/service/git.go index 2ec3a01a..9b10e901 100644 --- a/platform-api/src/internal/service/git.go +++ b/platform-api/src/internal/service/git.go @@ -19,6 +19,7 @@ package service import ( "fmt" + "gopkg.in/yaml.v3" "platform-api/src/internal/dto" ) @@ -26,6 +27,9 @@ type GitService interface { FetchRepoBranches(repoURL string) (*dto.GitRepoBranchesResponse, error) FetchRepoContent(repoURL, branch string) (*dto.GitRepoContentResponse, error) GetSupportedProviders() []string + FetchFileContent(repoURL, branch, path string) ([]byte, error) + ValidateAPIProject(repoURL, branch, path string) (*dto.APIProjectConfig, error) + FetchWSO2Artifact(repoURL, branch, path string) (*dto.APIDeploymentYAML, error) } type gitService struct { @@ -104,3 +108,82 @@ func (s *gitService) GetSupportedProviders() []string { } return providers } + +// FetchFileContent fetches the content of a specific file from a Git repository +func (s *gitService) FetchFileContent(repoURL, branch, path string) ([]byte, error) { + // Parse repository URL to determine provider and extract owner/repo + repoInfo, err := ParseRepositoryURL(repoURL) + if err != nil { + return nil, fmt.Errorf("invalid repository URL: %w", err) + } + + // Get the appropriate provider client + providerClient, exists := s.providers[repoInfo.Provider] + if !exists { + return nil, fmt.Errorf("unsupported Git provider: %s", repoInfo.Provider) + } + + // Use the provider-specific client to fetch file content + content, err := providerClient.FetchFileContent(repoInfo.Owner, repoInfo.Repo, branch, path) + if err != nil { + return nil, err + } + + return content, nil +} + +// ValidateAPIProject validates an API project structure in a Git repository +func (s *gitService) ValidateAPIProject(repoURL, branch, path string) (*dto.APIProjectConfig, error) { + // 1. Check if .api-platform directory exists + apiPlatformPath := path + "/.api-platform" + configContent, err := s.FetchFileContent(repoURL, branch, apiPlatformPath+"/config.yaml") + if err != nil { + return nil, fmt.Errorf("api project not found: .api-platform directory or config.yaml not found") + } + + var config dto.APIProjectConfig + if err := yaml.Unmarshal(configContent, &config); err != nil { + return nil, fmt.Errorf("malformed api project: invalid config.yaml format") + } + + // 3. Validate config structure + if len(config.APIs) == 0 { + return nil, fmt.Errorf("malformed api project: no APIs defined in config.yaml") + } + + for _, api := range config.APIs { + if api.OpenAPI == "" || api.WSO2Artifact == "" { + return nil, fmt.Errorf("malformed api project: apis.openapi and apis.wso2Artifact fields are required") + } + + // 4. Check if the referenced files exist in the project path + openAPIPath := path + "/" + api.OpenAPI + _, err := s.FetchFileContent(repoURL, branch, openAPIPath) + if err != nil { + return nil, fmt.Errorf("invalid api project: openapi file not found: %s", api.OpenAPI) + } + + wso2ArtifactPath := path + "/" + api.WSO2Artifact + _, err = s.FetchFileContent(repoURL, branch, wso2ArtifactPath) + if err != nil { + return nil, fmt.Errorf("invalid api project: wso2 artifact file not found: %s", api.WSO2Artifact) + } + } + + return &config, nil +} + +// FetchWSO2Artifact fetches and parses the WSO2 artifact file from a Git repository +func (s *gitService) FetchWSO2Artifact(repoURL, branch, path string) (*dto.APIDeploymentYAML, error) { + content, err := s.FetchFileContent(repoURL, branch, path) + if err != nil { + return nil, fmt.Errorf("failed to fetch WSO2 artifact file: %w", err) + } + + var artifact dto.APIDeploymentYAML + if err := yaml.Unmarshal(content, &artifact); err != nil { + return nil, fmt.Errorf("failed to parse WSO2 artifact file: %w", err) + } + + return &artifact, nil +} diff --git a/platform-api/src/internal/service/git_bitbucket_client.go b/platform-api/src/internal/service/git_bitbucket_client.go index 8c0d1077..191f1d57 100644 --- a/platform-api/src/internal/service/git_bitbucket_client.go +++ b/platform-api/src/internal/service/git_bitbucket_client.go @@ -20,6 +20,7 @@ package service import ( "encoding/json" "fmt" + "io" "net/http" "platform-api/src/internal/dto" "regexp" @@ -115,6 +116,37 @@ func (c *BitbucketClient) FetchRepoBranches(owner, repo string) (*dto.GitRepoBra return response, nil } +// FetchFileContent fetches the content of a specific file from a Bitbucket repository +func (c *BitbucketClient) FetchFileContent(owner, repo, branch, path string) ([]byte, error) { + apiURL := fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s/src/%s/%s", owner, repo, branch, path) + + resp, err := c.httpClient.Get(apiURL) + if err != nil { + return nil, fmt.Errorf("failed to fetch file content: %w", err) + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + // Continue processing + case http.StatusNotFound: + return nil, fmt.Errorf("file not found: %s", path) + case http.StatusForbidden: + return nil, fmt.Errorf("access forbidden - repository may be private or rate limit exceeded") + case http.StatusUnauthorized: + return nil, fmt.Errorf("unauthorized access - repository may be private") + default: + return nil, fmt.Errorf("unexpected response status: %d", resp.StatusCode) + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read file content: %w", err) + } + + return content, nil +} + // FetchRepoContent fetches the contents of a Bitbucket repository branch func (c *BitbucketClient) FetchRepoContent(owner, repo, branch string) (*dto.GitRepoContentResponse, error) { // Build tree structure recursively diff --git a/platform-api/src/internal/service/git_github_client.go b/platform-api/src/internal/service/git_github_client.go index 9995887e..cd21a465 100644 --- a/platform-api/src/internal/service/git_github_client.go +++ b/platform-api/src/internal/service/git_github_client.go @@ -18,8 +18,10 @@ package service import ( + "encoding/base64" "encoding/json" "fmt" + "io" "log" "net/http" "platform-api/src/internal/dto" @@ -213,6 +215,72 @@ func (c *GitHubClient) FetchRepoContent(owner, repo, branch string) (*dto.GitRep return response, nil } +// FetchFileContent fetches the content of a specific file from a GitHub repository +func (c *GitHubClient) FetchFileContent(owner, repo, branch, path string) ([]byte, error) { + apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s?ref=%s", owner, repo, path, branch) + + resp, err := c.httpClient.Get(apiURL) + if err != nil { + return nil, fmt.Errorf("failed to fetch file content: %w", err) + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + // Continue processing + case http.StatusNotFound: + return nil, fmt.Errorf("file not found: %s", path) + case http.StatusForbidden: + return nil, fmt.Errorf("access forbidden - repository may be private or rate limit exceeded") + case http.StatusUnauthorized: + return nil, fmt.Errorf("unauthorized access - repository may be private") + default: + return nil, fmt.Errorf("unexpected response status: %d", resp.StatusCode) + } + + var fileResponse struct { + Content string `json:"content"` + Encoding string `json:"encoding"` + DownloadURL string `json:"download_url"` + Size int `json:"size"` + } + + if err := json.NewDecoder(resp.Body).Decode(&fileResponse); err != nil { + return nil, fmt.Errorf("failed to parse file content response: %w", err) + } + + // If content is available and base64 encoded, decode it + if fileResponse.Content != "" && fileResponse.Encoding == "base64" { + decoded, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(fileResponse.Content, "\n", "")) + if err != nil { + return nil, fmt.Errorf("failed to decode base64 content: %w", err) + } + return decoded, nil + } + + // If content is not available (large files), use download_url + if fileResponse.DownloadURL != "" { + downloadResp, err := c.httpClient.Get(fileResponse.DownloadURL) + if err != nil { + return nil, fmt.Errorf("failed to download file from raw URL: %w", err) + } + defer downloadResp.Body.Close() + + if downloadResp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to download file, status: %d", downloadResp.StatusCode) + } + + content, err := io.ReadAll(downloadResp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read downloaded content: %w", err) + } + return content, nil + } + + // If neither content nor download_url is available + return nil, fmt.Errorf("file content not available in GitHub API response") +} + // extractNextLink parses the GitHub Link header to find the next page URL func (c *GitHubClient) extractNextLink(linkHeader string) string { if linkHeader == "" { diff --git a/platform-api/src/internal/service/git_gitlab_client.go b/platform-api/src/internal/service/git_gitlab_client.go index 5b542b76..3b510bc9 100644 --- a/platform-api/src/internal/service/git_gitlab_client.go +++ b/platform-api/src/internal/service/git_gitlab_client.go @@ -257,3 +257,9 @@ func (c *GitLabClient) ValidateName(name string) bool { validPattern := regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$`) return validPattern.MatchString(name) } + +// FetchFileContent fetches the content of a specific file from a GitLab repository +// TODO: Implement proper GitLab file content fetching +func (c *GitLabClient) FetchFileContent(owner, repo, branch, path string) ([]byte, error) { + return nil, fmt.Errorf("FetchFileContent not implemented for GitLab client - only GitHub is currently supported") +} diff --git a/platform-api/src/internal/service/git_provider.go b/platform-api/src/internal/service/git_provider.go index 3abcb02a..33454149 100644 --- a/platform-api/src/internal/service/git_provider.go +++ b/platform-api/src/internal/service/git_provider.go @@ -38,6 +38,7 @@ const ( type GitProviderClient interface { FetchRepoBranches(owner, repo string) (*dto.GitRepoBranchesResponse, error) FetchRepoContent(owner, repo, branch string) (*dto.GitRepoContentResponse, error) + FetchFileContent(owner, repo, branch, path string) ([]byte, error) ParseRepoURL(repoURL string) (owner, repo string, err error) ValidateName(name string) bool GetProvider() GitProvider diff --git a/platform-api/src/internal/utils/api.go b/platform-api/src/internal/utils/api.go index 65cb9992..b2071b1a 100644 --- a/platform-api/src/internal/utils/api.go +++ b/platform-api/src/internal/utils/api.go @@ -840,3 +840,87 @@ func (u *APIUtil) GenerateOpenAPIDefinition(api *dto.API) ([]byte, error) { return apiDefinition, nil } + +// APIYAMLData2ToDTO converts APIYAMLData2 to API DTO +// +// This function maps the fields from APIYAMLData2 (simplified YAML structure) +// to the complete API DTO structure. Fields that don't exist in APIYAMLData2 +// are left with their zero values and should be populated by the caller. +// +// Parameters: +// - yamlData: The APIYAMLData2 source data +// +// Returns: +// - *dto.API: Converted API DTO with mapped fields +func (u *APIUtil) APIYAMLData2ToDTO(yamlData *dto.APIYAMLData2) *dto.API { + if yamlData == nil { + return nil + } + + // Convert upstreams to backend services if present + var backendServices []dto.BackendService + if len(yamlData.Upstreams) > 0 { + backendServices = make([]dto.BackendService, len(yamlData.Upstreams)) + for i, upstream := range yamlData.Upstreams { + backendServices[i] = dto.BackendService{ + IsDefault: i == 0, // First backend service is default + Endpoints: []dto.BackendEndpoint{ + { + URL: upstream.URL, + Description: upstream.Description, + Weight: upstream.Weight, + HealthCheck: upstream.HealthCheck, + MTLS: upstream.MTLS, + }, + }, + } + } + } + + // Convert operations if present + var operations []dto.Operation + if len(yamlData.Operations) > 0 { + operations = make([]dto.Operation, len(yamlData.Operations)) + for i, op := range yamlData.Operations { + operations[i] = dto.Operation{ + Name: fmt.Sprintf("Operation-%d", i+1), + Description: fmt.Sprintf("Operation for %s %s", op.Method, op.Path), + Request: &dto.OperationRequest{ + Method: op.Method, + Path: op.Path, + }, + } + } + } + + // Create and populate API DTO with available fields + api := &dto.API{ + ID: yamlData.Id, + Name: yamlData.Name, + DisplayName: yamlData.DisplayName, + Description: yamlData.Description, + Context: yamlData.Context, + Version: yamlData.Version, + Provider: yamlData.Provider, + BackendServices: backendServices, + Operations: operations, + + // Set reasonable defaults for required fields that aren't in APIYAMLData2 + LifeCycleStatus: "CREATED", + Type: "HTTP", + Transport: []string{"http", "https"}, + HasThumbnail: false, + IsDefaultVersion: false, + IsRevision: false, + RevisionID: 0, + + // Fields that need to be set by caller: + // - ProjectID (required) + // - OrganizationID (required) + // - CreatedAt, UpdatedAt (timestamps) + // - RevisionedAPIID (if applicable) + // - MTLS, Security, CORS, APIRateLimiting configs + } + + return api +} diff --git a/platform-api/src/resources/openapi.yaml b/platform-api/src/resources/openapi.yaml index b1c837be..e0c87f85 100644 --- a/platform-api/src/resources/openapi.yaml +++ b/platform-api/src/resources/openapi.yaml @@ -230,6 +230,41 @@ paths: '500': $ref: '#/components/responses/InternalServerError' + /import/api-project: + post: + summary: Import API project + description: | + Imports an API project into the platform from a provided Git Repository URL. + An API is created from the imported API project and is associated with a specified project within the + organization in the JWT token. + operationId: ImportAPIProject + tags: + - APIs + requestBody: + description: API project import details + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportAPIProjectRequest' + responses: + '201': + description: API project imported successfully + content: + application/json: + schema: + $ref: '#/components/schemas/API' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '500': + $ref: '#/components/responses/InternalServerError' + /apis: get: summary: Get all APIs for an organization @@ -2421,6 +2456,43 @@ components: format: date-time description: Timestamp when API was unpublished example: "2025-10-29T21:08:59.834784232+05:30" + + ImportAPIProjectRequest: + type: object + required: + - repoUrl + - branch + - path + - api + properties: + repoUrl: + type: string + format: uri + description: URL of the public Git repository + example: "https://github.com/example-org/sample-api" + provider: + type: string + enum: [ "github", "gitlab", "bitbucket" ] + description: Git provider (optional - will be auto-detected if not provided) + example: "github" + branch: + type: string + description: Branch of the repository to import from + example: "main" + path: + type: string + description: Path within the repository where the API project is located + example: "apis/inventory-api" + api: + allOf: + - $ref: '#/components/schemas/API' + - type: object + description: API details for the imported project + required: + - name + - context + - version + - projectId responses: Unauthorized: From 3b7e5c6573a57f8da1ee0ef4cbd2dd08cd84a6b0 Mon Sep 17 00:00:00 2001 From: thivindu Date: Thu, 13 Nov 2025 12:05:54 +0530 Subject: [PATCH 2/8] Validate API Project imported from Github repo --- platform-api/src/go.mod | 46 ++++-- platform-api/src/go.sum | 98 +++++++++--- platform-api/src/internal/constants/error.go | 6 +- platform-api/src/internal/dto/api-project.go | 17 +++ platform-api/src/internal/dto/api.go | 6 +- platform-api/src/internal/handler/api.go | 44 ++++++ platform-api/src/internal/service/api.go | 78 ++++++++++ platform-api/src/internal/service/git.go | 1 + platform-api/src/internal/utils/api.go | 152 +++++++++++++++++++ platform-api/src/resources/openapi.yaml | 101 ++++++++++++ 10 files changed, 515 insertions(+), 34 deletions(-) diff --git a/platform-api/src/go.mod b/platform-api/src/go.mod index 80167e35..af3f6a98 100644 --- a/platform-api/src/go.mod +++ b/platform-api/src/go.mod @@ -1,10 +1,14 @@ module platform-api/src -go 1.24 +go 1.24.0 + +toolchain go1.24.7 require ( + github.com/getkin/kin-openapi v0.133.0 github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.11.0 + github.com/go-openapi/loads v0.23.2 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 @@ -19,31 +23,55 @@ require ( github.com/cloudwego/base64x v0.1.6 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-openapi/analysis v0.24.0 // indirect + github.com/go-openapi/errors v0.22.4 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.3 // indirect + github.com/go-openapi/spec v0.22.1 // indirect + github.com/go-openapi/strfmt v0.25.0 // indirect + github.com/go-openapi/swag/conv v0.25.1 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/swag/jsonutils v0.25.1 // indirect + github.com/go-openapi/swag/loading v0.25.1 // indirect + github.com/go-openapi/swag/mangling v0.25.1 // indirect + github.com/go-openapi/swag/stringutils v0.25.1 // indirect + github.com/go-openapi/swag/typeutils v0.25.1 // indirect + github.com/go-openapi/swag/yamlutils v0.25.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.18.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/oklog/ulid v1.3.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect + go.mongodb.org/mongo-driver v1.17.6 // indirect go.uber.org/mock v0.5.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.40.0 // indirect - golang.org/x/mod v0.25.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.27.0 // indirect - golang.org/x/tools v0.34.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.37.0 // indirect google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/platform-api/src/go.sum b/platform-api/src/go.sum index 5be960bb..b86239a3 100644 --- a/platform-api/src/go.sum +++ b/platform-api/src/go.sum @@ -4,18 +4,54 @@ github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZw github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7BdIXhsA= +github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw= +github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM= +github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= +github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= +github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4= +github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY= +github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k= +github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA= +github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= +github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= +github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= +github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg= +github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= +github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= +github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync= +github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ= +github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= +github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= +github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= +github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= +github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= +github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -24,6 +60,10 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= @@ -37,18 +77,22 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= @@ -58,16 +102,26 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -81,25 +135,31 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/platform-api/src/internal/constants/error.go b/platform-api/src/internal/constants/error.go index 596bb17e..d69d360a 100644 --- a/platform-api/src/internal/constants/error.go +++ b/platform-api/src/internal/constants/error.go @@ -88,7 +88,7 @@ var ( ErrAPIProjectNotFound = errors.New("api project not found") ErrMalformedAPIProject = errors.New("malformed api project") ErrInvalidAPIProject = errors.New("invalid api project") - ErrConfigFileNotFound = errors.New("config.yaml file not found") - ErrOpenAPIFileNotFound = errors.New("openapi file not found") - ErrWSO2ArtifactNotFound = errors.New("wso2 artifact file not found") + ErrConfigFileNotFound = errors.New("API Project config file not found") + ErrOpenAPIFileNotFound = errors.New("OpenAPI definition file not found") + ErrWSO2ArtifactNotFound = errors.New("WSO2 API artifact not found") ) diff --git a/platform-api/src/internal/dto/api-project.go b/platform-api/src/internal/dto/api-project.go index 60a47906..f56736ef 100644 --- a/platform-api/src/internal/dto/api-project.go +++ b/platform-api/src/internal/dto/api-project.go @@ -48,3 +48,20 @@ type SpectralRuleset struct { FileName string `yaml:"fileName"` RulesetContentPath string `yaml:"rulesetContentPath"` } + +// ValidateAPIProjectRequest represents the request payload for validating an API project from Git +type ValidateAPIProjectRequest struct { + RepoURL string `json:"repoUrl" binding:"required"` + Provider string `json:"provider,omitempty"` // Optional: "github", "gitlab", "bitbucket", etc. + Branch string `json:"branch" binding:"required"` + Path string `json:"path" binding:"required"` +} + +// APIProjectValidationResponse represents the response for API project validation +type APIProjectValidationResponse struct { + IsAPIProjectValid bool `json:"isAPIProjectValid"` + IsAPIConfigValid bool `json:"isAPIConfigValid"` + IsAPIDefinitionValid bool `json:"isAPIDefinitionValid"` + Errors []string `json:"errors,omitempty"` + API *API `json:"api,omitempty"` +} diff --git a/platform-api/src/internal/dto/api.go b/platform-api/src/internal/dto/api.go index c5788064..4b53dc65 100644 --- a/platform-api/src/internal/dto/api.go +++ b/platform-api/src/internal/dto/api.go @@ -181,9 +181,9 @@ type APIRevisionDeployment struct { // APIDeploymentYAML represents the API deployment YAML structure type APIDeploymentYAML struct { - Kind string `yaml:"kind"` - Version string `yaml:"version"` - Data APIYAMLData2 `yaml:"data"` + Kind string `yaml:"kind" binding:"required"` + Version string `yaml:"version" binding:"required"` + Data APIYAMLData2 `yaml:"data" binding:"required"` } // APIYAMLData2 represents a basic data section of the API deployment YAML diff --git a/platform-api/src/internal/handler/api.go b/platform-api/src/internal/handler/api.go index ad47b8a5..393abc5d 100644 --- a/platform-api/src/internal/handler/api.go +++ b/platform-api/src/internal/handler/api.go @@ -728,6 +728,46 @@ func (h *APIHandler) ImportAPIProject(c *gin.Context) { c.JSON(http.StatusCreated, api) } +// ValidateAPIProject handles POST /validate/api-project +func (h *APIHandler) ValidateAPIProject(c *gin.Context) { + var req dto.ValidateAPIProjectRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + return + } + + // Validate required fields + if req.RepoURL == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Repository URL is required")) + return + } + if req.Branch == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Branch is required")) + return + } + if req.Path == "" { + c.JSON(http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Path is required")) + return + } + + // Create Git service + gitService := service.NewGitService() + + // Validate API project + response, err := h.apiService.ValidateAPIProject(&req, gitService) + if err != nil { + c.JSON(http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to validate API project")) + return + } + + // Return validation response (200 OK even if validation fails - errors are in the response body) + c.JSON(http.StatusOK, response) +} + // RegisterRoutes registers all API routes func (h *APIHandler) RegisterRoutes(r *gin.Engine) { // API routes @@ -749,4 +789,8 @@ func (h *APIHandler) RegisterRoutes(r *gin.Engine) { { importGroup.POST("/api-project", h.ImportAPIProject) } + validateGroup := r.Group("/api/v1/validate") + { + validateGroup.POST("/api-project", h.ValidateAPIProject) + } } diff --git a/platform-api/src/internal/service/api.go b/platform-api/src/internal/service/api.go index 67cc5ec0..efa1c5db 100644 --- a/platform-api/src/internal/service/api.go +++ b/platform-api/src/internal/service/api.go @@ -32,6 +32,7 @@ import ( "platform-api/src/internal/constants" "github.com/google/uuid" + "gopkg.in/yaml.v3" ) // APIService handles business logic for API operations @@ -1063,6 +1064,83 @@ func (s *APIService) mergeAPIData(artifact *dto.APIYAMLData2, userAPIData *dto.A return apiDTO } +// ValidateAPIProject validates an API project from Git repository with comprehensive checks +func (s *APIService) ValidateAPIProject(req *dto.ValidateAPIProjectRequest, gitService GitService) (*dto.APIProjectValidationResponse, error) { + response := &dto.APIProjectValidationResponse{ + IsAPIProjectValid: false, + IsAPIConfigValid: false, + IsAPIDefinitionValid: false, + Errors: []string{}, + } + + // Step 1: Check if .api-platform directory exists and validate config + config, err := gitService.ValidateAPIProject(req.RepoURL, req.Branch, req.Path) + if err != nil { + response.Errors = append(response.Errors, err.Error()) + return response, nil + } + + // Process the first API entry (assuming single API per project for now) + apiEntry := config.APIs[0] + + // Step 3: Fetch and validate OpenAPI definition + openAPIPath := req.Path + "/" + apiEntry.OpenAPI + openAPIContent, err := gitService.FetchFileContent(req.RepoURL, req.Branch, openAPIPath) + if err != nil { + response.Errors = append(response.Errors, fmt.Sprintf("failed to fetch OpenAPI file: %s", err.Error())) + return response, nil + } + + // Basic OpenAPI validation (check if it's valid YAML/JSON with required fields) + if err := s.apiUtil.ValidateOpenAPIDefinition(openAPIContent); err != nil { + response.Errors = append(response.Errors, fmt.Sprintf("invalid OpenAPI definition: %s", err.Error())) + return response, nil + } + + response.IsAPIDefinitionValid = true + + // Step 4: Fetch and validate WSO2 artifact + wso2ArtifactPath := req.Path + "/" + apiEntry.WSO2Artifact + wso2ArtifactContent, err := gitService.FetchFileContent(req.RepoURL, req.Branch, wso2ArtifactPath) + if err != nil { + response.Errors = append(response.Errors, fmt.Sprintf("failed to fetch WSO2 artifact file: %s", err.Error())) + return response, nil + } + + var wso2Artifact dto.APIDeploymentYAML + if err := yaml.Unmarshal(wso2ArtifactContent, &wso2Artifact); err != nil { + response.Errors = append(response.Errors, fmt.Sprintf("invalid WSO2 artifact format: %s", err.Error())) + return response, nil + } + + // Step 5: Validate WSO2 artifact structure + if err := s.apiUtil.ValidateWSO2Artifact(&wso2Artifact); err != nil { + response.Errors = append(response.Errors, fmt.Sprintf("invalid WSO2 artifact: %s", err.Error())) + return response, nil + } + + response.IsAPIConfigValid = true + + // Step 6: Check if OpenAPI and WSO2 artifact match (optional validation) + if err := s.apiUtil.ValidateAPIDefinitionConsistency(openAPIContent, &wso2Artifact); err != nil { + response.Errors = append(response.Errors, fmt.Sprintf("API definitions mismatch: %s", err.Error())) + response.IsAPIProjectValid = false + return response, nil + } + + // Step 7: If all validations pass, convert to API DTO + api, err := s.apiUtil.ConvertAPIYAMLDataToDTO(&wso2Artifact) + if err != nil { + response.Errors = append(response.Errors, fmt.Sprintf("failed to convert API data: %s", err.Error())) + return response, nil + } + + response.API = api + response.IsAPIProjectValid = response.IsAPIConfigValid && response.IsAPIDefinitionValid + + return response, nil +} + // PublishAPIToDevPortal publishes an API to a specific DevPortal func (s *APIService) PublishAPIToDevPortal(apiID string, req *dto.PublishToDevPortalRequest, orgID string) (*dto.PublishToDevPortalResponse, error) { // Get the API diff --git a/platform-api/src/internal/service/git.go b/platform-api/src/internal/service/git.go index 9b10e901..202900a1 100644 --- a/platform-api/src/internal/service/git.go +++ b/platform-api/src/internal/service/git.go @@ -141,6 +141,7 @@ func (s *gitService) ValidateAPIProject(repoURL, branch, path string) (*dto.APIP return nil, fmt.Errorf("api project not found: .api-platform directory or config.yaml not found") } + // 2. Parse config.yaml var config dto.APIProjectConfig if err := yaml.Unmarshal(configContent, &config); err != nil { return nil, fmt.Errorf("malformed api project: invalid config.yaml format") diff --git a/platform-api/src/internal/utils/api.go b/platform-api/src/internal/utils/api.go index b2071b1a..bdeffdea 100644 --- a/platform-api/src/internal/utils/api.go +++ b/platform-api/src/internal/utils/api.go @@ -22,6 +22,8 @@ import ( "fmt" "strings" + "github.com/getkin/kin-openapi/openapi3" + "github.com/go-openapi/loads" "gopkg.in/yaml.v3" "platform-api/src/internal/constants" "platform-api/src/internal/dto" @@ -841,6 +843,15 @@ func (u *APIUtil) GenerateOpenAPIDefinition(api *dto.API) ([]byte, error) { return apiDefinition, nil } +// ConvertAPIYAMLDataToDTO converts APIDeploymentYAML to API DTO +func (u *APIUtil) ConvertAPIYAMLDataToDTO(artifact *dto.APIDeploymentYAML) (*dto.API, error) { + if artifact == nil { + return nil, fmt.Errorf("invalid artifact data") + } + + return u.APIYAMLData2ToDTO(&artifact.Data), nil +} + // APIYAMLData2ToDTO converts APIYAMLData2 to API DTO // // This function maps the fields from APIYAMLData2 (simplified YAML structure) @@ -924,3 +935,144 @@ func (u *APIUtil) APIYAMLData2ToDTO(yamlData *dto.APIYAMLData2) *dto.API { return api } + +// Validation functions for OpenAPI specifications and WSO2 artifacts + +// ValidateOpenAPIDefinition performs comprehensive validation on OpenAPI content using professional libraries +func (u *APIUtil) ValidateOpenAPIDefinition(content []byte) error { + // First, try to determine if it's OpenAPI 3.x or Swagger 2.0 + var rawDoc map[string]interface{} + if err := yaml.Unmarshal(content, &rawDoc); err != nil { + return fmt.Errorf("invalid YAML/JSON format: %s", err.Error()) + } + + // Check if it's OpenAPI 3.x + if openapi, exists := rawDoc["openapi"]; exists { + if openapiStr, ok := openapi.(string); ok && strings.HasPrefix(openapiStr, "3.") { + return u.ValidateOpenAPI3(content) + } + } + + // Check if it's Swagger 2.0 + if swagger, exists := rawDoc["swagger"]; exists { + if swaggerStr, ok := swagger.(string); ok && strings.HasPrefix(swaggerStr, "2.") { + return u.ValidateSwagger2(content) + } + } + + return fmt.Errorf("unsupported API specification format: must be OpenAPI 3.x or Swagger 2.0") +} + +// ValidateOpenAPI3 validates OpenAPI 3.x specifications using getkin/kin-openapi +func (u *APIUtil) ValidateOpenAPI3(content []byte) error { + loader := &openapi3.Loader{IsExternalRefsAllowed: true} + + // Load and parse the OpenAPI 3.x document + doc, err := loader.LoadFromData(content) + if err != nil { + return fmt.Errorf("invalid OpenAPI 3.x specification: %s", err.Error()) + } + + // Validate the OpenAPI 3.x document + if err := doc.Validate(loader.Context); err != nil { + return fmt.Errorf("OpenAPI 3.x validation failed: %s", err.Error()) + } + + // Additional basic checks for required fields + if doc.Info == nil { + return fmt.Errorf("missing required field: info") + } + + if doc.Info.Title == "" { + return fmt.Errorf("missing required field: info.title") + } + + if doc.Info.Version == "" { + return fmt.Errorf("missing required field: info.version") + } + + return nil +} + +// ValidateSwagger2 validates Swagger 2.0 specifications using go-openapi/loads +func (u *APIUtil) ValidateSwagger2(content []byte) error { + // Use go-openapi/loads for proper Swagger 2.0 validation + doc, err := loads.Analyzed(content, "") + if err != nil { + return fmt.Errorf("invalid Swagger 2.0 specification: %s", err.Error()) + } + + // The loads.Analyzed function automatically validates the Swagger spec + // including schema validation, reference resolution, and structural validation + + // Additional checks for required fields + if doc.Spec() == nil { + return fmt.Errorf("missing specification data") + } + + spec := doc.Spec() + + if spec.Info == nil { + return fmt.Errorf("missing required field: info") + } + + if spec.Info.Title == "" { + return fmt.Errorf("missing required field: info.title") + } + + if spec.Info.Version == "" { + return fmt.Errorf("missing required field: info.version") + } + + if spec.Swagger == "" { + return fmt.Errorf("missing required field: swagger version") + } + + // Validate that it's a proper 2.0 version + if !strings.HasPrefix(spec.Swagger, "2.") { + return fmt.Errorf("invalid swagger version: %s, expected 2.x", spec.Swagger) + } + + return nil +} + +// ValidateWSO2Artifact validates the structure of WSO2 artifact +func (u *APIUtil) ValidateWSO2Artifact(artifact *dto.APIDeploymentYAML) error { + if artifact.Data.Name == "" { + return fmt.Errorf("missing API name") + } + + if artifact.Data.Context == "" { + return fmt.Errorf("missing API context") + } + + if artifact.Data.Version == "" { + return fmt.Errorf("missing API version") + } + + return nil +} + +// ValidateAPIDefinitionConsistency checks if OpenAPI and WSO2 artifact are consistent +func (u *APIUtil) ValidateAPIDefinitionConsistency(openAPIContent []byte, wso2Artifact *dto.APIDeploymentYAML) error { + var openAPIDoc map[string]interface{} + if err := yaml.Unmarshal(openAPIContent, &openAPIDoc); err != nil { + return fmt.Errorf("failed to parse OpenAPI document") + } + + // Extract info from OpenAPI + info, exists := openAPIDoc["info"].(map[string]interface{}) + if !exists { + return fmt.Errorf("missing info section in OpenAPI") + } + + // Check version consistency + if version, exists := info["version"].(string); exists { + if version != wso2Artifact.Data.Version { + return fmt.Errorf("version mismatch between OpenAPI (%s) and WSO2 artifact (%s)", + version, wso2Artifact.Data.Version) + } + } + + return nil +} diff --git a/platform-api/src/resources/openapi.yaml b/platform-api/src/resources/openapi.yaml index e0c87f85..27051fbb 100644 --- a/platform-api/src/resources/openapi.yaml +++ b/platform-api/src/resources/openapi.yaml @@ -230,6 +230,41 @@ paths: '500': $ref: '#/components/responses/InternalServerError' + /validate/api-project: + post: + summary: Validate API project + description: | + Validates an API project into the platform from a provided Git Repository URL. + This endpoint checks the validity of the API project definitions and the overall validity of the API. + Returns the metadata of the API and the resources if the API project is valid. + operationId: ValidateAPIProject + tags: + - APIs + requestBody: + description: API project validation details + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ValidateAPIProjectRequest' + responses: + '201': + description: API project is valid + content: + application/json: + schema: + $ref: '#/components/schemas/APIProjectValidationResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '500': + $ref: '#/components/responses/InternalServerError' + /import/api-project: post: summary: Import API project @@ -2456,6 +2491,72 @@ components: format: date-time description: Timestamp when API was unpublished example: "2025-10-29T21:08:59.834784232+05:30" + + ValidateAPIProjectRequest: + type: object + required: + - repoUrl + - branch + - path + properties: + repoUrl: + type: string + format: uri + description: URL of the public Git repository + example: "https://github.com/example-org/sample-api" + provider: + type: string + enum: [ "github", "gitlab", "bitbucket" ] + description: Git provider (optional - will be auto-detected if not provided) + example: "github" + branch: + type: string + description: Branch of the repository to import from + example: "main" + path: + type: string + description: Path within the repository where the API project is located + example: "apis/inventory-api" + + APIProjectValidationResponse: + type: object + required: + - isAPIProjectValid + - isAPIConfigValid + - isAPIDefinitionValid + properties: + isAPIProjectValid: + type: boolean + description: Indicates if the API project structure is valid + example: true + isAPIConfigValid: + type: boolean + description: Indicates if the API configuration file is valid + example: true + isAPIDefinitionValid: + type: boolean + description: Indicates if the API definition file is valid + example: true + errors: + type: array + description: List of validation errors encountered + items: + type: string + example: [ "Missing API definition file", "Invalid API configuration format" ] + api: + allOf: + - $ref: '#/components/schemas/API' + - type: object + description: Details for the validated API project + required: + - name + - displayName + - description + - context + - version + - backend-services + - projectId + - operations ImportAPIProjectRequest: type: object From 23945a63a2c5df3b1a5995ec7836d598cadda689 Mon Sep 17 00:00:00 2001 From: thivindu Date: Thu, 13 Nov 2025 14:55:22 +0530 Subject: [PATCH 3/8] Normalize Git repo file paths --- platform-api/src/internal/service/git.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/platform-api/src/internal/service/git.go b/platform-api/src/internal/service/git.go index 202900a1..bd315daf 100644 --- a/platform-api/src/internal/service/git.go +++ b/platform-api/src/internal/service/git.go @@ -20,7 +20,9 @@ package service import ( "fmt" "gopkg.in/yaml.v3" + pathpkg "path" "platform-api/src/internal/dto" + "strings" ) type GitService interface { @@ -123,8 +125,12 @@ func (s *gitService) FetchFileContent(repoURL, branch, path string) ([]byte, err return nil, fmt.Errorf("unsupported Git provider: %s", repoInfo.Provider) } + normalizedPath := strings.TrimSpace(path) + normalizedPath = strings.TrimPrefix(normalizedPath, "/") + normalizedPath = strings.TrimPrefix(pathpkg.Clean("/"+normalizedPath), "/") + // Use the provider-specific client to fetch file content - content, err := providerClient.FetchFileContent(repoInfo.Owner, repoInfo.Repo, branch, path) + content, err := providerClient.FetchFileContent(repoInfo.Owner, repoInfo.Repo, branch, normalizedPath) if err != nil { return nil, err } From bf346bddf9046563e72e3d7d6935ea7558039eb6 Mon Sep 17 00:00:00 2001 From: thivindu Date: Thu, 13 Nov 2025 15:15:36 +0530 Subject: [PATCH 4/8] Address CodeRabbit comments --- platform-api/src/internal/service/git.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/platform-api/src/internal/service/git.go b/platform-api/src/internal/service/git.go index bd315daf..cd6b9bfe 100644 --- a/platform-api/src/internal/service/git.go +++ b/platform-api/src/internal/service/git.go @@ -141,8 +141,8 @@ func (s *gitService) FetchFileContent(repoURL, branch, path string) ([]byte, err // ValidateAPIProject validates an API project structure in a Git repository func (s *gitService) ValidateAPIProject(repoURL, branch, path string) (*dto.APIProjectConfig, error) { // 1. Check if .api-platform directory exists - apiPlatformPath := path + "/.api-platform" - configContent, err := s.FetchFileContent(repoURL, branch, apiPlatformPath+"/config.yaml") + configPath := pathpkg.Join(path, ".api-platform", "config.yaml") + configContent, err := s.FetchFileContent(repoURL, branch, configPath) if err != nil { return nil, fmt.Errorf("api project not found: .api-platform directory or config.yaml not found") } @@ -164,13 +164,13 @@ func (s *gitService) ValidateAPIProject(repoURL, branch, path string) (*dto.APIP } // 4. Check if the referenced files exist in the project path - openAPIPath := path + "/" + api.OpenAPI + openAPIPath := pathpkg.Join(path, api.OpenAPI) _, err := s.FetchFileContent(repoURL, branch, openAPIPath) if err != nil { return nil, fmt.Errorf("invalid api project: openapi file not found: %s", api.OpenAPI) } - wso2ArtifactPath := path + "/" + api.WSO2Artifact + wso2ArtifactPath := pathpkg.Join(path, api.WSO2Artifact) _, err = s.FetchFileContent(repoURL, branch, wso2ArtifactPath) if err != nil { return nil, fmt.Errorf("invalid api project: wso2 artifact file not found: %s", api.WSO2Artifact) @@ -184,12 +184,12 @@ func (s *gitService) ValidateAPIProject(repoURL, branch, path string) (*dto.APIP func (s *gitService) FetchWSO2Artifact(repoURL, branch, path string) (*dto.APIDeploymentYAML, error) { content, err := s.FetchFileContent(repoURL, branch, path) if err != nil { - return nil, fmt.Errorf("failed to fetch WSO2 artifact file: %w", err) + return nil, fmt.Errorf("failed to fetch WSO2 artifact file at %s: %w", path, err) } var artifact dto.APIDeploymentYAML if err := yaml.Unmarshal(content, &artifact); err != nil { - return nil, fmt.Errorf("failed to parse WSO2 artifact file: %w", err) + return nil, fmt.Errorf("failed to parse WSO2 artifact file at %s: %w", path, err) } return &artifact, nil From be0b9d48b67e2ba76feb3e316c233fa4d551f119 Mon Sep 17 00:00:00 2001 From: thivindu Date: Thu, 13 Nov 2025 15:28:01 +0530 Subject: [PATCH 5/8] Sanitize file paths of the API project traversal --- platform-api/src/internal/service/git.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/platform-api/src/internal/service/git.go b/platform-api/src/internal/service/git.go index cd6b9bfe..0ecce567 100644 --- a/platform-api/src/internal/service/git.go +++ b/platform-api/src/internal/service/git.go @@ -163,14 +163,24 @@ func (s *gitService) ValidateAPIProject(repoURL, branch, path string) (*dto.APIP return nil, fmt.Errorf("malformed api project: apis.openapi and apis.wso2Artifact fields are required") } + // Sanitize paths to prevent traversal attacks + openAPIClean := pathpkg.Clean(api.OpenAPI) + if strings.HasPrefix(openAPIClean, "..") || pathpkg.IsAbs(openAPIClean) { + return nil, fmt.Errorf("malformed api project: invalid openapi path: %s", api.OpenAPI) + } + wso2ArtifactClean := pathpkg.Clean(api.WSO2Artifact) + if strings.HasPrefix(wso2ArtifactClean, "..") || pathpkg.IsAbs(wso2ArtifactClean) { + return nil, fmt.Errorf("malformed api project: invalid wso2Artifact path: %s", api.WSO2Artifact) + } + // 4. Check if the referenced files exist in the project path - openAPIPath := pathpkg.Join(path, api.OpenAPI) + openAPIPath := pathpkg.Join(path, openAPIClean) _, err := s.FetchFileContent(repoURL, branch, openAPIPath) if err != nil { return nil, fmt.Errorf("invalid api project: openapi file not found: %s", api.OpenAPI) } - wso2ArtifactPath := pathpkg.Join(path, api.WSO2Artifact) + wso2ArtifactPath := pathpkg.Join(path, wso2ArtifactClean) _, err = s.FetchFileContent(repoURL, branch, wso2ArtifactPath) if err != nil { return nil, fmt.Errorf("invalid api project: wso2 artifact file not found: %s", api.WSO2Artifact) @@ -192,5 +202,10 @@ func (s *gitService) FetchWSO2Artifact(repoURL, branch, path string) (*dto.APIDe return nil, fmt.Errorf("failed to parse WSO2 artifact file at %s: %w", path, err) } + // Validate required fields + if artifact.Kind == "" || artifact.Version == "" { + return nil, fmt.Errorf("malformed WSO2 artifact at %s: kind and version are required", path) + } + return &artifact, nil } From 8c10bdafdbdd893b6ebec43f3ea5a9586ae58569 Mon Sep 17 00:00:00 2001 From: thivindu Date: Thu, 13 Nov 2025 17:20:06 +0530 Subject: [PATCH 6/8] Use libopenapi to parse and validate OpenAPI definitions --- platform-api/src/go.mod | 38 ++----- platform-api/src/go.sum | 73 ++---------- platform-api/src/internal/utils/api.go | 148 +++++++++++++++++-------- 3 files changed, 121 insertions(+), 138 deletions(-) diff --git a/platform-api/src/go.mod b/platform-api/src/go.mod index af3f6a98..7b9351fb 100644 --- a/platform-api/src/go.mod +++ b/platform-api/src/go.mod @@ -1,70 +1,48 @@ module platform-api/src -go 1.24.0 - -toolchain go1.24.7 +go 1.24.7 require ( - github.com/getkin/kin-openapi v0.133.0 github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.11.0 - github.com/go-openapi/loads v0.23.2 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/kelseyhightower/envconfig v1.4.0 github.com/mattn/go-sqlite3 v1.14.32 + github.com/pb33f/libopenapi v0.28.2 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-openapi/analysis v0.24.0 // indirect - github.com/go-openapi/errors v0.22.4 // indirect - github.com/go-openapi/jsonpointer v0.22.1 // indirect - github.com/go-openapi/jsonreference v0.21.3 // indirect - github.com/go-openapi/spec v0.22.1 // indirect - github.com/go-openapi/strfmt v0.25.0 // indirect - github.com/go-openapi/swag/conv v0.25.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect - github.com/go-openapi/swag/jsonutils v0.25.1 // indirect - github.com/go-openapi/swag/loading v0.25.1 // indirect - github.com/go-openapi/swag/mangling v0.25.1 // indirect - github.com/go-openapi/swag/stringutils v0.25.1 // indirect - github.com/go-openapi/swag/typeutils v0.25.1 // indirect - github.com/go-openapi/swag/yamlutils v0.25.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.18.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect - github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect - github.com/oklog/ulid v1.3.1 // indirect + github.com/pb33f/jsonpath v0.1.2 // indirect + github.com/pb33f/ordered-map/v2 v2.3.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect - github.com/woodsbury/decimal128 v1.3.0 // indirect - go.mongodb.org/mongo-driver v1.17.6 // indirect go.uber.org/mock v0.5.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect golang.org/x/arch v0.20.0 // indirect golang.org/x/crypto v0.43.0 // indirect golang.org/x/mod v0.28.0 // indirect diff --git a/platform-api/src/go.sum b/platform-api/src/go.sum index b86239a3..ccd0dc91 100644 --- a/platform-api/src/go.sum +++ b/platform-api/src/go.sum @@ -1,3 +1,7 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= @@ -9,49 +13,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= -github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= -github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= -github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7BdIXhsA= -github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw= -github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM= -github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= -github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= -github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= -github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= -github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= -github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4= -github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY= -github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k= -github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA= -github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= -github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= -github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= -github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= -github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= -github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= -github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg= -github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= -github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= -github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync= -github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ= -github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= -github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= -github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= -github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= -github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= -github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= -github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= -github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -60,10 +27,6 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= @@ -77,8 +40,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= @@ -91,8 +52,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= @@ -102,18 +61,14 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= -github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= -github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= -github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pb33f/jsonpath v0.1.2 h1:PlqXjEyecMqoYJupLxYeClCGWEpAFnh4pmzgspbXDPI= +github.com/pb33f/jsonpath v0.1.2/go.mod h1:TtKnUnfqZm48q7a56DxB3WtL3ipkVtukMKGKxaR/uXU= +github.com/pb33f/libopenapi v0.28.2 h1:AXVCE8DWzytXu0jv0Z+cXVopnO/bXU1oWvgA9qiRWgw= +github.com/pb33f/libopenapi v0.28.2/go.mod h1:mHMHA3ZKSZDTInNAuUtqkHlKLIjPm2HN1vgsGR57afc= +github.com/pb33f/ordered-map/v2 v2.3.0 h1:k2OhVEQkhTCQMhAicQ3Z6iInzoZNQ7L9MVomwKBZ5WQ= +github.com/pb33f/ordered-map/v2 v2.3.0/go.mod h1:oe5ue+6ZNhy7QN9cPZvPA23Hx0vMHnNVeMg4fGdCANw= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= -github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= @@ -135,14 +90,10 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= -github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= -go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= -go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= +go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= diff --git a/platform-api/src/internal/utils/api.go b/platform-api/src/internal/utils/api.go index bdeffdea..a933fc50 100644 --- a/platform-api/src/internal/utils/api.go +++ b/platform-api/src/internal/utils/api.go @@ -22,8 +22,9 @@ import ( "fmt" "strings" - "github.com/getkin/kin-openapi/openapi3" - "github.com/go-openapi/loads" + "github.com/pb33f/libopenapi" + v2high "github.com/pb33f/libopenapi/datamodel/high/v2" + v3high "github.com/pb33f/libopenapi/datamodel/high/v3" "gopkg.in/yaml.v3" "platform-api/src/internal/constants" "platform-api/src/internal/dto" @@ -938,47 +939,77 @@ func (u *APIUtil) APIYAMLData2ToDTO(yamlData *dto.APIYAMLData2) *dto.API { // Validation functions for OpenAPI specifications and WSO2 artifacts -// ValidateOpenAPIDefinition performs comprehensive validation on OpenAPI content using professional libraries +// ValidateOpenAPIDefinition performs comprehensive validation on OpenAPI content using libopenapi func (u *APIUtil) ValidateOpenAPIDefinition(content []byte) error { - // First, try to determine if it's OpenAPI 3.x or Swagger 2.0 - var rawDoc map[string]interface{} - if err := yaml.Unmarshal(content, &rawDoc); err != nil { - return fmt.Errorf("invalid YAML/JSON format: %s", err.Error()) + // Create a new document from the content + document, err := libopenapi.NewDocument(content) + if err != nil { + return fmt.Errorf("failed to parse document: %s", err.Error()) } - // Check if it's OpenAPI 3.x - if openapi, exists := rawDoc["openapi"]; exists { - if openapiStr, ok := openapi.(string); ok && strings.HasPrefix(openapiStr, "3.") { - return u.ValidateOpenAPI3(content) - } + // Check the specification version + specInfo := document.GetSpecInfo() + if specInfo == nil { + return fmt.Errorf("unable to determine specification version") } - // Check if it's Swagger 2.0 - if swagger, exists := rawDoc["swagger"]; exists { - if swaggerStr, ok := swagger.(string); ok && strings.HasPrefix(swaggerStr, "2.") { - return u.ValidateSwagger2(content) - } + // Handle different specification versions based on version string + switch { + case specInfo.Version != "" && strings.HasPrefix(specInfo.Version, "3."): + return u.validateOpenAPI3Document(document) + case specInfo.Version != "" && strings.HasPrefix(specInfo.Version, "2."): + return u.validateSwagger2Document(document) + default: + // Try to determine from the document structure + return u.validateDocumentByStructure(document) } - - return fmt.Errorf("unsupported API specification format: must be OpenAPI 3.x or Swagger 2.0") } -// ValidateOpenAPI3 validates OpenAPI 3.x specifications using getkin/kin-openapi -func (u *APIUtil) ValidateOpenAPI3(content []byte) error { - loader := &openapi3.Loader{IsExternalRefsAllowed: true} +// validateDocumentByStructure tries to validate by attempting to build both models +func (u *APIUtil) validateDocumentByStructure(document libopenapi.Document) error { + // Try OpenAPI 3.x first + v3Model, v3Errs := document.BuildV3Model() + if v3Errs == nil && v3Model != nil { + return u.validateOpenAPI3Model(v3Model) + } + + // Try Swagger 2.0 + v2Model, v2Errs := document.BuildV2Model() + if v2Errs == nil && v2Model != nil { + return u.validateSwagger2Model(v2Model) + } - // Load and parse the OpenAPI 3.x document - doc, err := loader.LoadFromData(content) + // Both failed, return error + var errorMessages []string + if v3Errs != nil { + errorMessages = append(errorMessages, "OpenAPI 3.x: "+v3Errs.Error()) + } + if v2Errs != nil { + errorMessages = append(errorMessages, "Swagger 2.0: "+v2Errs.Error()) + } + + return fmt.Errorf("document validation failed: %s", strings.Join(errorMessages, "; ")) +} + +// validateOpenAPI3Document validates OpenAPI 3.x documents using libopenapi +func (u *APIUtil) validateOpenAPI3Document(document libopenapi.Document) error { + // Build the OpenAPI 3.x model + docModel, err := document.BuildV3Model() if err != nil { - return fmt.Errorf("invalid OpenAPI 3.x specification: %s", err.Error()) + return fmt.Errorf("OpenAPI 3.x model build error: %s", err.Error()) } - // Validate the OpenAPI 3.x document - if err := doc.Validate(loader.Context); err != nil { - return fmt.Errorf("OpenAPI 3.x validation failed: %s", err.Error()) + return u.validateOpenAPI3Model(docModel) +} + +// validateOpenAPI3Model validates an OpenAPI 3.x model +func (u *APIUtil) validateOpenAPI3Model(docModel *libopenapi.DocumentModel[v3high.Document]) error { + if docModel == nil { + return fmt.Errorf("invalid OpenAPI 3.x document model") } - // Additional basic checks for required fields + // Get the OpenAPI document + doc := &docModel.Model if doc.Info == nil { return fmt.Errorf("missing required field: info") } @@ -994,48 +1025,71 @@ func (u *APIUtil) ValidateOpenAPI3(content []byte) error { return nil } -// ValidateSwagger2 validates Swagger 2.0 specifications using go-openapi/loads -func (u *APIUtil) ValidateSwagger2(content []byte) error { - // Use go-openapi/loads for proper Swagger 2.0 validation - doc, err := loads.Analyzed(content, "") +// validateSwagger2Document validates Swagger 2.0 documents using libopenapi +func (u *APIUtil) validateSwagger2Document(document libopenapi.Document) error { + // Build the Swagger 2.0 model + docModel, err := document.BuildV2Model() if err != nil { - return fmt.Errorf("invalid Swagger 2.0 specification: %s", err.Error()) + return fmt.Errorf("Swagger 2.0 model build error: %s", err.Error()) } - // The loads.Analyzed function automatically validates the Swagger spec - // including schema validation, reference resolution, and structural validation + return u.validateSwagger2Model(docModel) +} - // Additional checks for required fields - if doc.Spec() == nil { - return fmt.Errorf("missing specification data") +// validateSwagger2Model validates a Swagger 2.0 model +func (u *APIUtil) validateSwagger2Model(docModel *libopenapi.DocumentModel[v2high.Swagger]) error { + if docModel == nil { + return fmt.Errorf("invalid Swagger 2.0 document model") } - spec := doc.Spec() - - if spec.Info == nil { + // Get the Swagger document + doc := &docModel.Model + if doc.Info == nil { return fmt.Errorf("missing required field: info") } - if spec.Info.Title == "" { + if doc.Info.Title == "" { return fmt.Errorf("missing required field: info.title") } - if spec.Info.Version == "" { + if doc.Info.Version == "" { return fmt.Errorf("missing required field: info.version") } - if spec.Swagger == "" { + if doc.Swagger == "" { return fmt.Errorf("missing required field: swagger version") } // Validate that it's a proper 2.0 version - if !strings.HasPrefix(spec.Swagger, "2.") { - return fmt.Errorf("invalid swagger version: %s, expected 2.x", spec.Swagger) + if !strings.HasPrefix(doc.Swagger, "2.") { + return fmt.Errorf("invalid swagger version: %s, expected 2.x", doc.Swagger) } return nil } +// ValidateOpenAPI3 validates OpenAPI 3.x specifications using libopenapi +func (u *APIUtil) ValidateOpenAPI3(content []byte) error { + // Create a new document from the content + document, err := libopenapi.NewDocument(content) + if err != nil { + return fmt.Errorf("failed to parse OpenAPI 3.x document: %s", err.Error()) + } + + return u.validateOpenAPI3Document(document) +} + +// ValidateSwagger2 validates Swagger 2.0 specifications using libopenapi +func (u *APIUtil) ValidateSwagger2(content []byte) error { + // Create a new document from the content + document, err := libopenapi.NewDocument(content) + if err != nil { + return fmt.Errorf("failed to parse Swagger 2.0 document: %s", err.Error()) + } + + return u.validateSwagger2Document(document) +} + // ValidateWSO2Artifact validates the structure of WSO2 artifact func (u *APIUtil) ValidateWSO2Artifact(artifact *dto.APIDeploymentYAML) error { if artifact.Data.Name == "" { From c6e5b56d40d3e92130b902e3f19a70e3188e9ee6 Mon Sep 17 00:00:00 2001 From: thivindu Date: Thu, 13 Nov 2025 19:13:49 +0530 Subject: [PATCH 7/8] Address CodeRabbit comments --- platform-api/src/internal/handler/api.go | 2 +- platform-api/src/internal/service/api.go | 19 ++++++++----- platform-api/src/internal/utils/api.go | 34 +++++++++--------------- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/platform-api/src/internal/handler/api.go b/platform-api/src/internal/handler/api.go index 393abc5d..06ad87e6 100644 --- a/platform-api/src/internal/handler/api.go +++ b/platform-api/src/internal/handler/api.go @@ -757,7 +757,7 @@ func (h *APIHandler) ValidateAPIProject(c *gin.Context) { gitService := service.NewGitService() // Validate API project - response, err := h.apiService.ValidateAPIProject(&req, gitService) + response, err := h.apiService.ValidateAndRetrieveAPIProject(&req, gitService) if err != nil { c.JSON(http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to validate API project")) diff --git a/platform-api/src/internal/service/api.go b/platform-api/src/internal/service/api.go index efa1c5db..f1a8529f 100644 --- a/platform-api/src/internal/service/api.go +++ b/platform-api/src/internal/service/api.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "log" + pathpkg "path" "platform-api/src/internal/dto" "platform-api/src/internal/model" "platform-api/src/internal/repository" @@ -973,7 +974,8 @@ func (s *APIService) ImportAPIProject(req *dto.ImportAPIProjectRequest, orgId st apiConfig := config.APIs[0] // 5. Fetch the WSO2 artifact file content - wso2ArtifactPath := req.Path + "/" + apiConfig.WSO2Artifact + wso2ArtifactClean := pathpkg.Clean(apiConfig.WSO2Artifact) + wso2ArtifactPath := pathpkg.Join(req.Path, wso2ArtifactClean) artifactData, err := gitService.FetchWSO2Artifact(req.RepoURL, req.Branch, wso2ArtifactPath) if err != nil { return nil, constants.ErrWSO2ArtifactNotFound @@ -1064,8 +1066,9 @@ func (s *APIService) mergeAPIData(artifact *dto.APIYAMLData2, userAPIData *dto.A return apiDTO } -// ValidateAPIProject validates an API project from Git repository with comprehensive checks -func (s *APIService) ValidateAPIProject(req *dto.ValidateAPIProjectRequest, gitService GitService) (*dto.APIProjectValidationResponse, error) { +// ValidateAndRetrieveAPIProject validates an API project from Git repository with comprehensive checks +func (s *APIService) ValidateAndRetrieveAPIProject(req *dto.ValidateAPIProjectRequest, + gitService GitService) (*dto.APIProjectValidationResponse, error) { response := &dto.APIProjectValidationResponse{ IsAPIProjectValid: false, IsAPIConfigValid: false, @@ -1083,8 +1086,9 @@ func (s *APIService) ValidateAPIProject(req *dto.ValidateAPIProjectRequest, gitS // Process the first API entry (assuming single API per project for now) apiEntry := config.APIs[0] - // Step 3: Fetch and validate OpenAPI definition - openAPIPath := req.Path + "/" + apiEntry.OpenAPI + // Step 3: Fetch OpenAPI definition + openAPIClean := pathpkg.Clean(apiEntry.OpenAPI) + openAPIPath := pathpkg.Join(req.Path, openAPIClean) openAPIContent, err := gitService.FetchFileContent(req.RepoURL, req.Branch, openAPIPath) if err != nil { response.Errors = append(response.Errors, fmt.Sprintf("failed to fetch OpenAPI file: %s", err.Error())) @@ -1099,8 +1103,9 @@ func (s *APIService) ValidateAPIProject(req *dto.ValidateAPIProjectRequest, gitS response.IsAPIDefinitionValid = true - // Step 4: Fetch and validate WSO2 artifact - wso2ArtifactPath := req.Path + "/" + apiEntry.WSO2Artifact + // Step 4: Fetch WSO2 artifact (api.yaml) + wso2ArtifactClean := pathpkg.Clean(apiEntry.WSO2Artifact) + wso2ArtifactPath := pathpkg.Join(req.Path, wso2ArtifactClean) wso2ArtifactContent, err := gitService.FetchFileContent(req.RepoURL, req.Branch, wso2ArtifactPath) if err != nil { response.Errors = append(response.Errors, fmt.Sprintf("failed to fetch WSO2 artifact file: %s", err.Error())) diff --git a/platform-api/src/internal/utils/api.go b/platform-api/src/internal/utils/api.go index a933fc50..37e48d06 100644 --- a/platform-api/src/internal/utils/api.go +++ b/platform-api/src/internal/utils/api.go @@ -898,8 +898,12 @@ func (u *APIUtil) APIYAMLData2ToDTO(yamlData *dto.APIYAMLData2) *dto.API { Name: fmt.Sprintf("Operation-%d", i+1), Description: fmt.Sprintf("Operation for %s %s", op.Method, op.Path), Request: &dto.OperationRequest{ - Method: op.Method, - Path: op.Path, + Method: op.Method, + Path: op.Path, + BackendServices: op.BackendServices, + Authentication: op.Authentication, + RequestPolicies: op.RequestPolicies, + ResponsePolicies: op.ResponsePolicies, }, } } @@ -1068,30 +1072,16 @@ func (u *APIUtil) validateSwagger2Model(docModel *libopenapi.DocumentModel[v2hig return nil } -// ValidateOpenAPI3 validates OpenAPI 3.x specifications using libopenapi -func (u *APIUtil) ValidateOpenAPI3(content []byte) error { - // Create a new document from the content - document, err := libopenapi.NewDocument(content) - if err != nil { - return fmt.Errorf("failed to parse OpenAPI 3.x document: %s", err.Error()) +// ValidateWSO2Artifact validates the structure of WSO2 artifact +func (u *APIUtil) ValidateWSO2Artifact(artifact *dto.APIDeploymentYAML) error { + if artifact.Kind == "" { + return fmt.Errorf("invalid artifact: missing kind") } - return u.validateOpenAPI3Document(document) -} - -// ValidateSwagger2 validates Swagger 2.0 specifications using libopenapi -func (u *APIUtil) ValidateSwagger2(content []byte) error { - // Create a new document from the content - document, err := libopenapi.NewDocument(content) - if err != nil { - return fmt.Errorf("failed to parse Swagger 2.0 document: %s", err.Error()) + if artifact.Version == "" { + return fmt.Errorf("invalid artifact: missing version") } - return u.validateSwagger2Document(document) -} - -// ValidateWSO2Artifact validates the structure of WSO2 artifact -func (u *APIUtil) ValidateWSO2Artifact(artifact *dto.APIDeploymentYAML) error { if artifact.Data.Name == "" { return fmt.Errorf("missing API name") } From 346940b691a75ad7a84a8bb22826ca0d8beca50c Mon Sep 17 00:00:00 2001 From: thivindu Date: Thu, 13 Nov 2025 19:27:51 +0530 Subject: [PATCH 8/8] Use database-generated timestamps in the DTO response --- platform-api/src/internal/service/api.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform-api/src/internal/service/api.go b/platform-api/src/internal/service/api.go index f1a8529f..dce0f337 100644 --- a/platform-api/src/internal/service/api.go +++ b/platform-api/src/internal/service/api.go @@ -163,8 +163,8 @@ func (s *APIService) CreateAPI(req *CreateAPIRequest, orgId string) (*dto.API, e return nil, fmt.Errorf("failed to create api: %w", err) } - api.CreatedAt = time.Now() - api.UpdatedAt = time.Now() + api.CreatedAt = apiModel.CreatedAt + api.UpdatedAt = apiModel.UpdatedAt // Associate backend services with the API for i, backendServiceUUID := range backendServiceIdList {