diff --git a/console-extensions.json b/console-extensions.json
index f295227..a01658e 100644
--- a/console-extensions.json
+++ b/console-extensions.json
@@ -18,6 +18,17 @@
"component": { "$codeRef": "GatewayCreatePage" }
}
},
+ {
+ "type": "console.resource/create",
+ "properties": {
+ "model": {
+ "group": "gateway.networking.k8s.io",
+ "version": "v1",
+ "kind": "HTTPRoute"
+ },
+ "component": { "$codeRef": "HTTPRouteCreatePage" }
+ }
+ },
{
"type": "console.tab/horizontalNav",
"properties": {
@@ -33,6 +44,25 @@
"component": { "$codeRef": "GatewaySingleOverview" }
}
},
+ {
+ "type": "console.page/route",
+ "properties": {
+ "exact": true,
+ "path": "/k8s/ns/:namespace/gateway.networking.k8s.io~v1~HTTPRoute/:name/edit",
+ "component": { "$codeRef": "HTTPRouteCreatePage" }
+ }
+ },
+ {
+ "type": "console.action/resource-provider",
+ "properties": {
+ "model": {
+ "group": "gateway.networking.k8s.io",
+ "version": "v1",
+ "kind": "HTTPRoute"
+ },
+ "provider": { "$codeRef": "useHTTPRouteActions" }
+ }
+ },
{
"type": "console.tab/horizontalNav",
"properties": {
@@ -48,4 +78,4 @@
"component": { "$codeRef": "HTTPRouteSingleOverview" }
}
}
-]
\ No newline at end of file
+]
diff --git a/locales/en/plugin__gateway-api-console-plugin.json b/locales/en/plugin__gateway-api-console-plugin.json
index 42c6b94..49f80bb 100644
--- a/locales/en/plugin__gateway-api-console-plugin.json
+++ b/locales/en/plugin__gateway-api-console-plugin.json
@@ -1,21 +1,33 @@
{
"A Gateway represents an instance of a service-traffic handling infrastructure by binding Listeners to a set of IP addresses.": "A Gateway represents an instance of a service-traffic handling infrastructure by binding Listeners to a set of IP addresses.",
+ "A list of actions to perform on a request or response before it is sent to the backend.": "A list of actions to perform on a request or response before it is sent to the backend.",
"A map of key/value pairs to enable implementation-specific TLS options, such as minimum TLS version or cipher suites.": "A map of key/value pairs to enable implementation-specific TLS options, such as minimum TLS version or cipher suites.",
"A unique name for the gateway within the namespace.": "A unique name for the gateway within the namespace.",
"A unique name for this listener within the gateway.": "A unique name for this listener within the gateway.",
"Actions": "Actions",
"Add": "Add",
"Add address": "Add address",
+ "Add backend reference": "Add backend reference",
"Add certificate reference": "Add certificate reference",
+ "Add filter": "Add filter",
+ "Add hostname": "Add hostname",
"Add listener": "Add listener",
- "Add Listener": "Add Listener",
+ "Add match": "Add match",
+ "Add more": "Add more",
+ "Add parent reference": "Add parent reference",
"Add route kind": "Add route kind",
+ "Add rule": "Add rule",
"Add TLS option": "Add TLS option",
+ "Add, set, or remove request headers.": "Add, set, or remove request headers.",
+ "Add, set, or remove response headers.": "Add, set, or remove response headers.",
"Addresses (Optional)": "Addresses (Optional)",
"Addresses help": "Addresses help",
"Addresses table": "Addresses table",
"All": "All",
+ "All backend references": "All backend references",
+ "All filters": "All filters",
"All hostnames": "All hostnames",
+ "All matches": "All matches",
"All route kinds": "All route kinds",
"Allow from all namespaces.": "Allow from all namespaces.",
"Allow from namespaces matching a specific label.": "Allow from namespaces matching a specific label.",
@@ -24,6 +36,13 @@
"Allowed Route Kinds": "Allowed Route Kinds",
"Allowed Routes": "Allowed Routes",
"At least one listener is required to create a Gateway.": "At least one listener is required to create a Gateway.",
+ "At least one parent reference required for the HTTPRoute": "At least one parent reference required for the HTTPRoute",
+ "At least one rule is required for a HTTPRoute to handle traffic.": "At least one rule is required for a HTTPRoute to handle traffic.",
+ "Attaches this route to the selected Gateway(s), linking it to the entry point.": "Attaches this route to the selected Gateway(s), linking it to the entry point.",
+ "Auto-filled from selected service": "Auto-filled from selected service",
+ "Backend references": "Backend references",
+ "Backend References": "Backend References",
+ "Backend Service": "Backend Service",
"Cancel": "Cancel",
"Certificate name": "Certificate name",
"Certificate namespace": "Certificate namespace",
@@ -32,30 +51,57 @@
"Certificate references for TLS termination. Specify the name, namespace, and kind of the certificate resource.": "Certificate references for TLS termination. Specify the name, namespace, and kind of the certificate resource.",
"Configuration": "Configuration",
"Create": "Create",
+ "Create an HTTPRoute to route traffic from the Gateway to backend services.": "Create an HTTPRoute to route traffic from the Gateway to backend services.",
"Create Gateway": "Create Gateway",
+ "Create HTTPRoute": "Create HTTPRoute",
+ "Defines the backend Kubernetes Service(s) to forward traffic to. Traffic is load-balanced between them based on weight.": "Defines the backend Kubernetes Service(s) to forward traffic to. Traffic is load-balanced between them based on weight.",
+ "Defines the backend Kubernetes Service(s) to forward traffic to. Traffic is load-balanced between them based on weight. If omitted, this rule will have no effect.": "Defines the backend Kubernetes Service(s) to forward traffic to. Traffic is load-balanced between them based on weight. If omitted, this rule will have no effect.",
+ "Defines the criteria for a request to match this rule. If multiple matches are specified, they are OR ed. If omitted, this rule matches all requests.": "Defines the criteria for a request to match this rule. If multiple matches are specified, they are OR ed. If omitted, this rule matches all requests.",
+ "Defines the criteria for a request to match this rule. If multiple matches are specified, they are OR ed. If omitted, this rule matches all requests. Multiple Matches in one Rule share all BackendRefs.": "Defines the criteria for a request to match this rule. If multiple matches are specified, they are OR ed. If omitted, this rule matches all requests. Multiple Matches in one Rule share all BackendRefs.",
+ "Delete": "Delete",
+ "Delete it permanently": "Delete it permanently",
+ "Delete match": "Delete match",
"e.g., 192.168.1.100": "e.g., 192.168.1.100",
"e.g., gateway.example.com": "e.g., gateway.example.com",
"e.g., gateway.networking.k8s.io": "e.g., gateway.networking.k8s.io",
"e.g., minVersion, cipherSuites": "e.g., minVersion, cipherSuites",
"e.g., TLSv1.2, ECDHE-RSA-AES256-GCM-SHA384": "e.g., TLSv1.2, ECDHE-RSA-AES256-GCM-SHA384",
+ "Edit": "Edit",
+ "Edit an HTTPRoute to route traffic from the Gateway to backend services.": "Edit an HTTPRoute to route traffic from the Gateway to backend services.",
"Edit Gateway": "Edit Gateway",
+ "Edit HTTPRoute": "Edit HTTPRoute",
"Edit listener": "Edit listener",
- "Edit Listener": "Edit Listener",
+ "Edit rule": "Edit rule",
"Enter gateway name": "Enter gateway name",
"Enter hostname (optional)": "Enter hostname (optional)",
"Enter listener name": "Enter listener name",
"Enter port (1-65535)": "Enter port (1-65535)",
"Envoy Gateway": "Envoy Gateway",
"Error: YAML Validation": "Error: YAML Validation",
+ "example.com": "example.com",
+ "Filter type": "Filter type",
+ "Filters": "Filters",
"Gateway class cannot be changed after creation.": "Gateway class cannot be changed after creation.",
"Gateway Class Name": "Gateway Class Name",
- "Gateway Name": "Gateway Name",
+ "Gateway is terminating.": "Gateway is terminating.",
+ "Gateway name": "Gateway name",
"Gateway names cannot be changed after creation.": "Gateway names cannot be changed after creation.",
+ "Gateway Namespace": "Gateway Namespace",
"Group": "Group",
+ "Header name": "Header name",
+ "Header value": "Header value",
+ "Headers": "Headers",
"Hostname": "Hostname",
+ "Hostnames": "Hostnames",
+ "Hostnames help": "Hostnames help",
+ "HTTP method": "HTTP method",
+ "HTTPRoute name": "HTTPRoute name",
+ "Inherits from parent listener if empty.": "Inherits from parent listener if empty.",
"Istio": "Istio",
"Key": "Key",
"Kind": "Kind",
+ "Learn more": "Learn more",
+ "Listener is not available for route binding.": "Listener is not available for route binding.",
"Listener Name": "Listener Name",
"Listener Summary": "Listener Summary",
"Listeners": "Listeners",
@@ -64,7 +110,11 @@
"Listeners table": "Listeners table",
"Loading gateway...": "Loading gateway...",
"Loading YAML editor...": "Loading YAML editor...",
+ "Matches": "Matches",
+ "Matches traffic for these hostnames.": "Matches traffic for these hostnames.",
+ "Mirror backend name": "Mirror backend name",
"Missing required metadata fields (uid, resourceVersion) for {{kind}} update": "Missing required metadata fields (uid, resourceVersion) for {{kind}} update",
+ "more": "more",
"N/A": "N/A",
"Name": "Name",
"Namespace": "Namespace",
@@ -72,31 +122,86 @@
"No attached resources found": "No attached resources found",
"No matching resources found": "No matching resources found",
"None": "None",
+ "Not allowed by Gateway settings.": "Not allowed by Gateway settings.",
+ "Not set": "Not set",
"Not specified": "Not specified",
+ "Only HTTPRoute is supported by this Gateway.": "Only HTTPRoute is supported by this Gateway.",
"Optional hostname to match requests. Leave empty to match all hostnames.": "Optional hostname to match requests. Leave empty to match all hostnames.",
+ "Parent reference": "Parent reference",
+ "Parent references": "Parent references",
+ "Parent references help": "Parent references help",
+ "Path redirect": "Path redirect",
+ "Path redirect type": "Path redirect type",
+ "Path replacement type": "Path replacement type",
+ "Path rewrite": "Path rewrite",
+ "Path type": "Path type",
+ "Path value": "Path value",
"Port": "Port",
"Protocol": "Protocol",
+ "Query param name": "Query param name",
+ "Query param value": "Query param value",
+ "Query Params": "Query Params",
+ "Redirect path value": "Redirect path value",
+ "Redirect the request to a different hostname, path, or port.": "Redirect the request to a different hostname, path, or port.",
+ "Redirect type": "Redirect type",
"Remove": "Remove",
"Remove address": "Remove address",
"Remove listener": "Remove listener",
+ "Remove parent reference": "Remove parent reference",
"Request a specific static IP address or hostname for the Gateway. This is optional and used to specify where the Gateway should be accessible.": "Request a specific static IP address or hostname for the Gateway. This is optional and used to specify where the Gateway should be accessible.",
+ "Request Header Modifier": "Request Header Modifier",
+ "Request Mirror": "Request Mirror",
+ "Request Redirect": "Request Redirect",
+ "Requests are evaluated against rules in order.": "Requests are evaluated against rules in order.",
+ "Response Header Modifier": "Response Header Modifier",
+ "Restart configuration": "Restart configuration",
"Restricts the types of Route resources that can attach to this listener (e.g., only HTTPRoute).": "Restricts the types of Route resources that can attach to this listener (e.g., only HTTPRoute).",
"Review & Create": "Review & Create",
+ "Review and create": "Review and create",
+ "Rewrite the hostname or path of the request before forwarding.": "Rewrite the hostname or path of the request before forwarding.",
"Route Kind": "Route Kind",
+ "Rule ID": "Rule ID",
+ "Rule incomplete": "Rule incomplete",
+ "Rules": "Rules",
+ "Rules are used for matching and processing requests. ": "Rules are used for matching and processing requests. ",
+ "Rules help": "Rules help",
+ "Rules table": "Rules table",
"Same": "Same",
"Save": "Save",
+ "Scheme": "Scheme",
+ "Scheme redirect": "Scheme redirect",
"Search by {{filterValue}}...": "Search by {{filterValue}}...",
+ "Section": "Section",
+ "Section name": "Section name",
"Select Address Type": "Select Address Type",
"Select Allowed Namespaces": "Select Allowed Namespaces",
"Select Certificate Kind": "Select Certificate Kind",
+ "Select Gateway": "Select Gateway",
"Select Gateway Class": "Select Gateway Class",
+ "Select header action": "Select header action",
+ "Select header type": "Select header type",
+ "Select HTTP method": "Select HTTP method",
+ "Select path type": "Select path type",
+ "Select Port": "Select Port",
"Select Protocol": "Select Protocol",
+ "Select query param type": "Select query param type",
"Select Route Kind": "Select Route Kind",
+ "Select Section": "Select Section",
+ "Select Service": "Select Service",
"Select TLS Mode": "Select TLS Mode",
+ "Select type": "Select type",
"Selector": "Selector",
+ "Send a copy of the request to a different backend (for traffic shadowing).": "Send a copy of the request to a different backend (for traffic shadowing).",
+ "Service Name": "Service Name",
+ "Set": "Set",
+ "Show detailed errors": "Show detailed errors",
+ "Show warnings": "Show warnings",
"Some references for the HTTPRoute could not be resolved.": "Some references for the HTTPRoute could not be resolved.",
"Specify static IP addresses or hostnames where the Gateway should be accessible. This is optional and depends on your infrastructure setup.": "Specify static IP addresses or hostnames where the Gateway should be accessible. This is optional and depends on your infrastructure setup.",
"Status": "Status",
+ "Status code": "Status code",
+ "Supports wildcards (e.g., *.example.com).": "Supports wildcards (e.g., *.example.com).",
+ "The first rule that matches is used.": "The first rule that matches is used.",
"The gateway class name must be unique within the namespace and conform to DNS-1123 label standards (lowercase alphanumeric characters or \"-\").": "The gateway class name must be unique within the namespace and conform to DNS-1123 label standards (lowercase alphanumeric characters or \"-\").",
"The Gateway configuration is accepted but not yet programmed.": "The Gateway configuration is accepted but not yet programmed.",
"The Gateway has issues and is not ready to serve traffic.": "The Gateway has issues and is not ready to serve traffic.",
@@ -104,18 +209,26 @@
"The Gateway is accepted, programmed, and ready to serve traffic.": "The Gateway is accepted, programmed, and ready to serve traffic.",
"The HTTPRoute is accepted by at least one parent gateway.": "The HTTPRoute is accepted by at least one parent gateway.",
"The HTTPRoute is not accepted by any parent gateways.": "The HTTPRoute is not accepted by any parent gateways.",
+ "The name of the Gateway to attach to.": "The name of the Gateway to attach to.",
+ "The namespace of the Gateway.": "The namespace of the Gateway.",
"The network port that this listener will bind to (1-65535).": "The network port that this listener will bind to (1-65535).",
"The protocol that this listener will accept.": "The protocol that this listener will accept.",
"The resource is being processed.": "The resource is being processed.",
"The status of the resource is unknown.": "The status of the resource is unknown.",
"This resource has no related items configured": "This resource has no related items configured",
+ "This rule cannot be created until it includes at least one match, filter, or backend reference.": "This rule cannot be created until it includes at least one match, filter, or backend reference.",
+ "This rule has validation errors that must be fixed before creation.": "This rule has validation errors that must be fixed before creation.",
"TLS Mode": "TLS Mode",
"TLS Option": "TLS Option",
"TLS Options": "TLS Options",
"TLS termination mode. Terminate decrypts TLS at the gateway, Passthrough forwards encrypted traffic.": "TLS termination mode. Terminate decrypts TLS at the gateway, Passthrough forwards encrypted traffic.",
"Try adjusting your search criteria": "Try adjusting your search criteria",
"Type": "Type",
+ "Unique name of the HTTPRoute": "Unique name of the HTTPRoute",
"Unnamed": "Unnamed",
"Update": "Update",
- "Value": "Value"
+ "URL Rewrite": "URL Rewrite",
+ "Validation warnings": "Validation warnings",
+ "Value": "Value",
+ "Weight": "Weight"
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 1d5a675..8fa3279 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,9 @@
"exposedModules": {
"GatewaySingleOverview": "./components/GatewaySingleOverview",
"HTTPRouteSingleOverview": "./components/HTTPRouteSingleOverview",
- "GatewayCreatePage": "./components/GatewayCreatePage"
+ "GatewayCreatePage": "./components/GatewayCreatePage",
+ "HTTPRouteCreatePage": "./components/HTTPRouteCreatePage",
+ "useHTTPRouteActions": "./components/httproute/useHTTPRouteActions"
},
"dependencies": {
"@console/pluginAPI": "*"
diff --git a/src/components/GatewayApiCreateUpdate.tsx b/src/components/GatewayApiCreateUpdate.tsx
index 650ea45..b99650e 100644
--- a/src/components/GatewayApiCreateUpdate.tsx
+++ b/src/components/GatewayApiCreateUpdate.tsx
@@ -9,12 +9,17 @@ import {
PageSection,
ActionGroup,
} from '@patternfly/react-core';
-import { k8sCreate, k8sUpdate } from '@openshift-console/dynamic-plugin-sdk';
+import {
+ k8sCreate,
+ k8sUpdate,
+ K8sModel,
+ K8sResourceCommon,
+} from '@openshift-console/dynamic-plugin-sdk';
-interface GatewayApiCreateUpdateProps {
- resource: any;
+interface GatewayApiCreateUpdateProps {
+ resource: TResource;
formValidation: boolean;
- model: any;
+ model: K8sModel;
ns: string;
view: string;
resourceKind?: string;
@@ -83,12 +88,12 @@ const GatewayApiCreateUpdate: React.FC = ({
const resourcePath = `${model.apiGroup}~${model.apiVersion}~${model.kind}`;
history.push(`/k8s/ns/${ns}/${resourcePath}`);
}
- } catch (error) {
+ } catch (error: unknown) {
const action = update ? 'updating' : 'creating';
setErrorAlertMsg(
t(`Error ${action} {{kind}}: {{error}}`, {
kind: resourceKind,
- error: error,
+ error: error instanceof Error ? error.message : String(error),
}),
);
}
diff --git a/src/components/GatewayCreatePage.tsx b/src/components/GatewayCreatePage.tsx
index 6909e5c..624b753 100644
--- a/src/components/GatewayCreatePage.tsx
+++ b/src/components/GatewayCreatePage.tsx
@@ -34,11 +34,14 @@ import {
ResourceYAMLEditor,
useActiveNamespace,
useK8sWatchResource,
+ getGroupVersionKindForResource,
+ useK8sModel,
} from '@openshift-console/dynamic-plugin-sdk';
import * as yaml from 'js-yaml';
import GatewayApiCreateUpdate from './GatewayApiCreateUpdate';
import { useLocation } from 'react-router';
import { GatewayResource } from './gateway/GatewayModel';
+import type { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk';
import {
generateUniqueId,
removeCertsAndTlsOptionsForPassthrough,
@@ -50,14 +53,16 @@ const GatewayCreatePage: React.FC = () => {
const [createView, setCreateView] = React.useState<'form' | 'yaml'>('form');
const [selectedNamespace] = useActiveNamespace();
const [create, setCreate] = React.useState(true);
- const [originalMetadata, setOriginalMetadata] = React.useState(null);
+ const [originalMetadata, setOriginalMetadata] = React.useState<
+ K8sResourceCommon['metadata'] | null
+ >(null);
// Gateway settings
const [gatewayName, setGatewayName] = React.useState('');
const [gatewayClassName, setGatewayClassName] = React.useState('istio');
// YAML editor
- const [yamlContent, setYamlContent] = React.useState(null);
+ const [yamlContent, setYamlContent] = React.useState(null);
const location = useLocation();
const pathSplit = location.pathname.split('/');
@@ -66,17 +71,41 @@ const GatewayCreatePage: React.FC = () => {
// Modal
const [isModalOpen, setIsModalOpen] = React.useState(false);
- const [listeners, setListeners] = React.useState([]);
+ type TLSOptionRow = { id: string; key: string; value: string };
+ type CertificateRefRow = {
+ id: string;
+ name: string;
+ namespace?: string;
+ kind: 'Secret' | 'ConfigMap';
+ };
+ type AllowedRouteKindRow = { id: string; kind: string; group: string };
+ type AllowedRoutesUI = {
+ namespaces: { from: 'All' | 'Same' | 'Selector' };
+ kinds: AllowedRouteKindRow[];
+ };
+ type ListenerUI = {
+ name: string;
+ protocol: 'HTTP' | 'HTTPS' | 'TLS' | 'TCP' | 'UDP';
+ port: number;
+ hostname: string;
+ tlsMode: 'Terminate' | 'Passthrough';
+ tlsOptions: TLSOptionRow[];
+ certificateRefs: CertificateRefRow[];
+ allowedRoutes: AllowedRoutesUI;
+ };
+ type AddressRow = { id: string; type: 'IPAddress' | 'Hostname'; value: string };
+
+ const [listeners, setListeners] = React.useState([]);
const [editingListenerIndex, setEditingListenerIndex] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false);
const [yamlError, setYamlError] = React.useState(null);
// Addresses settings
- const [addresses, setAddresses] = React.useState([]);
+ const [addresses, setAddresses] = React.useState([]);
const [isAddressesExpanded, setIsAddressesExpanded] = React.useState(false);
// Prefilled data for listener fields (temporary data)
- const [currentListener, setCurrentListener] = React.useState({
+ const [currentListener, setCurrentListener] = React.useState({
name: '',
protocol: 'HTTP',
port: 80,
@@ -139,12 +168,15 @@ const GatewayCreatePage: React.FC = () => {
});
};
- const gatewayModel = {
- apiGroup: 'gateway.networking.k8s.io',
- apiVersion: 'v1',
+ const gatewayGVK = getGroupVersionKindForResource({
+ apiVersion: 'gateway.networking.k8s.io/v1',
kind: 'Gateway',
- plural: 'gateways',
- };
+ });
+ const [gatewayModel] = useK8sModel({
+ group: gatewayGVK.group,
+ version: gatewayGVK.version,
+ kind: gatewayGVK.kind,
+ });
const handleListenerSave = () => {
// Remove certificates and TLS options when TLS mode is Passthrough
@@ -185,7 +217,7 @@ const GatewayCreatePage: React.FC = () => {
};
const handleAddCertificateRef = () => {
- const newRef = {
+ const newRef: CertificateRefRow = {
id: generateUniqueId('cert'),
name: '',
namespace: selectedNamespace,
@@ -197,16 +229,24 @@ const GatewayCreatePage: React.FC = () => {
});
};
- const handleCertificateRefChange = (id: number, field: string, value: string) => {
+ const handleCertificateRefChange = (
+ id: string,
+ field: 'name' | 'namespace' | 'kind',
+ value: string,
+ ) => {
setCurrentListener({
...currentListener,
certificateRefs: currentListener.certificateRefs.map((ref) =>
- ref.id === id ? { ...ref, [field]: value } : ref,
+ ref.id === id
+ ? field === 'kind'
+ ? { ...ref, kind: value as CertificateRefRow['kind'] }
+ : { ...ref, [field]: value }
+ : ref,
),
});
};
- const handleRemoveCertificateRef = (id: number) => {
+ const handleRemoveCertificateRef = (id: string) => {
setCurrentListener({
...currentListener,
certificateRefs: currentListener.certificateRefs.filter((ref) => ref.id !== id),
@@ -228,7 +268,7 @@ const GatewayCreatePage: React.FC = () => {
});
};
- const handleRouteKindChange = (id: number, field: string, value: string) => {
+ const handleRouteKindChange = (id: string, field: 'kind' | 'group', value: string) => {
setCurrentListener({
...currentListener,
allowedRoutes: {
@@ -240,7 +280,7 @@ const GatewayCreatePage: React.FC = () => {
});
};
- const handleRemoveRouteKind = (id: number) => {
+ const handleRemoveRouteKind = (id: string) => {
setCurrentListener({
...currentListener,
allowedRoutes: {
@@ -262,7 +302,7 @@ const GatewayCreatePage: React.FC = () => {
});
};
- const handleTlsOptionChange = (id: number, field: string, value: string) => {
+ const handleTlsOptionChange = (id: string, field: 'key' | 'value', value: string) => {
setCurrentListener({
...currentListener,
tlsOptions: currentListener.tlsOptions.map((option) =>
@@ -271,7 +311,7 @@ const GatewayCreatePage: React.FC = () => {
});
};
- const handleRemoveTlsOption = (id: number) => {
+ const handleRemoveTlsOption = (id: string) => {
setCurrentListener({
...currentListener,
tlsOptions: currentListener.tlsOptions.filter((option) => option.id !== id),
@@ -280,7 +320,7 @@ const GatewayCreatePage: React.FC = () => {
// Address handlers
const handleAddAddress = () => {
- const newAddress = {
+ const newAddress: AddressRow = {
id: generateUniqueId('addr'),
type: 'IPAddress',
value: '',
@@ -288,147 +328,187 @@ const GatewayCreatePage: React.FC = () => {
setAddresses([...addresses, newAddress]);
};
- const handleAddressChange = (id: number, field: string, value: string) => {
+ const handleAddressChange = (id: string, field: 'type' | 'value', value: string) => {
setAddresses(
- addresses.map((address) => (address.id === id ? { ...address, [field]: value } : address)),
+ addresses.map((address) =>
+ address.id === id
+ ? field === 'type'
+ ? { ...address, type: value as AddressRow['type'] }
+ : { ...address, value }
+ : address,
+ ),
);
};
- const handleRemoveAddress = (id: number) => {
+ const handleRemoveAddress = (id: string) => {
setAddresses(addresses.filter((address) => address.id !== id));
};
// When form completed, build gateway resource object from form data
- const gatewayObject = React.useMemo(() => {
- const gateway = {
- apiVersion: 'gateway.networking.k8s.io/v1',
- kind: 'Gateway',
- metadata: originalMetadata
- ? {
- ...originalMetadata,
- }
- : {
- name: gatewayName,
- namespace: selectedNamespace,
- },
- spec: {
- gatewayClassName: gatewayClassName,
- listeners: listeners.map((listener) => {
- const formattedListener: any = {
- name: listener.name,
- port: listener.port,
- protocol: listener.protocol,
+ const gatewayObject = React.useMemo(() => {
+ const baseSpec = {
+ gatewayClassName: gatewayClassName,
+ listeners: listeners.map((listener) => {
+ const formattedListener: {
+ name: string;
+ port: number;
+ protocol: ListenerUI['protocol'];
+ hostname?: string;
+ tls?: {
+ mode?: 'Terminate' | 'Passthrough';
+ certificateRefs?: { group?: string; kind?: string; name: string; namespace?: string }[];
+ options?: Record;
+ };
+ allowedRoutes?: {
+ namespaces?: { from?: 'All' | 'Same' | 'Selector' };
+ kinds?: { group?: string; kind: string }[];
+ };
+ } = {
+ name: listener.name,
+ port: listener.port,
+ protocol: listener.protocol,
+ };
+
+ if (listener.hostname) {
+ formattedListener.hostname = listener.hostname;
+ }
+
+ if (listener.protocol === 'HTTPS' || listener.protocol === 'TLS') {
+ formattedListener.tls = {
+ mode: listener.tlsMode,
};
- if (listener.hostname) {
- formattedListener.hostname = listener.hostname;
+ if (listener.certificateRefs && listener.certificateRefs.length > 0) {
+ formattedListener.tls.certificateRefs = listener.certificateRefs.map((ref) => ({
+ name: ref.name,
+ namespace: ref.namespace || undefined,
+ kind: ref.kind,
+ }));
}
- if (listener.protocol === 'HTTPS' || listener.protocol === 'TLS') {
- formattedListener.tls = {
- mode: listener.tlsMode,
- };
-
- if (listener.certificateRefs && listener.certificateRefs.length > 0) {
- formattedListener.tls.certificateRefs = listener.certificateRefs.map((ref) => ({
- name: ref.name,
- namespace: ref.namespace || undefined,
- kind: ref.kind,
- }));
- }
-
- if (listener.tlsOptions && listener.tlsOptions.length > 0) {
- formattedListener.tls.options = listener.tlsOptions.reduce((acc, option) => {
+ if (listener.tlsOptions && listener.tlsOptions.length > 0) {
+ formattedListener.tls.options = listener.tlsOptions.reduce>(
+ (acc, option) => {
acc[option.key] = option.value;
return acc;
- }, {});
- }
+ },
+ {},
+ );
}
+ }
- if (listener.allowedRoutes) {
- formattedListener.allowedRoutes = {
- namespaces: {
- from: listener.allowedRoutes.namespaces.from,
- },
- };
-
- if (listener.allowedRoutes.kinds && listener.allowedRoutes.kinds.length > 0) {
- formattedListener.allowedRoutes.kinds = listener.allowedRoutes.kinds.map((kind) => ({
- kind: kind.kind,
- group: kind.group,
- }));
- }
+ if (listener.allowedRoutes) {
+ formattedListener.allowedRoutes = {
+ namespaces: {
+ from: listener.allowedRoutes.namespaces.from,
+ },
+ };
+
+ if (listener.allowedRoutes.kinds && listener.allowedRoutes.kinds.length > 0) {
+ formattedListener.allowedRoutes.kinds = listener.allowedRoutes.kinds.map((kind) => ({
+ kind: kind.kind,
+ group: kind.group,
+ }));
}
+ }
- return formattedListener;
- }),
- },
- };
+ return formattedListener;
+ }),
+ ...(addresses.length > 0
+ ? {
+ addresses: addresses.map((address) => ({
+ type: address.type,
+ value: address.value,
+ })),
+ }
+ : {}),
+ } as const;
- if (addresses.length > 0) {
- (gateway.spec as any).addresses = addresses.map((address) => ({
- type: address.type,
- value: address.value,
- }));
- }
+ const gateway: GatewayResource = {
+ apiVersion: 'gateway.networking.k8s.io/v1',
+ kind: 'Gateway',
+ metadata: originalMetadata
+ ? {
+ ...originalMetadata,
+ }
+ : {
+ name: gatewayName,
+ namespace: selectedNamespace,
+ },
+ spec: baseSpec,
+ };
return gateway;
}, [gatewayName, gatewayClassName, listeners, addresses, selectedNamespace, originalMetadata]);
- const populateFormFromGateway = (gateway: any) => {
+ type ApiListener = NonNullable['listeners'][number];
+ type ApiAddress = NonNullable['addresses'] extends (infer U)[]
+ ? U
+ : never;
+
+ const populateFormFromGateway = (gateway: unknown) => {
try {
- if (gateway.metadata?.name) {
- setGatewayName(gateway.metadata.name);
+ const g = gateway as Partial;
+ if (g.metadata?.name) {
+ setGatewayName(g.metadata.name);
}
- if (gateway.spec?.gatewayClassName) {
- setGatewayClassName(gateway.spec.gatewayClassName);
+ if (g.spec?.gatewayClassName) {
+ setGatewayClassName(g.spec.gatewayClassName);
}
- if (gateway.spec?.listeners) {
- const formattedListeners = gateway.spec.listeners.map((listener: any, index: number) => ({
- name: listener.name || '',
- port: listener.port || 80,
- protocol: listener.protocol || 'HTTP',
- hostname: listener.hostname || '',
- tlsMode: listener.tls?.mode || 'Terminate',
- tlsOptions: listener.tls?.options
- ? Object.entries(listener.tls.options).map(([key, value], idx) => ({
- id: generateUniqueId(`tls_${index}_${idx}`),
- key,
- value,
- }))
- : [],
- certificateRefs: listener.tls?.certificateRefs
- ? listener.tls.certificateRefs.map((ref: any, idx: number) => ({
- id: generateUniqueId(`cert_${index}_${idx}`),
- name: ref.name || '',
- namespace: ref.namespace || '',
- kind: ref.kind || 'Secret',
- }))
- : [],
- allowedRoutes: {
- namespaces: {
- from: listener.allowedRoutes?.namespaces?.from || 'Same',
- },
- kinds: listener.allowedRoutes?.kinds
- ? listener.allowedRoutes.kinds.map((kind: any, idx: number) => ({
- id: generateUniqueId(`route_${index}_${idx}`),
- kind: kind.kind || 'HTTPRoute',
- group: kind.group || 'gateway.networking.k8s.io',
+ if (g.spec?.listeners) {
+ const formattedListeners: ListenerUI[] = g.spec.listeners.map(
+ (listener: ApiListener, index: number) => ({
+ name: listener.name || '',
+ port: listener.port || 80,
+ protocol: (listener.protocol as ListenerUI['protocol']) || 'HTTP',
+ hostname: listener.hostname || '',
+ tlsMode: (listener.tls?.mode as ListenerUI['tlsMode']) || 'Terminate',
+ tlsOptions: listener.tls?.options
+ ? (Object.entries(listener.tls.options) as Array<[string, string]>).map(
+ ([key, value], idx) => ({
+ id: generateUniqueId(`tls_${index}_${idx}`),
+ key,
+ value,
+ }),
+ )
+ : [],
+ certificateRefs: listener.tls?.certificateRefs
+ ? listener.tls.certificateRefs.map((ref, idx: number) => ({
+ id: generateUniqueId(`cert_${index}_${idx}`),
+ name: ref.name || '',
+ namespace: ref.namespace || '',
+ kind: (ref.kind as CertificateRefRow['kind']) || 'Secret',
}))
: [],
- },
- }));
+ allowedRoutes: {
+ namespaces: {
+ from:
+ (listener.allowedRoutes?.namespaces
+ ?.from as AllowedRoutesUI['namespaces']['from']) || 'Same',
+ },
+ kinds: listener.allowedRoutes?.kinds
+ ? listener.allowedRoutes.kinds.map((kind, idx: number) => ({
+ id: generateUniqueId(`route_${index}_${idx}`),
+ kind: kind.kind || 'HTTPRoute',
+ group: kind.group || 'gateway.networking.k8s.io',
+ }))
+ : [],
+ },
+ }),
+ );
setListeners(formattedListeners);
}
- if (gateway.spec?.addresses) {
- const formattedAddresses = gateway.spec.addresses.map((address: any, index: number) => ({
- id: generateUniqueId(`addr_${index}`),
- type: address.type || 'IPAddress',
- value: address.value || '',
- }));
+ if (g.spec?.addresses) {
+ const formattedAddresses: AddressRow[] = g.spec.addresses.map(
+ (address: ApiAddress, index: number) => ({
+ id: generateUniqueId(`addr_${index}`),
+ type: (address.type as AddressRow['type']) || 'IPAddress',
+ value: address.value || '',
+ }),
+ );
setAddresses(formattedAddresses);
}
} catch (error) {
@@ -473,9 +553,10 @@ const GatewayCreatePage: React.FC = () => {
if (parsedGateway && typeof parsedGateway === 'object') {
populateFormFromGateway(parsedGateway);
}
- } catch (error: any) {
+ } catch (error: unknown) {
+ const err = error as Error;
const errorMessage =
- error.message ||
+ err?.message ||
'Invalid YAML syntax. Please review the Gateway Resource YAML and try again.';
setYamlError(errorMessage);
console.warn('Invalid YAML syntax, not updating form:', error);
@@ -585,7 +666,10 @@ const GatewayCreatePage: React.FC = () => {
- setCurrentListener({ ...currentListener, protocol: value })
+ setCurrentListener({
+ ...currentListener,
+ protocol: value as ListenerUI['protocol'],
+ })
}
aria-label={t('Select Protocol')}
>
@@ -612,7 +696,10 @@ const GatewayCreatePage: React.FC = () => {
- setCurrentListener({ ...currentListener, tlsMode: value })
+ setCurrentListener({
+ ...currentListener,
+ tlsMode: value as ListenerUI['tlsMode'],
+ })
}
aria-label={t('Select TLS Mode')}
>
@@ -830,7 +917,7 @@ const GatewayCreatePage: React.FC = () => {
...currentListener.allowedRoutes,
namespaces: {
...currentListener.allowedRoutes.namespaces,
- from: value,
+ from: value as AllowedRoutesUI['namespaces']['from'],
},
},
})
@@ -1050,7 +1137,7 @@ const GatewayCreatePage: React.FC = () => {
{createView === 'form' ? (
+ }
+ aria-label={t('Parent references help')}
+ >
+
+
+
+ }
+ fieldId="parent-references"
+ >
+ {!hasValidParentRef && (
+
+ )}
+
+ {parentRefs.map((parentRef, index) => {
+ const descriptionParts: string[] = [];
+ if (parentRef.gatewayNamespace)
+ descriptionParts.push(`${t('Namespace')}: ${parentRef.gatewayNamespace}`);
+ if (parentRef.sectionName)
+ descriptionParts.push(`${t('Section')}: ${parentRef.sectionName}`);
+ if (parentRef.port && parentRef.port > 0)
+ descriptionParts.push(`${t('Port')}: ${parentRef.port}`);
+ const description = descriptionParts.length > 0 ? descriptionParts.join(' | ') : undefined;
+
+ return (
+ removeParentReference(parentRef.id)}
+ aria-label={t('Remove parent reference')}
+ icon={}
+ />
+ )
+ }
+ />
+ }
+ style={{
+ marginBottom: '16px',
+ }}
+ >
+
+ {/* Gateway selection */}
+
+
+
+ updateParentReference(parentRef.id, 'gatewayName', value)
+ }
+ aria-label={t('Select Gateway')}
+ isDisabled={isDisabled}
+ >
+
+ {getSortedGateways().map((gateway) => {
+ const restriction = validateGateway(gateway);
+
+ return (
+
+ );
+ })}
+
+
+
+ {t('The name of the Gateway to attach to.')}
+
+
+
+
+
+
+
+
+ {t('The namespace of the Gateway.')}
+
+
+
+
+
+ {/* Section and Port */}
+
+
+
+ updateParentReference(parentRef.id, 'sectionName', value)
+ }
+ aria-label={t('Select Section')}
+ isDisabled={isDisabled || !parentRef.gatewayName}
+ >
+
+ {getSortedSections(parentRef.gatewayName, parentRef.gatewayNamespace).map(
+ (listener) => {
+ const gateway = availableGateways.find(
+ (gw) =>
+ gw.metadata.name === parentRef.gatewayName &&
+ gw.metadata.namespace === parentRef.gatewayNamespace,
+ );
+ const restriction = gateway
+ ? validateListener(gateway, listener.name)
+ : null;
+
+ return (
+
+ );
+ },
+ )}
+
+
+
+
+
+
+
+
+
+ );
+ })}
+
+ {/* Add button */}
+ {!isDisabled && (
+ }
+ onClick={addParentReference}
+ isInline
+ isDisabled={
+ parentRefs.length > 0 &&
+ (!parentRefs[parentRefs.length - 1]?.gatewayName ||
+ !parentRefs[parentRefs.length - 1]?.sectionName)
+ }
+ >
+ {t('Add parent reference')}
+
+ )}
+
+ );
+};
+
+export default ParentReferencesSelect;
diff --git a/src/utils/gatewayCreateEditHelpers.ts b/src/utils/gatewayCreateEditHelpers.ts
index 2b58a76..8ac34b0 100644
--- a/src/utils/gatewayCreateEditHelpers.ts
+++ b/src/utils/gatewayCreateEditHelpers.ts
@@ -6,7 +6,12 @@ export const generateUniqueId = (prefix = 'item'): string => {
};
// Remove certificates and TLS options when TLS mode is Passthrough
-export const removeCertsAndTlsOptionsForPassthrough = (listener: any) => {
+export const removeCertsAndTlsOptionsForPassthrough = (listener: {
+ protocol: 'HTTP' | 'HTTPS' | 'TLS' | 'TCP' | 'UDP';
+ tlsMode: 'Terminate' | 'Passthrough';
+ tlsOptions?: Array;
+ certificateRefs?: Array;
+}) => {
if (listener.protocol === 'HTTPS' || listener.protocol === 'TLS') {
if (listener.tlsMode === 'Passthrough') {
return {