Skip to content

Commit 0d84cad

Browse files
5.0.1 (#59)
* 2024052802 - API v5.0 Updated JSON schema for Developer Portal definition * 2024052901 - API v5.0.1 Backstage.io integration - first commit * 20240530-01 - Backstage devportal * 20240530-01 - Backstage devportal * 20240612-01 - Backstage devportal * 20240613-01 - Backstage devportal, NGINX One SaaS console dev * 20240614 - backstage.io and NGINX One Cloud Console alpha stage support
1 parent 449c2ab commit 0d84cad

14 files changed

+1187
-51
lines changed

FEATURES.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@
3030

3131
### API Gateway - Developer Portal
3232

33-
| Feature | API v4.2 | API v5.0 | Notes |
34-
|-------------------------------------------------|----------|----------|---------------------------|
35-
| Developer Portal generation from OpenAPI schema | X | X | <li>Based on Redocly</li> |
33+
| Feature | API v4.2 | API v5.0 | Notes |
34+
|-------------------------------------|----------|-----------|-------|
35+
| Redocly-based developer portal | X | X | |
36+
| Backstage.io-based developer portal | | X | |
3637

3738
### Client authentication
3839

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@
22

33
[![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active)
44

5-
This project provides a set of declarative REST API for [NGINX Instance Manager](https://docs.nginx.com/nginx-management-suite/nim/).
5+
This project provides a set of declarative REST API for [NGINX Instance Manager](https://docs.nginx.com/nginx-management-suite/nim/) and [NGINX One Cloud Console - currently in early stage](https://docs.nginx.com/nginx-one/).
66

77
It can be used to manage NGINX Plus configuration lifecycle and to create NGINX Plus configurations using JSON service definitions.
88

9-
GitOps integration is supported when used with NGINX Instance Manager: source of truth is checked for updates (NGINX App Protect policies, TLS certificates, keys and chains/bundles, Swagger/OpenAPI definitions) and NGINX configurations are automatically kept in sync.
9+
GitOps integration is supported: source of truth is checked for updates (NGINX App Protect policies, TLS certificates, keys and chains/bundles, Swagger/OpenAPI definitions) and NGINX configurations are automatically kept in sync.
1010

1111
Use cases include:
1212

1313
- Rapid configuration generation and templating
14-
- CI/CD integration with NGINX Instance Manager (instance groups and staged configs)
15-
- NGINX App Protect DevSecOps integration
14+
- CI/CD integration with NGINX Instance Manager (instance groups and staged configs) and NGINX One Cloud Console (clusters)
15+
- NGINX App Protect DevSecOps integration (NGINX Instance Manager only)
1616
- API Gateway deployments with automated Swagger / OpenAPI schema import
1717
- API Developer portals zero-touch deployment
1818
- GitOps integration with source of truth support for
1919
- NGINX App Protect WAF policies
20-
- TLS certificates, keys and chains/bundles
20+
- TLS certificates, keys and chains/bundles (NGINX Instance Manager only)
2121
- mTLS certificates
2222
- `http` snippets, upstreams, servers, locations
2323
- `stream` snippets, upstreams, servers
@@ -27,6 +27,7 @@ Use cases include:
2727
## Supported releases
2828

2929
- NGINX Instance Manager 2.14+
30+
- NGINX One Cloud Console
3031
- NGINX Plus R30+
3132
- NGINX App Protect WAF 4.8+
3233

USAGE-v5.0.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ The JSON schema is self explanatory. See also the [sample Postman collection](/c
3737
- `.output.nms.policies[].versions[].displayName` the policy version's display name
3838
- `.output.nms.policies[].versions[].description` the policy version's description
3939
- `.output.nms.policies[].versions[].contents` this can be either base64-encoded or be a HTTP(S) URL that will be fetched dynamically from a source of truth
40+
- *nginxone* - NGINX configuration is published to a NGINX One Cloud Console cluster
41+
- `.output.nginxone.url` the NGINX One Cloud Console URL
42+
- `.output.nginxone.namespace` the NGINX One Cloud Console namespace
43+
- `.output.nginxone.token` the authentication token
44+
- `.output.nginxone.cluster` the cluster name
45+
- `.output.nginxone.synctime` **optional**, used for GitOps autosync. When specified and the declaration includes HTTP(S) references to NGINX App Protect policies, TLS certificates/keys/chains, the HTTP(S) endpoints will be checked every `synctime` seconds and if external contents have changed, the updated configuration will automatically be published to NGINX Instance Manager
46+
- `.output.nginxone.modules` an optional array of NGINX module names (ie. 'ngx_http_app_protect_module', 'ngx_http_js_module','ngx_stream_js_module')
4047
- `.declaration` describes the NGINX configuration to be created.
4148

4249
### Locations ###
@@ -111,8 +118,9 @@ Declaration path `.declaration.http.servers[].locations[].apigateway` defines th
111118
- `api_gateway.strip_uri` - removes the `.declaration.http.servers[].locations[].uri` part of the URI before forwarding requests to the upstream
112119
- `api_gateway.server_url` - the base URL of the upstream server
113120
- `developer_portal.enabled` - enable/disable Developer portal provisioning
114-
- `developer_portal.type` - developer portal type. Currently supported are: `redocly`
115-
- `developer_portal.redocly.uri` - the trailing part of the Developer portal URI, this is appended to `.declaration.http.servers[].locations[].uri`. If omitted it defaults to `devportal.html`
121+
- `developer_portal.type` - developer portal type. Currently supported are: `redocly`, `backstage`
122+
- `developer_portal.redocly.*` - Redocly-based developer portal parameters. See the [Postman collection](/contrib/postman)
123+
- `developer_portal.backstage.*` - Backstage-based developer portal parameters. See the [Postman collection](/contrib/postman)
116124
- `authentication` - optional, used to enforce authentication at the API Gateway level
117125
- `authentication.client[]` - authentication profile names
118126
- `authentication.enforceOnPaths` - if set to `true` authentication is enforced on all API endpoints listed under `authentication.paths`. if set to `false` authentication is enforced on all API endpoints but those listed under `authentication.paths`
@@ -124,13 +132,15 @@ Declaration path `.declaration.http.servers[].locations[].apigateway` defines th
124132
- `rate_limit` - optional, used to enforce rate limiting at the API Gateway level
125133
- `rate_limit.enforceOnPaths` - if set to `true` rate limiting is enforced on all API endpoints listed under `rate_limit.paths`. if set to `false` rate limiting is enforced on all API endpoints but those listed under `rate_limit.paths`
126134

127-
A sample API Gateway declaration to publish the `https://petstore.swagger.io` REST API and enforce:
135+
A sample API Gateway declaration to publish the `https://petstore.swagger.io` REST API using:
128136

129137
- REST API endpoint URIs
130138
- HTTP Methods
131139
- Rate limiting on `/user/login`, `/usr/logout` and `/pet/{petId}/uploadImage`
132140
- JWT authentication on `/user/login`, `/usr/logout` and `/pet/{petId}/uploadImage`
133141
- JWT claim-based authorization on `/user/login`, `/usr/logout` and `/pet/{petId}/uploadImage`
142+
- Redocly-based developer portal
143+
- NGINX App Protect WAF security
134144

135145
can be found in the [Postman collection](/contrib/)
136146

@@ -195,4 +205,4 @@ For a list of all supported authentication profile types see the [feature matrix
195205

196206
### Usage Examples ###
197207

198-
A sample Postman collection is available [here](/contrib/postman)
208+
A sample Postman collection is available [here](/contrib/postman)

contrib/postman/NGINX Declarative API.postman_collection.json

Lines changed: 752 additions & 9 deletions
Large diffs are not rendered by default.

contrib/postman/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This collection contains:
44

55
API v5.0 - Latest
66
- `Configuration generation` - Declaration examples with output to plaintext, JSON, Kubernetes ConfigMap, HTTP POST
7-
- `Declarative automation examples` - Several examples and use cases
7+
- `Declarative automation examples` - Several examples and use cases for NGINX Instance Manager and NGINX One Cloud Console
88
- `API Gateway` - Sample API gateway requests for Swagger and OpenAPI schemas import
99
- `CRUD automation` - Sample requests for CRUD-based automation
1010
- `GitOps autosync` - GitOps automation demo

etc/config.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ auth_server_root = "authn/server"
2222

2323
authz_client_root = "authz/client"
2424

25+
devportal_root = "devportal"
26+
2527
# NGINX Declarative API Server
2628
[apiserver]
2729
host = "0.0.0.0"

src/V5_0_CreateConfig.py

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosyn
6666
# NGINX auxiliary files for staged config
6767
auxFiles = {'files': []}
6868

69+
# Extra manifests to be returned to the caller
70+
extraOutputManifests = []
71+
6972
try:
7073
# Pydantic JSON validation
7174
ConfigDeclaration(**declaration.model_dump())
@@ -400,7 +403,7 @@ def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosyn
400403
{"code": status,
401404
"content": f"invalid server authentication profile [{openApiAuthProfile[0]['profile']}] for OpenAPI schema [{loc['apigateway']['openapi_schema']['content']}]"}}}
402405

403-
status, apiGatewayConfigDeclaration = v5_0.APIGateway.createAPIGateway(locationDeclaration = loc, authProfiles = d['declaration']['http']['authentication'])
406+
status, apiGatewayConfigDeclaration, openAPISchemaJSON = v5_0.APIGateway.createAPIGateway(locationDeclaration = loc, authProfiles = loc['apigateway']['openapi_schema']['authentication'])
404407

405408
# API Gateway configuration template rendering
406409
if apiGatewayConfigDeclaration:
@@ -415,21 +418,35 @@ def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosyn
415418
# API Gateway Developer portal provisioning
416419
if loc['apigateway'] and loc['apigateway']['developer_portal'] and 'enabled' in loc['apigateway']['developer_portal'] and loc['apigateway']['developer_portal']['enabled'] == True:
417420

418-
status, devPortalHTML = v5_0.DevPortal.createDevPortal(locationDeclaration = loc, authProfiles = d['declaration']['http']['authentication'])
419-
420-
if status != 200:
421-
return {"status_code": 412,
422-
"message": {"status_code": status, "message":
423-
{"code": status, "content": f"Developer Portal creation failed for {loc['uri']}"}}}
424-
425-
### Add optional API Developer portal HTML files
421+
### Redocly developer portal - Add optional API Developer portal HTML files
426422
# devPortalHTML
427-
if loc['apigateway']['developer_portal']['type'].lower() == "redocly":
423+
if loc['apigateway']['developer_portal']['type'].lower() == 'redocly':
424+
status, devPortalHTML = v5_0.DevPortal.createDevPortal(locationDeclaration=loc,
425+
authProfiles=
426+
d['declaration']['http'][
427+
'authentication'])
428+
429+
if status != 200:
430+
return {"status_code": 412,
431+
"message": {"status_code": status, "message":
432+
{"code": status,
433+
"content": f"Developer Portal creation failed for {loc['uri']}"}}}
434+
428435
newAuxFile = {'contents': devPortalHTML, 'name': NcgConfig.config['nms']['devportal_dir'] +
429436
loc['apigateway']['developer_portal']['redocly']['uri']}
430437
auxFiles['files'].append(newAuxFile)
431438

432-
### / Add optional API Developer portal HTML files
439+
### / Redocly developer portal - Add optional API Developer portal HTML files
440+
441+
### Backstage developer portal - Create Kubernetes Backstage manifest
442+
# devPortalHTML
443+
if loc['apigateway']['developer_portal']['type'].lower() == 'backstage':
444+
backstageManifest = j2_env.get_template(f"{NcgConfig.config['templates']['devportal_root']}/backstage.tmpl").render(
445+
declaration=loc['apigateway']['developer_portal']['backstage'], openAPISchema = v5_0.MiscUtils.json_to_yaml(openAPISchemaJSON), ncgconfig=NcgConfig.config)
446+
447+
extraOutputManifests.append(backstageManifest)
448+
449+
### / Backstage developer portal - Create Kubernetes Backstage manifest
433450

434451
if loc['rate_limit'] is not None:
435452
if 'profile' in loc['rate_limit'] and loc['rate_limit']['profile'] and loc['rate_limit'][
@@ -547,30 +564,30 @@ def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosyn
547564
# NGINX auxiliary files for staged config
548565
auxFiles['rootDir'] = NcgConfig.config['nms']['config_dir']
549566

550-
return v5_0.NMSOutput.NMSOutput(d = d, declaration = declaration, apiversion = apiversion,
567+
finalReply = v5_0.NMSOutput.NMSOutput(d = d, declaration = declaration, apiversion = apiversion,
551568
b64HttpConf = b64HttpConf, b64StreamConf = b64StreamConf,
552569
configFiles = configFiles,
553570
auxFiles = auxFiles,
554571
runfromautosync = runfromautosync, configUid = configUid )
555572

573+
finalReply['message']['message']['content']['manifests'] = extraOutputManifests
574+
575+
return finalReply
576+
556577
elif decltype.lower() == 'nginxone':
557578
# Output to NGINX One SaaS Console
558579

559580
# NGINX configuration files for staged config
560581
configFiles['name'] = NcgConfig.config['nms']['config_dir']
561582

562583
# NGINX auxiliary files for staged config
563-
# TODO
564-
# auxFiles['name'] = NcgConfig.config['nms']['config_dir']
565-
566-
#return v5_0.NGINXOneOutput.NGINXOneOutput(d = d, declaration = declaration, apiversion = apiversion,
567-
# b64HttpConf = b64HttpConf, b64StreamConf = b64StreamConf,
568-
# configFiles = configFiles,
569-
# auxFiles = auxFiles,
570-
# runfromautosync = runfromautosync, configUid = configUid )
571-
572-
return {"status_code": 501, "message": {"code": 501, "content": "NGINX One support not yet available"}}
584+
auxFiles['name'] = NcgConfig.config['nms']['config_dir']
573585

586+
return v5_0.NGINXOneOutput.NGINXOneOutput(d = d, declaration = declaration, apiversion = apiversion,
587+
b64HttpConf = b64HttpConf, b64StreamConf = b64StreamConf,
588+
configFiles = configFiles,
589+
auxFiles = auxFiles,
590+
runfromautosync = runfromautosync, configUid = configUid )
574591
else:
575592
return {"status_code": 422, "message": {"status_code": 422, "message": f"output type {decltype} unknown"}}
576593

src/V5_0_NginxConfigDeclaration.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -772,16 +772,35 @@ class DevPortal_Redocly(BaseModel, extra="forbid"):
772772
uri: Optional[str] = "/devportal.html"
773773

774774

775+
class DevPortal_Backstage(BaseModel, extra="forbid"):
776+
name: str
777+
lifecycle: Optional[str] = "production"
778+
owner: str = ""
779+
system: Optional[str] = ""
780+
781+
@model_validator(mode='after')
782+
def check_type(self) -> 'DevPortal_Backstage':
783+
_lifecycle = self.lifecycle
784+
785+
valid = ['experimental', 'production', 'deprecated']
786+
if _lifecycle not in valid:
787+
raise ValueError(f"Invalid developer portal type [{_lifecycle}] must be one of {str(valid)}")
788+
789+
return self
790+
791+
775792
class DeveloperPortal(BaseModel, extra="forbid"):
776793
enabled: Optional[bool] = False
777794
type: str
778795
redocly: Optional[DevPortal_Redocly] = {}
796+
backstage: Optional[DevPortal_Backstage] = {}
779797

780798
@model_validator(mode='after')
781799
def check_type(self) -> 'DeveloperPortal':
782-
_type, _redocly = self.type, self.redocly
800+
_type, _redocly, _backstage = self.type, self.redocly, self.backstage
801+
802+
valid = ['redocly', 'backstage']
783803

784-
valid = ['redocly']
785804
if _type not in valid:
786805
raise ValueError(f"Invalid developer portal type [{_type}] must be one of {str(valid)}")
787806

@@ -790,6 +809,9 @@ def check_type(self) -> 'DeveloperPortal':
790809
if _type == 'redocly' and not _redocly:
791810
isError = True
792811

812+
if _type == 'backstage' and not _backstage:
813+
isError = True
814+
793815
if isError:
794816
raise ValueError(f"Missing developer portal data for type [{_type}]")
795817

src/v5_0/APIGateway.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ def createAPIGateway(locationDeclaration: dict, authProfiles: Authentication={})
3434
apiGwDeclaration['paths'] = apiSchema.paths()
3535
apiGwDeclaration['version'] = apiSchema.version()
3636

37-
return 200, apiGwDeclaration
37+
return 200, apiGwDeclaration, apiSchemaString['content']

src/v5_0/MiscUtils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ def yaml_to_json(document: str):
4646
return json.dumps(yaml.safe_load(document))
4747

4848

49+
"""
50+
JSON TO YAML conversion
51+
"""
52+
def json_to_yaml(document: str):
53+
return yaml.dump(json.loads(document))
54+
55+
4956
"""
5057
Returns a unique ID
5158
"""

0 commit comments

Comments
 (0)