diff --git a/gateway/examples/anthropic-openai-proxy.yaml b/gateway/examples/anthropic-openai-proxy.yaml index ebccbaa33..674c9b6b7 100644 --- a/gateway/examples/anthropic-openai-proxy.yaml +++ b/gateway/examples/anthropic-openai-proxy.yaml @@ -32,6 +32,8 @@ apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProxy metadata: name: openai-to-anthropic + annotations: + "gateway.api-platform.wso2.com/project-id": "default" spec: displayName: OpenAI to Anthropic Proxy version: v1.0 diff --git a/gateway/examples/azure-openai-proxy.yaml b/gateway/examples/azure-openai-proxy.yaml index 6c031a564..f77e7e0ee 100644 --- a/gateway/examples/azure-openai-proxy.yaml +++ b/gateway/examples/azure-openai-proxy.yaml @@ -30,6 +30,8 @@ apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProxy metadata: name: openai-to-azure-openai + annotations: + "gateway.api-platform.wso2.com/project-id": "default" spec: displayName: OpenAI to Azure OpenAI Proxy version: v1.0 diff --git a/gateway/examples/basic-ratelimit-demo-api.yaml b/gateway/examples/basic-ratelimit-demo-api.yaml index de2808a99..c07e4f3aa 100644 --- a/gateway/examples/basic-ratelimit-demo-api.yaml +++ b/gateway/examples/basic-ratelimit-demo-api.yaml @@ -23,6 +23,8 @@ apiVersion: gateway.api-platform.wso2.com/v1 kind: RestApi metadata: name: ecommerce-api-v1 + annotations: + "gateway.api-platform.wso2.com/project-id": "default" spec: displayName: E-Commerce API with Rate Limiting version: v1.0 diff --git a/gateway/examples/llm-proxy.yaml b/gateway/examples/llm-proxy.yaml index 7fa82569a..a249949ca 100644 --- a/gateway/examples/llm-proxy.yaml +++ b/gateway/examples/llm-proxy.yaml @@ -21,7 +21,7 @@ kind: LlmProxy metadata: name: wso2con-assistant annotations: - "gateway.api-platform.wso2.com/project-id": "Default" + "gateway.api-platform.wso2.com/project-id": "default" spec: displayName: WSO2 Con Assistant version: v1.0 diff --git a/gateway/examples/mcp-proxy.yaml b/gateway/examples/mcp-proxy.yaml index 7be3bdd5f..2c176f394 100644 --- a/gateway/examples/mcp-proxy.yaml +++ b/gateway/examples/mcp-proxy.yaml @@ -21,7 +21,7 @@ kind: Mcp metadata: name: everything-mcp-v1.0 annotations: - "gateway.api-platform.wso2.com/project-id": "Default" + "gateway.api-platform.wso2.com/project-id": "default" spec: displayName: Everything version: v1.0 diff --git a/gateway/examples/mistral-openai-proxy.yaml b/gateway/examples/mistral-openai-proxy.yaml index 26d7d2960..ae16a5311 100644 --- a/gateway/examples/mistral-openai-proxy.yaml +++ b/gateway/examples/mistral-openai-proxy.yaml @@ -33,6 +33,8 @@ apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProxy metadata: name: openai-to-mistral + annotations: + "gateway.api-platform.wso2.com/project-id": "default" spec: displayName: OpenAI to Mistral Proxy version: v1.0 diff --git a/gateway/examples/openai-multi-provider-proxy.yaml b/gateway/examples/openai-multi-provider-proxy.yaml index 113c3594b..3a972bd8e 100644 --- a/gateway/examples/openai-multi-provider-proxy.yaml +++ b/gateway/examples/openai-multi-provider-proxy.yaml @@ -30,6 +30,8 @@ apiVersion: gateway.api-platform.wso2.com/v1 kind: LlmProxy metadata: name: openai-multi + annotations: + "gateway.api-platform.wso2.com/project-id": "default" spec: displayName: OpenAI Multi-Provider Proxy version: v1.0 diff --git a/gateway/examples/petstore-api.yaml b/gateway/examples/petstore-api.yaml index 962f591fc..665af43d0 100644 --- a/gateway/examples/petstore-api.yaml +++ b/gateway/examples/petstore-api.yaml @@ -22,7 +22,7 @@ metadata: name: petstore-api-v1.0 annotations: gateway.api-platform.wso2.com/artifact-id: 019d953f-d386-7a64-4444-1869a28292e0 - gateway.api-platform.wso2.com/project-id: 019d953f-d386-7a64-1111-1869a28292e0 + gateway.api-platform.wso2.com/project-id: default spec: displayName: PetStore API test version: v1.0 diff --git a/gateway/examples/sample-echo-api.yaml b/gateway/examples/sample-echo-api.yaml index dbb21c48c..f1463a307 100644 --- a/gateway/examples/sample-echo-api.yaml +++ b/gateway/examples/sample-echo-api.yaml @@ -24,6 +24,8 @@ apiVersion: gateway.api-platform.wso2.com/v1 kind: RestApi metadata: name: sample-echo-v1 + annotations: + "gateway.api-platform.wso2.com/project-id": "default" spec: displayName: Sample Echo version: v1 diff --git a/platform-api/internal/service/artifact_import.go b/platform-api/internal/service/artifact_import.go index 7392b3374..24ee449f7 100644 --- a/platform-api/internal/service/artifact_import.go +++ b/platform-api/internal/service/artifact_import.go @@ -61,8 +61,8 @@ type ImportContext struct { DeployedAt *time.Time Properties map[string]interface{} - ProjectName string // metadata.project (may be empty) - ProjectID string // resolved project UUID (empty for org-level kinds) + ProjectHandle string // project handle from the project-id annotation (may be empty) + ProjectID string // resolved project UUID (empty for org-level kinds) // Existing is the already-stored artifacts-table row keyed by ID, or nil if new. // It is nil for organization-level kinds that are not backed by the artifacts @@ -246,30 +246,30 @@ func (s *ArtifactImportService) importValidated(orgID, gatewayID string, req dto UpdatedAt: req.UpdatedAt, DeployedAt: req.DeployedAt, Properties: req.Properties, - ProjectName: utils.ResolveImportProject(req.Configuration.Metadata), + ProjectHandle: utils.ResolveImportProject(req.Configuration.Metadata), } // Resolve project for project-scoped kinds. Org-level kinds ignore the project. if importer.RequiresProject() { - if ictx.ProjectName == "" { - // The gateway must always supply the project (as a metadata label) for + if ictx.ProjectHandle == "" { + // The gateway must always supply the project (as the project-id annotation) for // project-scoped kinds; a push without one is a contract violation. s.slogger.Error("Project is required for gateway-imported artifact but was not provided", "kind", kind, "artifactId", req.DPID) return nil, apperror.ValidationFailed.New(fmt.Sprintf("A project is required for artifact kind %q.", kind)) } - project, err := s.projectRepo.GetProjectByNameAndOrgID(ictx.ProjectName, orgID) + project, err := s.projectRepo.GetProjectByHandleAndOrgID(ictx.ProjectHandle, orgID) if err != nil { - return nil, fmt.Errorf("failed to resolve project %q: %w", ictx.ProjectName, err) + return nil, fmt.Errorf("failed to resolve project %q: %w", ictx.ProjectHandle, err) } if project == nil { // The project was provided but does not exist in this organization. Reject the // import with ErrProjectNotFound (the project is never created or defaulted); // the artifact is not created in the control plane. s.slogger.Error("Project is not available in the organization; gateway artifact not imported", - "kind", kind, "artifactId", req.DPID, "project", ictx.ProjectName, "orgId", orgID) + "kind", kind, "artifactId", req.DPID, "projectHandle", ictx.ProjectHandle, "orgId", orgID) return nil, apperror.ProjectNotFound.New().WithLogMessage( - fmt.Sprintf("project %q does not exist in org %q", ictx.ProjectName, orgID)) + fmt.Sprintf("project with handle %q does not exist in org %q", ictx.ProjectHandle, orgID)) } ictx.ProjectID = project.ID } diff --git a/platform-api/internal/service/artifact_import_test.go b/platform-api/internal/service/artifact_import_test.go index fac9f4094..f5da028c1 100644 --- a/platform-api/internal/service/artifact_import_test.go +++ b/platform-api/internal/service/artifact_import_test.go @@ -107,7 +107,7 @@ func setupImportTest(t *testing.T) *importTestDeps { } if _, err := db.Exec(`INSERT INTO projects (uuid, handle, display_name, organization_uuid, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))`, - importTestProjectID, "default", "default", importTestOrgID, ""); err != nil { + importTestProjectID, "default", "Default", importTestOrgID, ""); err != nil { t.Fatalf("seed project: %v", err) } if _, err := db.Exec(`INSERT INTO gateways (uuid, organization_uuid, handle, display_name, description, properties, created_at, updated_at) @@ -248,6 +248,45 @@ func TestArtifactImport_NonexistentProject(t *testing.T) { } } +// TestArtifactImport_ResolvesProjectByHandleNotDisplayName is the regression guard for #2678: +// the project-id annotation carries the project handle, not the display name. The seeded +// project has handle "default" and display name "Default". A push whose annotation is the +// handle ("default") must resolve and create the artifact; a push whose annotation is the +// display name ("Default") must be rejected with ProjectNotFound. +func TestArtifactImport_ResolvesProjectByHandleNotDisplayName(t *testing.T) { + d := setupImportTest(t) + + // The display name ("Default") is not a handle -> the import must fail. + byDisplayName := restImportRequest("55555555-5555-5555-5555-555555555555", "by-display-name", "By Display Name") + byDisplayName.Configuration.Metadata.Annotations = projectAnnotations("Default") + if _, err := d.svc.Import(importTestOrgID, importTestGatewayID, byDisplayName); !apperror.ProjectNotFound.Is(err) { + t.Fatalf("Import(display name) error = %v, want ErrProjectNotFound", err) + } + if art, err := d.artifactRepo.GetByHandle("by-display-name", importTestOrgID); err != nil { + t.Fatalf("GetByHandle: %v", err) + } else if art != nil { + t.Errorf("artifact was created for a display-name project reference") + } + + // The handle ("default") resolves -> the import succeeds and lands in the project. + byHandle := restImportRequest("66666666-6666-6666-6666-666666666666", "by-handle", "By Handle") + byHandle.Configuration.Metadata.Annotations = projectAnnotations("default") + resp, err := d.svc.Import(importTestOrgID, importTestGatewayID, byHandle) + if err != nil { + t.Fatalf("Import(handle) error = %v", err) + } + apiModel, err := d.apiRepo.GetAPIByUUID(resp.ID, importTestOrgID) + if err != nil { + t.Fatalf("GetAPIByUUID: %v", err) + } + if apiModel == nil { + t.Fatalf("artifact was not created for a valid handle project reference") + } + if apiModel.ProjectID != importTestProjectID { + t.Errorf("artifact ProjectID = %q, want %q (the project resolved by handle)", apiModel.ProjectID, importTestProjectID) + } +} + func TestArtifactImport_CPArtifactMetadataNotOverwritten(t *testing.T) { d := setupImportTest(t) diff --git a/platform-api/internal/utils/import_artifacts.go b/platform-api/internal/utils/import_artifacts.go index 2c1bc8c76..455266471 100644 --- a/platform-api/internal/utils/import_artifacts.go +++ b/platform-api/internal/utils/import_artifacts.go @@ -142,14 +142,14 @@ func ArtifactImportRank(kind string) int { return len(artifactImportOrder) } -// ResolveImportProject extracts the project identifier from the k8s-shaped metadata. -// The project is identified by the domain-prefixed project-id annotation or the label as a fallback. +// ResolveImportProject extracts the project handle from the k8s-shaped metadata. +// The project is identified by the domain-prefixed project-id annotation or the deprecated bare label as a fallback. func ResolveImportProject(md dto.ArtifactImportMetadata) string { - projectId := md.Annotations[commonconstants.AnnotationProjectID] - if projectId == "" { - projectId = md.Labels[commonconstants.DeprecatedLabelProjectID] + projectHandle := md.Annotations[commonconstants.AnnotationProjectID] + if projectHandle == "" { + projectHandle = md.Labels[commonconstants.DeprecatedLabelProjectID] } - return projectId + return projectHandle } // IsNewerDeployment reports whether incoming supersedes the working copy's current