Developer Portal APIs and Controllers for Kubernetes-based API management.
The Developer Portal Controller provides Kubernetes Custom Resource Definitions (CRDs) for managing API products and API keys in a developer portal ecosystem. It integrates with Kuadrant and Gateway API to provide a complete API lifecycle management solution.
The APIProduct resource represents an API offering in the developer portal. It references an HTTPRoute and can include documentation, contact information, and usage plans.
apiVersion: devportal.kuadrant.io/v1alpha1
kind: APIProduct
metadata:
name: toystore-api
namespace: default
spec:
displayName: "Toystore API"
description: "A comprehensive API for managing toy inventory, orders, and customer data"
version: "v1"
approvalMode: manual
publishStatus: Published
tags:
- retail
- inventory
- e-commerce
targetRef:
group: gateway.networking.k8s.io
kind: HTTPRoute
name: toystore-route
documentation:
openAPISpecURL: "https://api.example.com/toystore/openapi.yaml"
swaggerUI: "https://api.example.com/toystore/docs"
docsURL: "https://docs.example.com/toystore"
gitRepository: "https://github.com/example/toystore-api"
techdocsRef: "url:https://github.com/example/toystore-api"
contact:
team: "Platform Team"
email: "platform@example.com"
slack: "#api-support"
url: "https://example.com/support"
status:
conditions:
- lastTransitionTime: "2026-01-14T17:02:07Z"
message: Discovered PlanPolicy toystore-plans targeting HTTPRoute toystore
reason: Found
status: "True"
type: PlanPolicyDiscovered
- lastTransitionTime: "2026-01-14T17:02:08Z"
message: Discovered AuthPolicy toystore targeting HTTPRoute toystore
reason: Found
status: "True"
type: AuthPolicyDiscovered
- lastTransitionTime: "2026-01-14T17:02:07Z"
message: HTTPRoute toystore/toystore accepted
reason: HTTPRouteAccepted
status: "True"
type: Ready
discoveredAuthScheme:
authentication:
api-key-users:
apiKey:
allNamespaces: true
selector:
matchLabels:
app: toystore
credentials:
authorizationHeader:
prefix: APIKEY
metrics: false
priority: 0
discoveredPlans:
- limits:
daily: 100
tier: gold
- limits:
daily: 50
tier: silver
- limits:
daily: 10
tier: bronze
observedGeneration: 1
openapi:
lastSyncTime: "2026-01-14T17:02:07Z"
raw: |
---
openapi: "3.0.2"
info:
title: "Pet Store API"
version: "1.0.0"
servers:
- url: https://toplevel.example.io/v1
paths:
/cat:
get:
operationId: "getCat"
responses:
405:
description: "invalid input"
post:
operationId: "postCat"
responses:
405:
description: "invalid input"
/dog:
get:
operationId: "getDog"
responses:
405:
description: "invalid input"Note
Breaking changes: The current v1alpha1 API is in dev preview support mode, so breaking changes are acceptable.
displayName(required): Human-readable name for the API productdescription: Detailed description of the API productversion: API version (e.g., v1, v2)approvalMode: Whether access requests are auto-approved (automatic) or require manual review (manual)publishStatus: Controls visibility in the catalog (DraftorPublished)tags: List of tags for categorization and searchtargetRef: Reference to the HTTPRoute that this API product representsdocumentation: API documentation links (OpenAPI spec, Swagger UI, docs URL, git repository, techdocs)contact: Contact information for API owners (team, email, Slack, URL)
observedGeneration: Generation of the most recently observed specdiscoveredPlans: List of plan policies discovered from the HTTPRouteopenapi: OpenAPI specification fetched from the API with sync timestampconditions: Current state conditions (Ready, PlanPolicyDiscovered)
The APIKey resource represents a developer's request for API access. It includes information about the requester, the desired plan tier, and the use case.
Before creating an APIKey, the consumer must create a Secret in their namespace containing the API key:
apiVersion: v1
kind: Secret
metadata:
name: toystore-apikey-secret
namespace: consumer-namespace
type: Opaque
stringData:
api_key: "your-api-key-value-here"Then, create the APIKey resource:
apiVersion: devportal.kuadrant.io/v1alpha1
kind: APIKey
metadata:
name: toystore-apikey
namespace: consumer-namespace
labels:
app.kubernetes.io/name: developer-portal-controller
app.kubernetes.io/managed-by: kustomize
spec:
apiProductRef:
name: toystore-api
namespace: api-owner-namespace
secretRef:
name: toystore-apikey-secret
planTier: gold
useCase: "Authentication key for our Toystore API integration"
requestedBy:
userId: user-12345
email: developer@example.com
status:
apiHostname: api.example.com
# Rate limits from selected plan
limits:
daily: 1000
monthly: 300000
custom:
- limit: 100
window: 1m
# Authentication scheme
authScheme:
credentials:
authorizationHeader:
prefix: "Bearer"
authenticationSpec:
selector:
matchLabels:
kuadrant.io/apikey: mobile-app-payment-key
# Approval conditions
# Lifecycle states:
# - Pending: Pending condition with status "True" (initial state after creation)
# - Approved: Approved condition with status "True"
# - Denied: Denied condition with status "True"
# - Failed: Failed condition with status "True"
conditions:
- type: Approved
status: "True"
reason: ApprovedByOwner
message: APIKey has been approved for toystore integration
lastTransitionTime: "2025-12-09T10:30:00Z"Note
Breaking changes: The current v1alpha1 API is in dev preview support mode, so breaking changes are acceptable.
apiProductRef(required): Reference to the APIProduct this APIKey belongs toname: Name of the APIProductnamespace: Namespace of the APIProduct (enables cross-namespace references)
secretRef(required): Reference to the secret containing the API keyname: Name of the secret in the consumer's namespace- Consumer creates this secret before creating the APIKey
- The secret must contain an
api_keyentry with the value of the API key - Controller reads the API key from this secret on approval
planTier(required): Tier of the plan (e.g., "gold", "silver", "bronze", "premium", "basic")useCase(required): Description of how the API key will be usedrequestedBy(required): Information about the requesteruserId: Identifier of the user requesting the API keyemail: Email address of the user (validated with regex pattern)
apiHostname: Hostname from the HTTPRoutelimits: Rate limits for the planauthScheme: Authentication scheme from the AuthPolicyconditions: Latest observations of the APIKey's state- Lifecycle states based on conditions:
- Pending:
Pendingcondition with status"True"(awaiting approval) - Approved:
Approvedcondition with status"True" - Denied:
Deniedcondition with status"True" - Failed:
Failedcondition with status"True"
- Pending:
- Lifecycle states based on conditions:
The APIKeyRequest resource is a shadow resource automatically created by the Developer Portal Controller in the API owner's namespace whenever a consumer creates an APIKey. This design enables namespace-based RBAC: API owners can list and review access requests for their API products without requiring cluster-wide permissions to view all APIKeys across all consumer namespaces.
Key characteristics:
- Created automatically by the controller (not by users)
- Lives in the API owner's namespace
- Contains request metadata (use case, requester info, plan tier)
- Does NOT contain the API key secret value (preserves consumer data isolation)
- Serves as the basis for approval/denial decisions
apiVersion: devportal.kuadrant.io/v1alpha1
kind: APIKeyRequest
metadata:
name: toystore-request-12345
namespace: api-owner-namespace # Owner's namespace
labels:
app.kubernetes.io/name: developer-portal-controller
app.kubernetes.io/managed-by: kustomize
spec:
# Reference to APIProduct in owner's namespace
apiProductRef:
name: toystore-api
# Plan and use case details from consumer's request
planTier: gold
useCase: "Authentication key for our mobile app integration with Toystore API"
# Information about who requested access
requestedBy:
userId: user-12345
email: developer@example.com
# Cross-namespace reference to the consumer's APIKey
apiKeyRef:
name: toystore-apikey
namespace: consumer-namespace
status:
conditions:
- type: Pending
status: "True"
reason: AwaitingApproval
message: Waiting for API owner to review the request
lastTransitionTime: "2026-04-10T10:00:00Z"Note
Breaking changes: The current v1alpha1 API is in dev preview support mode, so breaking changes are acceptable.
apiProductRef(required): Reference to the APIProduct in the owner's namespacename: Name of the APIProduct
planTier(required): Tier of the plan (e.g., "gold", "silver", "bronze")useCase(required): Description of how the API key will be usedrequestedBy(required): Information about the requesteruserId: Identifier of the user requesting the API keyemail: Email address of the user
apiKeyRef(required): Cross-namespace reference to the consumer's APIKeyname: Name of the APIKey in the consumer's namespacenamespace: Namespace where the APIKey was created
conditions: Latest observations of the APIKeyRequest's state- Pending: Request is awaiting review
- Approved: Request has been approved
- Denied: Request has been denied
The APIKeyApproval resource represents an API owner's decision to approve or deny an APIKeyRequest. API owners create this resource in their own namespace to review and make decisions on API access requests for their API products.
Key characteristics:
- Created by API owners (manual approval mode) or automatically (automatic mode)
- Lives in the API owner's namespace (same as the APIProduct)
- Contains approval decision and reviewer metadata
- Controller validates that the APIKeyApproval namespace matches the APIProduct namespace to prevent cross-namespace approval attacks
apiVersion: devportal.kuadrant.io/v1alpha1
kind: APIKeyApproval
metadata:
name: toystore-approval-12345
namespace: api-owner-namespace
labels:
app.kubernetes.io/name: developer-portal-controller
app.kubernetes.io/managed-by: kustomize
spec:
# Reference to the APIKeyRequest being reviewed
apiKeyRequestRef:
name: toystore-request-12345
# Approval decision
approved: true
# Reviewer information
reviewedBy: api-owner@example.com
reviewedAt: "2026-04-10T12:00:00Z"
# Optional context for the decision
reason: "Approved"
message: "The use case is valid and meets our API usage guidelines. Approved for production use with gold tier access."apiVersion: devportal.kuadrant.io/v1alpha1
kind: APIKeyApproval
metadata:
name: toystore-approval-67890
namespace: api-owner-namespace
spec:
apiKeyRequestRef:
name: toystore-request-67890
approved: false
reviewedBy: api-owner@example.com
reviewedAt: "2026-04-10T14:30:00Z"
reason: "InvalidUseCase"
message: "The requested use case does not align with our API terms of service. Please contact support at api-support@example.com for more information."Note
Breaking changes: The current v1alpha1 API is in dev preview support mode, so breaking changes are acceptable.
apiKeyRequestRef(required): Reference to the APIKeyRequest being reviewedname: Name of the APIKeyRequest (same namespace only)
approved(required): Boolean indicating whether the request is approved (true) or denied (false)reviewedBy(required): Identifier of the person who reviewed the request (e.g., email, username)reviewedAt(required): Timestamp when the request was reviewed (RFC3339 format)reason(optional): Short reason for the decision (e.g., "Approved", "Denied", "InvalidUseCase")message(optional): Additional context or explanation for the approval or denial decision
The following diagram illustrates the relationships between resources across namespace boundaries:
graph TB
subgraph ConsumerNamespace["Consumer Namespace"]
APIKey["APIKey"]
end
subgraph OwnerNamespace["API Owner Namespace"]
APIProduct["APIProduct"]
APIKeyRequest["APIKeyRequest<br/>(Shadow Resource)"]
APIKeyApproval["APIKeyApproval"]
end
%% Cross-namespace references
APIKey -->|"spec.apiProductRef<br/>(cross-namespace)"| APIProduct
APIKeyRequest -->|"spec.apiKeyRef<br/>(cross-namespace)"| APIKey
%% Same-namespace references
APIKeyRequest -->|"spec.apiProductRef<br/>(local)"| APIProduct
APIKeyApproval -->|"spec.apiKeyRequestRef<br/>(local)"| APIKeyRequest
%% Controller actions
APIKey -.->|"Controller creates"| APIKeyRequest
classDef consumerNS fill:#e1f5ff,stroke:#0077cc,stroke-width:2px
classDef ownerNS fill:#fff4e6,stroke:#ff9800,stroke-width:2px
classDef shadowResource fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
class APIKey consumerNS
class APIProduct,APIKeyApproval ownerNS
class APIKeyRequest shadowResource
Key Concepts:
- Namespace Isolation: Consumer resources (APIKey) are isolated in the consumer's namespace; owner resources (APIProduct, APIKeyRequest, APIKeyApproval) are in the owner's namespace
- Cross-Namespace References (solid arrows): Enable consumers to request access to APIs owned by others
- Local References (solid arrows): Keep owner-side resources scoped within the owner's namespace
- Controller Actions (dashed arrows): Automated creation and updates by the Developer Portal Controller
- Shadow Resource Pattern: APIKeyRequest mirrors APIKey metadata in the owner's namespace without exposing sensitive data
The Developer Portal Controller implements a namespace-based RBAC model with three personas:
-
Consumer (API Consumer namespace)
- Creates an
APIKeyresource in their namespace - Specifies the desired API product, plan tier, and use case
- APIKey starts with a
Pendingcondition (status"True") indicating it awaits approval (see APIKey Status Fields)
- Creates an
-
Controller (Automatic)
- Detects the new APIKey
- Creates an
APIKeyRequestshadow resource in the API Owner's namespace - Copies request metadata (use case, requester, plan tier) but NOT the API key value
-
API Owner (API Owner namespace)
- Lists
APIKeyRequestresources in their namespace - Reviews requests for their API products
- Creates an
APIKeyApprovalresource with their decision - Controller validates that the approval namespace matches the APIProduct namespace
- Lists
-
Controller (Automatic)
- Processes the
APIKeyApprovaldecision - Updates the consumer's
APIKeyconditions (Approved/Denied) - If approved: Reads the API key from the consumer's secret and creates an enforcement secret in the Kuadrant namespace
- Processes the
-
Consumer (API Consumer namespace)
- Checks their
APIKeystatus for approval - Accesses the API key value from the secret they created (referenced in
spec.secretRef) - Uses the API key to authenticate API requests
- Checks their
If the APIProduct has approvalMode: automatic, the controller automatically creates an APIKeyApproval resource with approved: true when the APIKeyRequest is created, skipping the manual review step.
The make local-setup command sets up a complete local development environment using Kind (Kubernetes in Docker). This is useful for local development when you don't have Istio or Kuadrant controllers installed.
Running make local-setup performs the following steps:
-
Creates a Kind cluster
- Deletes any existing
devportal-controller-localKind cluster - Creates a fresh Kind cluster with the name
devportal-controller-local - Uses configuration from
utils/kind-cluster.yaml
- Deletes any existing
-
Installs API dependencies (CRDs)
- Installs Developer Portal Controller CRDs (APIProduct, APIKey, APIKeyRequest, APIKeyApproval)
- Installs Gateway API CRDs (HTTPRoute, Gateway, etc.)
- Installs Kuadrant core CRDs (PlanPolicy, AuthPolicy)
-
Installs demo resources
- Applies demo manifests from
utils/demo/gamestore.yaml - Provides sample resources for testing and development
- Applies demo manifests from
-
Builds and deploys the controller
- Builds the controller Docker image from current code
- Loads the image into the Kind cluster
- Deploys the controller to the
developer-portal-controller-systemnamespace - Waits for the deployment to become available
# Set up the complete local development environment
make local-setupYou can also run individual steps separately:
# Create only the Kind cluster with CRDs
make local-cluster-setup
# Add demo resources to an existing cluster
make demo-install
# Deploy controller from current code to an existing cluster
make local-deploy- Create an HTTPRoute for your API in your namespace
- Create a PlanPolicy to define rate limits and tiers for the HTTPRoute
- Create an APIProduct referencing the HTTPRoute
- Set
approvalModetomanualorautomaticbased on your approval requirements
-
Create a Secret containing your API key credentials in your namespace
- The secret must contain an
api_keyentry with your desired API key value
- The secret must contain an
-
Create an APIKey resource in your namespace referencing the API owner's APIProduct
- Use a cross-namespace reference:
spec.apiProductRef.namespace - Reference the secret you created via
spec.secretRef - Specify your desired plan tier and use case
- Provide your user ID and email
- Use a cross-namespace reference:
-
Wait for Review (if
approvalModeismanual)- Controller automatically creates an
APIKeyRequestin the API owner's namespace - API owner reviews your request and creates an
APIKeyApprovalresource - Check your
APIKeystatus conditions for approval/denial
- Controller automatically creates an
-
Access Your API Key
- Once approved, retrieve the API key value from the secret you created (referenced in
spec.secretRef) - Use the key to authenticate your API requests
- Once approved, retrieve the API key value from the secret you created (referenced in
-
List Requests for your API products
- View
APIKeyRequestresources in your namespace - Each request contains consumer's use case and requester information
- View
-
Make a Decision
- Create an
APIKeyApprovalresource with your decision - Set
approved: true(approve) orapproved: false(deny) - Provide reviewer info and optional reason/message
- Create an
-
Track Approvals
- List
APIKeyApprovalresources to see all decisions - Controller automatically updates the consumer's
APIKeystatus
- List
# List APIProducts
kubectl get apiproducts
# List APIKeys (shortname: apik)
kubectl get apik
# View APIKey details with approval status
kubectl get apik -o wide
# Describe an APIKey to see status details
kubectl describe apikey toystore-apikey
# List APIKeyRequests (API owner view)
kubectl get apikeyrequests -n api-owner-namespace
# Describe an APIKeyRequest to review details
kubectl describe apikeyrequest toystore-request-12345 -n api-owner-namespace
# List APIKeyApprovals (API owner view)
kubectl get apikeyapprovals -n api-owner-namespace
# Describe an APIKeyApproval to see the decision
kubectl describe apikeyapproval toystore-approval-12345 -n api-owner-namespace# As an API owner, list all pending requests for your API products
kubectl get apikeyrequests -n my-api-namespace
# Review a specific request
kubectl describe apikeyrequest request-12345 -n my-api-namespace
# Approve the request by creating an APIKeyApproval
kubectl apply -f - <<EOF
apiVersion: devportal.kuadrant.io/v1alpha1
kind: APIKeyApproval
metadata:
name: approval-12345
namespace: my-api-namespace
spec:
apiKeyRequestRef:
name: request-12345
approved: true
reviewedBy: owner@example.com
reviewedAt: "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
reason: "Approved"
message: "Use case is valid"
EOFCopyright 2025.
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.