diff --git a/api/v1/prefectserver_types.go b/api/v1/prefectserver_types.go index 009b6d3..0d02feb 100644 --- a/api/v1/prefectserver_types.go +++ b/api/v1/prefectserver_types.go @@ -42,6 +42,9 @@ type PrefectServerSpec struct { // ExtraContainers defines additional containers to add to the Prefect Server Deployment ExtraContainers []corev1.Container `json:"extraContainers,omitempty"` + // ExtraServicePorts defines additional ports to expose on the Prefect Server Service + ExtraServicePorts []corev1.ServicePort `json:"extraServicePorts,omitempty"` + // Ephemeral defines whether the server will be deployed with an ephemeral storage backend Ephemeral *EphemeralConfiguration `json:"ephemeral,omitempty"` diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 4bd4d5b..c450cd2 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -246,6 +246,13 @@ func (in *PrefectServerSpec) DeepCopyInto(out *PrefectServerSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.ExtraServicePorts != nil { + in, out := &in.ExtraServicePorts, &out.ExtraServicePorts + *out = make([]corev1.ServicePort, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Ephemeral != nil { in, out := &in.Ephemeral, &out.Ephemeral *out = new(EphemeralConfiguration) diff --git a/deploy/charts/prefect-operator/crds/prefect.io_prefectservers.yaml b/deploy/charts/prefect-operator/crds/prefect.io_prefectservers.yaml index 2eb5e0e..720e11d 100644 --- a/deploy/charts/prefect-operator/crds/prefect.io_prefectservers.yaml +++ b/deploy/charts/prefect-operator/crds/prefect.io_prefectservers.yaml @@ -1394,6 +1394,82 @@ spec: - name type: object type: array + extraServicePorts: + description: ExtraServicePorts defines additional ports to expose + on the Prefect Server Service + items: + description: ServicePort contains information on service's port. + properties: + appProtocol: + description: |- + The application protocol for this port. + This is used as a hint for implementations to offer richer behavior for protocols that they understand. + This field follows standard Kubernetes label syntax. + Valid values are either: + + + * Un-prefixed protocol names - reserved for IANA standard service names (as per + RFC-6335 and https://www.iana.org/assignments/service-names). + + + * Kubernetes-defined prefixed names: + * 'kubernetes.io/h2c' - HTTP/2 prior knowledge over cleartext as described in https://www.rfc-editor.org/rfc/rfc9113.html#name-starting-http-2-with-prior- + * 'kubernetes.io/ws' - WebSocket over cleartext as described in https://www.rfc-editor.org/rfc/rfc6455 + * 'kubernetes.io/wss' - WebSocket over TLS as described in https://www.rfc-editor.org/rfc/rfc6455 + + + * Other protocols should use implementation-defined prefixed names such as + mycompany.com/my-custom-protocol. + type: string + name: + description: |- + The name of this port within the service. This must be a DNS_LABEL. + All ports within a ServiceSpec must have unique names. When considering + the endpoints for a Service, this must match the 'name' field in the + EndpointPort. + Optional if only one ServicePort is defined on this service. + type: string + nodePort: + description: |- + The port on each node on which this service is exposed when type is + NodePort or LoadBalancer. Usually assigned by the system. If a value is + specified, in-range, and not in use it will be used, otherwise the + operation will fail. If not specified, a port will be allocated if this + Service requires one. If this field is specified when creating a + Service which does not need it, creation will fail. This field will be + wiped when updating a Service to no longer need it (e.g. changing type + from NodePort to ClusterIP). + More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport + format: int32 + type: integer + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + default: TCP + description: |- + The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". + Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the pods targeted by the service. + Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named port in the + target Pod's container ports. If this is not specified, the value + of the 'port' field is used (an identity map). + This field is ignored for services with clusterIP=None, and should be + omitted or set equal to the 'port' field. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array image: description: Image defines the exact image to deploy for the Prefect Server, overriding Version diff --git a/deploy/charts/prefect-operator/crds/prefect.io_prefectworkpools.yaml b/deploy/charts/prefect-operator/crds/prefect.io_prefectworkpools.yaml index 86044e3..aa45b48 100644 --- a/deploy/charts/prefect-operator/crds/prefect.io_prefectworkpools.yaml +++ b/deploy/charts/prefect-operator/crds/prefect.io_prefectworkpools.yaml @@ -68,7 +68,7 @@ spec: type: object extraContainers: description: ExtraContainers defines additional containers to add - to the Prefect Server Deployment + to each worker in the Work Pool items: description: A single application container that you want to run within a pod. diff --git a/internal/controller/prefectserver_controller.go b/internal/controller/prefectserver_controller.go index 5551a3f..08fae57 100644 --- a/internal/controller/prefectserver_controller.go +++ b/internal/controller/prefectserver_controller.go @@ -585,6 +585,10 @@ func (r *PrefectServerReconciler) prefectServerService(server *prefectiov1.Prefe }, }, } + // Append any extra ports into the Service if configured. + if server.Spec.ExtraServicePorts != nil { + service.Spec.Ports = append(service.Spec.Ports, server.Spec.ExtraServicePorts...) + } // TODO: handle errors from SetControllerReference. _ = ctrl.SetControllerReference(server, &service, r.Scheme) diff --git a/internal/controller/prefectserver_controller_test.go b/internal/controller/prefectserver_controller_test.go index bdf1c29..79fd5cf 100644 --- a/internal/controller/prefectserver_controller_test.go +++ b/internal/controller/prefectserver_controller_test.go @@ -550,6 +550,43 @@ var _ = Describe("PrefectServer controller", func() { container := deployment.Spec.Template.Spec.Containers[1] Expect(container.Name).To(Equal("extra-container")) }) + + It("should update the Service with the extra port", func() { + // Update the PrefectServer with an extra port + Expect(k8sClient.Get(ctx, name, prefectserver)).To(Succeed()) + prefectserver.Spec.ExtraServicePorts = []corev1.ServicePort{ + { + Name: "extra-port", + Port: 4300, + TargetPort: intstr.FromString("extra-port"), + Protocol: corev1.ProtocolTCP, + }, + } + + Expect(k8sClient.Update(ctx, prefectserver)).To(Succeed()) + // Reconcile to apply the changes + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).NotTo(HaveOccurred()) + + // Check if the Service was updated with the extra port + service := &corev1.Service{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: prefectserver.Name, + }, service) + }).Should(Succeed()) + + Expect(service.Spec.Ports).To(HaveLen(2)) + port := service.Spec.Ports[1] + Expect(port.Name).To(Equal("extra-port")) + }) }) Context("When evaluating changes with any server", func() {