Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gateway/examples/anthropic-openai-proxy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions gateway/examples/azure-openai-proxy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions gateway/examples/basic-ratelimit-demo-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion gateway/examples/llm-proxy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion gateway/examples/mcp-proxy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions gateway/examples/mistral-openai-proxy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions gateway/examples/openai-multi-provider-proxy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion gateway/examples/petstore-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions gateway/examples/sample-echo-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions platform-api/internal/service/artifact_import.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
41 changes: 40 additions & 1 deletion platform-api/internal/service/artifact_import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
12 changes: 6 additions & 6 deletions platform-api/internal/utils/import_artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading