Skip to content

Commit 8efdfc8

Browse files
authored
NGINX Plus R33 support (#2760) (#2783)
Adding support for NGINX Plus R33. The major change with this release is that NGINX Plus now requires a JWT in order to run. A user must create a Secret with this JWT and supply the secret name to NGF when installing. A user can also create client SSL and CA Secrets for NIM connections. All of these Secrets are mounted to the nginx container. Because of the new usage reporting method, the old usage reporting method has been removed and CLI arguments have been altered. Since this release is a breaking change for N+ users, the choice was made to remove the unused usage reporting flags instead of deprecating them. Updated documentation to describe this process, while also cleaning up the JWT docker registry process for N+.
1 parent 1f60f9e commit 8efdfc8

File tree

83 files changed

+2191
-2078
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+2191
-2078
lines changed

.github/workflows/ci.yml

+2
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ jobs:
238238
with:
239239
image: ${{ matrix.image }}
240240
k8s-version: ${{ matrix.k8s-version }}
241+
secrets: inherit
241242
permissions:
242243
contents: read
243244

@@ -259,6 +260,7 @@ jobs:
259260
image: ${{ matrix.image }}
260261
k8s-version: ${{ matrix.k8s-version }}
261262
enable-experimental: ${{ matrix.enable-experimental }}
263+
secrets: inherit
262264
permissions:
263265
contents: write
264266

.github/workflows/conformance.yml

+6
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ jobs:
135135
kind create cluster --name ${{ github.run_id }} --image=kindest/node:${{ inputs.k8s-version }}
136136
kind load docker-image ${{ join(fromJSON(steps.ngf-meta.outputs.json).tags, ' ') }} ${{ join(fromJSON(steps.nginx-meta.outputs.json).tags, ' ') }} --name ${{ github.run_id }}
137137
138+
- name: Setup license file for plus
139+
if: ${{ inputs.image == 'plus' }}
140+
env:
141+
PLUS_LICENSE: ${{ secrets.JWT_PLUS_REGISTRY }}
142+
run: echo "${PLUS_LICENSE}" > license.jwt
143+
138144
- name: Setup conformance tests
139145
run: |
140146
ngf_prefix=ghcr.io/nginxinc/nginx-gateway-fabric

.github/workflows/functional.yml

+6
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ jobs:
100100
NGINX_CONF_DIR=internal/mode/static/nginx/conf
101101
BUILD_AGENT=gha
102102
103+
- name: Setup license file for plus
104+
if: ${{ inputs.image == 'plus' }}
105+
env:
106+
PLUS_LICENSE: ${{ secrets.JWT_PLUS_REGISTRY }}
107+
run: echo "${PLUS_LICENSE}" > license.jwt
108+
103109
- name: Install cloud-provider-kind
104110
run: |
105111
CLOUD_PROVIDER_KIND_VERSION=v0.4.0 # renovate: datasource=github-tags depName=kubernetes-sigs/cloud-provider-kind

.github/workflows/helm.yml

+15-2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ jobs:
9898
kind create cluster --name ${{ github.run_id }} --image=kindest/node:${{ inputs.k8s-version }}
9999
kind load docker-image ${{ join(fromJSON(steps.ngf-meta.outputs.json).tags, ' ') }} ${{ join(fromJSON(steps.nginx-meta.outputs.json).tags, ' ') }} --name ${{ github.run_id }}
100100
kubectl kustomize config/crd/gateway-api/standard | kubectl apply -f -
101+
kubectl create namespace nginx-gateway
102+
103+
- name: Create plus secret
104+
if: ${{ inputs.image == 'plus' }}
105+
env:
106+
PLUS_LICENSE: ${{ secrets.JWT_PLUS_REGISTRY }}
107+
run: |
108+
echo "${PLUS_LICENSE}" > license.jwt
109+
kubectl create secret generic nplus-license --from-file license.jwt -n nginx-gateway
101110
102111
- name: Set up Python
103112
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
@@ -110,7 +119,7 @@ jobs:
110119

111120
- name: Install Chart
112121
run: |
113-
ct install --config .ct.yaml --helm-extra-set-args="--set=nginxGateway.image.tag=${{ steps.ngf-meta.outputs.version }} \
122+
ct install --config .ct.yaml --namespace nginx-gateway --helm-extra-set-args="--set=nginxGateway.image.tag=${{ steps.ngf-meta.outputs.version }} \
114123
--set=nginx.image.repository=ghcr.io/nginxinc/nginx-gateway-fabric/nginx${{ inputs.image == 'plus' && '-plus' || ''}} \
115124
--set=nginx.plus=${{ inputs.image == 'plus' }} \
116125
--set=nginx.image.tag=${{ steps.nginx-meta.outputs.version }} \
@@ -143,10 +152,14 @@ jobs:
143152
kubectl kustomize config/crd/gateway-api/standard | kubectl apply -f -
144153
kubectl create namespace nginx-gateway
145154
146-
- name: Create k8s secret
155+
- name: Create plus secrets
147156
if: ${{ inputs.image == 'plus' }}
157+
env:
158+
PLUS_LICENSE: ${{ secrets.JWT_PLUS_REGISTRY }}
148159
run: |
160+
echo "${PLUS_LICENSE}" > license.jwt
149161
kubectl create secret docker-registry nginx-plus-registry-secret --docker-server=private-registry.nginx.com --docker-username=${{ secrets.JWT_PLUS_REGISTRY }} --docker-password=none -n nginx-gateway
162+
kubectl create secret generic nplus-license --from-file license.jwt -n nginx-gateway
150163
151164
- name: Set up Python
152165
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0

.github/workflows/nfr.yml

+6
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ jobs:
111111
echo "GKE_NUM_NODES=12" >> vars.env
112112
echo "GKE_MACHINE_TYPE=n2d-standard-16" >> vars.env
113113
114+
- name: Setup license file for plus
115+
if: matrix.type == 'plus'
116+
env:
117+
PLUS_LICENSE: ${{ secrets.JWT_PLUS_REGISTRY }}
118+
run: echo "${PLUS_LICENSE}" > license.jwt
119+
114120
- name: Create GKE cluster
115121
working-directory: ./tests
116122
run: make create-gke-cluster CI=true

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ internal/mode/static/nginx/modules/coverage
4646
*.crt
4747
*.key
4848

49+
# JWT files
50+
*.jwt
51+
4952
# Dotenv files
5053
**/*.env
5154

.hugo_build.lock

Whitespace-only changes.

.yamllint.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ ignore:
33
- charts/nginx-gateway-fabric/templates
44
- config/crd/bases/
55
- deploy/crds.yaml
6+
- deploy/*nginx-plus
67
- site/static
78

89
rules:

Makefile

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
# variables that should not be overridden by the user
22
VERSION = edge
3-
SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
3+
SELF_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
44
CHART_DIR = $(SELF_DIR)charts/nginx-gateway-fabric
55
NGINX_CONF_DIR = internal/mode/static/nginx/conf
66
NJS_DIR = internal/mode/static/nginx/modules/src
77
KIND_CONFIG_FILE = $(SELF_DIR)config/cluster/kind-cluster.yaml
8-
NGINX_DOCKER_BUILD_PLUS_ARGS = --secret id=nginx-repo.crt,src=nginx-repo.crt --secret id=nginx-repo.key,src=nginx-repo.key
9-
BUILD_AGENT=local
10-
PLUS_ENABLED ?= false
8+
NGINX_DOCKER_BUILD_PLUS_ARGS = --secret id=nginx-repo.crt,src=$(SELF_DIR)nginx-repo.crt --secret id=nginx-repo.key,src=$(SELF_DIR)nginx-repo.key
9+
BUILD_AGENT = local
1110

1211
PROD_TELEMETRY_ENDPOINT = oss.edge.df.f5.com:443
1312
# the telemetry related variables below are also configured in goreleaser.yml
@@ -49,6 +48,8 @@ TARGET ?= local## The target of the build. Possible values: local and container
4948
OUT_DIR ?= build/out## The folder where the binary will be stored
5049
GOARCH ?= amd64## The architecture of the image and/or binary. For example: amd64 or arm64
5150
GOOS ?= linux## The OS of the image and/or binary. For example: linux or darwin
51+
PLUS_ENABLED ?= false
52+
PLUS_LICENSE_FILE ?= $(SELF_DIR)license.jwt
5253
override NGINX_DOCKER_BUILD_OPTIONS += --build-arg NJS_DIR=$(NJS_DIR) --build-arg NGINX_CONF_DIR=$(NGINX_CONF_DIR) --build-arg BUILD_AGENT=$(BUILD_AGENT)
5354

5455
.DEFAULT_GOAL := help
@@ -227,7 +228,9 @@ helm-install-local: install-gateway-crds ## Helm install NGF on configured kind
227228

228229
.PHONY: helm-install-local-with-plus
229230
helm-install-local-with-plus: install-gateway-crds ## Helm install NGF with NGINX Plus on configured kind cluster with local images. To build, load, and install with helm run make install-ngf-local-build-with-plus.
230-
helm install nginx-gateway $(CHART_DIR) --set nginx.image.repository=$(NGINX_PLUS_PREFIX) --create-namespace --wait --set nginxGateway.image.pullPolicy=Never --set service.type=NodePort --set nginxGateway.image.repository=$(PREFIX) --set nginxGateway.image.tag=$(TAG) --set nginx.image.tag=$(TAG) --set nginx.image.pullPolicy=Never --set nginxGateway.gwAPIExperimentalFeatures.enable=$(ENABLE_EXPERIMENTAL) -n nginx-gateway --set nginx.plus=true $(HELM_PARAMETERS)
231+
kubectl create namespace nginx-gateway || true
232+
kubectl -n nginx-gateway create secret generic nplus-license --from-file $(PLUS_LICENSE_FILE) || true
233+
helm install nginx-gateway $(CHART_DIR) --set nginx.image.repository=$(NGINX_PLUS_PREFIX) --wait --set nginxGateway.image.pullPolicy=Never --set service.type=NodePort --set nginxGateway.image.repository=$(PREFIX) --set nginxGateway.image.tag=$(TAG) --set nginx.image.tag=$(TAG) --set nginx.image.pullPolicy=Never --set nginxGateway.gwAPIExperimentalFeatures.enable=$(ENABLE_EXPERIMENTAL) -n nginx-gateway --set nginx.plus=true $(HELM_PARAMETERS)
231234

232235
# Debug Targets
233236
.PHONY: debug-build

build/Dockerfile.nginx

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ ARG NGINX_CONF_DIR
66
ARG BUILD_AGENT
77

88
RUN apk add --no-cache libcap \
9-
&& mkdir -p /var/lib/nginx /usr/lib/nginx/modules \
9+
&& mkdir -p /usr/lib/nginx/modules \
1010
&& setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx \
1111
&& setcap -v 'cap_net_bind_service=+ep' /usr/sbin/nginx \
1212
&& setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx-debug \
@@ -18,7 +18,7 @@ COPY ${NGINX_CONF_DIR}/nginx.conf /etc/nginx/nginx.conf
1818
COPY ${NGINX_CONF_DIR}/grpc-error-locations.conf /etc/nginx/grpc-error-locations.conf
1919
COPY ${NGINX_CONF_DIR}/grpc-error-pages.conf /etc/nginx/grpc-error-pages.conf
2020

21-
RUN chown -R 101:1001 /etc/nginx /var/cache/nginx /var/lib/nginx
21+
RUN chown -R 101:1001 /etc/nginx /var/cache/nginx
2222

2323
LABEL org.nginx.ngf.image.build.agent="${BUILD_AGENT}"
2424

build/Dockerfile.nginxplus

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ ADD --link --chown=101:1001 https://cs.nginx.com/static/keys/nginx_signing.rsa.p
77

88
FROM alpine:3.20
99

10-
ARG NGINX_PLUS_VERSION=R32
10+
ARG NGINX_PLUS_VERSION=R33
1111
ARG NJS_DIR
1212
ARG NGINX_CONF_DIR
1313
ARG BUILD_AGENT
@@ -19,7 +19,7 @@ RUN --mount=type=secret,id=nginx-repo.crt,dst=/etc/apk/cert.pem,mode=0644 \
1919
&& adduser -S -D -H -u 101 -h /var/cache/nginx -s /sbin/nologin -G nginx -g nginx nginx \
2020
&& printf "%s\n" "https://pkgs.nginx.com/plus/${NGINX_PLUS_VERSION}/alpine/v$(grep -E -o '^[0-9]+\.[0-9]+' /etc/alpine-release)/main" >> /etc/apk/repositories \
2121
&& apk add --no-cache nginx-plus nginx-plus-module-njs nginx-plus-module-otel libcap \
22-
&& mkdir -p /var/lib/nginx /usr/lib/nginx/modules \
22+
&& mkdir -p /usr/lib/nginx/modules \
2323
&& setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx \
2424
&& setcap -v 'cap_net_bind_service=+ep' /usr/sbin/nginx \
2525
&& setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx-debug \

charts/nginx-gateway-fabric/README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -268,10 +268,12 @@ The following table lists the configurable parameters of the NGINX Gateway Fabri
268268
| `nginx.image.tag` | | string | `"edge"` |
269269
| `nginx.lifecycle` | The lifecycle of the nginx container. | object | `{}` |
270270
| `nginx.plus` | Is NGINX Plus image being used | bool | `false` |
271-
| `nginx.usage.clusterName` | The display name of the Kubernetes cluster in the NGINX Plus usage reporting server. | string | `""` |
272-
| `nginx.usage.insecureSkipVerify` | Disable client verification of the NGINX Plus usage reporting server certificate. | bool | `false` |
273-
| `nginx.usage.secretName` | The namespace/name of the Secret containing the credentials for NGINX Plus usage reporting. | string | `""` |
274-
| `nginx.usage.serverURL` | The base server URL of the NGINX Plus usage reporting server. | string | `""` |
271+
| `nginx.usage.caSecretName` | The name of the Secret containing the NGINX Instance Manager CA certificate. Must exist in the same namespace that the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway). | string | `""` |
272+
| `nginx.usage.clientSSLSecretName` | The name of the Secret containing the client certificate and key for authenticating with NGINX Instance Manager. Must exist in the same namespace that the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway). | string | `""` |
273+
| `nginx.usage.endpoint` | The endpoint of the NGINX Plus usage reporting server. Default: product.connect.nginx.com | string | `""` |
274+
| `nginx.usage.resolver` | The nameserver used to resolve the NGINX Plus usage reporting endpoint. Used with NGINX Instance Manager. | string | `""` |
275+
| `nginx.usage.secretName` | The name of the Secret containing the JWT for NGINX Plus usage reporting. Must exist in the same namespace that the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway). | string | `"nplus-license"` |
276+
| `nginx.usage.skipVerify` | Disable client verification of the NGINX Plus usage reporting server certificate. | bool | `false` |
275277
| `nginxGateway.config.logging.level` | Log level. | string | `"info"` |
276278
| `nginxGateway.configAnnotations` | Set of custom annotations for NginxGateway objects. | object | `{}` |
277279
| `nginxGateway.extraVolumeMounts` | extraVolumeMounts are the additional volume mounts for the nginx-gateway container. | list | `[]` |

charts/nginx-gateway-fabric/templates/clusterrole.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ rules:
1818
- get
1919
- list
2020
- watch
21-
{{- if .Values.nginxGateway.productTelemetry.enable }}
21+
{{- if or .Values.nginxGateway.productTelemetry.enable .Values.nginx.plus }}
2222
- apiGroups:
2323
- ""
2424
resources:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: nginx-includes-bootstrap
5+
namespace: {{ .Release.Namespace }}
6+
labels:
7+
{{- include "nginx-gateway.labels" . | nindent 4 }}
8+
data:
9+
main.conf: |
10+
{{- if and .Values.nginx.config .Values.nginx.config.logging .Values.nginx.config.logging.errorLevel }}
11+
error_log stderr {{ .Values.nginx.config.logging.errorLevel }};
12+
{{ else }}
13+
error_log stderr info;
14+
{{- end }}
15+
{{- if .Values.nginx.plus }}
16+
mgmt.conf: |
17+
mgmt {
18+
{{- if .Values.nginx.usage.endpoint }}
19+
usage_report endpoint={{ .Values.nginx.usage.endpoint }};
20+
{{- end }}
21+
{{- if .Values.nginx.usage.skipVerify }}
22+
ssl_verify off;
23+
{{- end }}
24+
{{- if .Values.nginx.usage.caSecretName }}
25+
ssl_trusted_certificate /etc/nginx/certs-bootstrap/ca.crt;
26+
{{- end }}
27+
{{- if .Values.nginx.usage.clientSSLSecretName }}
28+
ssl_certificate /etc/nginx/certs-bootstrap/tls.crt;
29+
ssl_certificate_key /etc/nginx/certs-bootstrap/tls.key;
30+
{{- end }}
31+
enforce_initial_report off;
32+
}
33+
{{- end }}

charts/nginx-gateway-fabric/templates/deployment.yaml

+61-16
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,12 @@ spec:
4242
- copy
4343
- --source
4444
- /includes/main.conf
45+
{{- if .Values.nginx.plus }}
46+
- --source
47+
- /includes/mgmt.conf
48+
{{- end }}
4549
- --destination
46-
- /etc/nginx/main-includes/main.conf
50+
- /etc/nginx/main-includes
4751
securityContext:
4852
seccompProfile:
4953
type: RuntimeDefault
@@ -56,7 +60,7 @@ spec:
5660
runAsUser: 102
5761
runAsGroup: 1001
5862
volumeMounts:
59-
- name: nginx-includes-configmap
63+
- name: nginx-includes-bootstrap
6064
mountPath: /includes
6165
- name: nginx-main-includes
6266
mountPath: /etc/nginx/main-includes
@@ -69,6 +73,24 @@ spec:
6973
- --service={{ include "nginx-gateway.fullname" . }}
7074
{{- if .Values.nginx.plus }}
7175
- --nginx-plus
76+
{{- if .Values.nginx.usage.secretName }}
77+
- --usage-report-secret={{ .Values.nginx.usage.secretName }}
78+
{{- end }}
79+
{{- if .Values.nginx.usage.endpoint }}
80+
- --usage-report-endpoint={{ .Values.nginx.usage.endpoint }}
81+
{{- end }}
82+
{{- if .Values.nginx.usage.resolver }}
83+
- --usage-report-resolver={{ .Values.nginx.usage.resolver }}
84+
{{- end }}
85+
{{- if .Values.nginx.usage.skipVerify }}
86+
- --usage-report-skip-verify
87+
{{- end }}
88+
{{- if .Values.nginx.usage.caSecretName }}
89+
- --usage-report-ca-secret={{ .Values.nginx.usage.caSecretName }}
90+
{{- end }}
91+
{{- if .Values.nginx.usage.clientSSLSecretName }}
92+
- --usage-report-client-ssl-secret={{ .Values.nginx.usage.clientSSLSecretName }}
93+
{{- end }}
7294
{{- end }}
7395
{{- if .Values.metrics.enable }}
7496
- --metrics-port={{ .Values.metrics.port }}
@@ -94,18 +116,6 @@ spec:
94116
{{- if .Values.nginxGateway.gwAPIExperimentalFeatures.enable }}
95117
- --gateway-api-experimental-features
96118
{{- end }}
97-
{{- if .Values.nginx.usage.secretName }}
98-
- --usage-report-secret={{ .Values.nginx.usage.secretName }}
99-
{{- end }}
100-
{{- if .Values.nginx.usage.serverURL }}
101-
- --usage-report-server-url={{ .Values.nginx.usage.serverURL }}
102-
{{- end }}
103-
{{- if .Values.nginx.usage.clusterName }}
104-
- --usage-report-cluster-name={{ .Values.nginx.usage.clusterName }}
105-
{{- end }}
106-
{{- if .Values.nginx.usage.insecureSkipVerify }}
107-
- --usage-report-skip-verify
108-
{{- end }}
109119
{{- if .Values.nginxGateway.snippetsFilters.enable }}
110120
- --snippets-filters
111121
{{- end }}
@@ -214,6 +224,19 @@ spec:
214224
mountPath: /var/cache/nginx
215225
- name: nginx-includes
216226
mountPath: /etc/nginx/includes
227+
{{- if .Values.nginx.plus }}
228+
- name: nginx-lib
229+
mountPath: /var/lib/nginx/state
230+
{{- if .Values.nginx.usage.secretName }}
231+
- name: nginx-plus-license
232+
mountPath: /etc/nginx/license.jwt
233+
subPath: license.jwt
234+
{{- end }}
235+
{{- if or .Values.nginx.usage.caSecretName .Values.nginx.usage.clientSSLSecretName }}
236+
- name: nginx-plus-usage-certs
237+
mountPath: /etc/nginx/certs-bootstrap/
238+
{{- end }}
239+
{{- end }}
217240
{{- with .Values.nginx.extraVolumeMounts -}}
218241
{{ toYaml . | nindent 8 }}
219242
{{- end }}
@@ -257,9 +280,31 @@ spec:
257280
emptyDir: {}
258281
- name: nginx-includes
259282
emptyDir: {}
260-
- name: nginx-includes-configmap
283+
- name: nginx-includes-bootstrap
261284
configMap:
262-
name: nginx-includes
285+
name: nginx-includes-bootstrap
286+
{{- if .Values.nginx.plus }}
287+
- name: nginx-lib
288+
emptyDir: {}
289+
{{- if .Values.nginx.usage.secretName }}
290+
- name: nginx-plus-license
291+
secret:
292+
secretName: {{ .Values.nginx.usage.secretName }}
293+
{{- end }}
294+
{{- if or .Values.nginx.usage.caSecretName .Values.nginx.usage.clientSSLSecretName }}
295+
- name: nginx-plus-usage-certs
296+
projected:
297+
sources:
298+
{{- if .Values.nginx.usage.caSecretName }}
299+
- secret:
300+
name: {{ .Values.nginx.usage.caSecretName }}
301+
{{- end }}
302+
{{- if .Values.nginx.usage.clientSSLSecretName }}
303+
- secret:
304+
name: {{ .Values.nginx.usage.clientSSLSecretName }}
305+
{{- end }}
306+
{{- end }}
307+
{{- end }}
263308
{{- with .Values.extraVolumes -}}
264309
{{ toYaml . | nindent 6 }}
265310
{{- end }}

charts/nginx-gateway-fabric/templates/include-configmap.yaml

-14
This file was deleted.

charts/nginx-gateway-fabric/templates/scc.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ volumes:
3333
- emptyDir
3434
- secret
3535
- configMap
36+
- projected
3637
users:
3738
- {{ printf "system:serviceaccount:%s:%s" .Release.Namespace (include "nginx-gateway.serviceAccountName" .) }}
3839
allowedCapabilities:

0 commit comments

Comments
 (0)