Skip to content

Commit e702151

Browse files
mitchnielsenclaude
andcommitted
Add IPv6 support to PrefectServer
The operator was hardcoding `--host 0.0.0.0` when starting Prefect servers, which prevented them from running in IPv6-only Kubernetes clusters. This adds an optional `host` field to the PrefectServerSpec that lets users configure the bind address. Changes: - Added `host` field to PrefectServerSpec (defaults to "0.0.0.0" for backward compatibility) - Fixed typo: EntrypointArugments → EntrypointArguments (kept deprecated wrapper) - Users can now set `host: "::"` for IPv6-only or `host: ""` for dual-stack environments - Added tests and sample configurations for different scenarios - Fixed Makefile generate target to work with macOS BSD sed The implementation follows the pattern used elsewhere in the codebase (like Postgres host configuration) and maintains full backward compatibility. The Prefect Helm chart handles this correctly, and now the operator does too. Closes #224 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
1 parent 89720fd commit e702151

File tree

10 files changed

+200
-7
lines changed

10 files changed

+200
-7
lines changed

.mise.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ actionlint = "1.7.9"
33
ginkgo = '2.25.2'
44
golang = '1.24'
55
golangci-lint = "2.6.2"
6-
"go:golang.org/x/tools/cmd/goimports" = "0.39.0"
6+
# "go:golang.org/x/tools/cmd/goimports" = "0.39.0"
77
'go:fybrik.io/crdoc' = 'latest'
88
helm = "3.19"
99
helm-ct = "3.14.0"

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,13 @@ manifests: tools ## Generate WebhookConfiguration, ClusterRole and CustomResourc
8585
generate: tools ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
8686
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
8787
@echo "Adding coverage ignore directive to generated files..."
88-
@sed -i '1a\\n//go:coverage ignore' api/v1/zz_generated.deepcopy.go
88+
@grep -q "go:coverage ignore" api/v1/zz_generated.deepcopy.go || \
89+
(head -1 api/v1/zz_generated.deepcopy.go > /tmp/zz_temp && \
90+
echo "" >> /tmp/zz_temp && \
91+
echo "//go:coverage ignore" >> /tmp/zz_temp && \
92+
echo "" >> /tmp/zz_temp && \
93+
tail -n +2 api/v1/zz_generated.deepcopy.go >> /tmp/zz_temp && \
94+
mv /tmp/zz_temp api/v1/zz_generated.deepcopy.go)
8995

9096
.PHONY: fmt
9197
fmt: ## Run go fmt against code.

api/v1/prefectserver_types.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ type PrefectServerSpec struct {
3636
// Image defines the exact image to deploy for the Prefect Server, overriding Version
3737
Image *string `json:"image,omitempty"`
3838

39+
// Host defines the host address to bind the Prefect Server to.
40+
// Defaults to "0.0.0.0" for IPv4 compatibility.
41+
// Use "::" for IPv6-only environments, or "" for dual-stack.
42+
// +kubebuilder:validation:Optional
43+
Host *string `json:"host,omitempty"`
44+
3945
// Resources defines the CPU and memory resources for each replica of the Prefect Server
4046
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
4147

@@ -394,13 +400,23 @@ func (s *PrefectServer) Image() string {
394400
return DEFAULT_PREFECT_IMAGE
395401
}
396402

397-
func (s *PrefectServer) EntrypointArugments() []string {
398-
command := []string{"prefect", "server", "start", "--host", "0.0.0.0"}
403+
func (s *PrefectServer) EntrypointArguments() []string {
404+
host := "0.0.0.0" // Default to IPv4 for backward compatibility
405+
if s.Spec.Host != nil {
406+
host = *s.Spec.Host
407+
}
408+
409+
command := []string{"prefect", "server", "start", "--host", host}
399410
command = append(command, s.Spec.ExtraArgs...)
400411

401412
return command
402413
}
403414

415+
// Deprecated: EntrypointArugments is misspelled, use EntrypointArguments instead
416+
func (s *PrefectServer) EntrypointArugments() []string {
417+
return s.EntrypointArguments()
418+
}
419+
404420
func (s *PrefectServer) ToEnvVars() []corev1.EnvVar {
405421
envVars := []corev1.EnvVar{
406422
{

api/v1/prefectserver_types_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,5 +620,73 @@ var _ = Describe("PrefectServer type", func() {
620620

621621
Expect(envVars).To(ConsistOf(expectedEnvVars))
622622
})
623+
624+
Context("Host binding configuration", func() {
625+
It("should use default host 0.0.0.0 when Host is nil", func() {
626+
server := &PrefectServer{
627+
Spec: PrefectServerSpec{},
628+
}
629+
630+
args := server.EntrypointArguments()
631+
Expect(args).To(Equal([]string{"prefect", "server", "start", "--host", "0.0.0.0"}))
632+
})
633+
634+
It("should use custom IPv6 host when specified", func() {
635+
server := &PrefectServer{
636+
Spec: PrefectServerSpec{
637+
Host: ptr.To("::"),
638+
},
639+
}
640+
641+
args := server.EntrypointArguments()
642+
Expect(args).To(Equal([]string{"prefect", "server", "start", "--host", "::"}))
643+
})
644+
645+
It("should support empty string for dual-stack", func() {
646+
server := &PrefectServer{
647+
Spec: PrefectServerSpec{
648+
Host: ptr.To(""),
649+
},
650+
}
651+
652+
args := server.EntrypointArguments()
653+
Expect(args).To(Equal([]string{"prefect", "server", "start", "--host", ""}))
654+
})
655+
656+
It("should use custom host with ExtraArgs", func() {
657+
server := &PrefectServer{
658+
Spec: PrefectServerSpec{
659+
Host: ptr.To("::"),
660+
ExtraArgs: []string{"--some-arg", "some-value"},
661+
},
662+
}
663+
664+
args := server.EntrypointArguments()
665+
Expect(args).To(Equal([]string{"prefect", "server", "start", "--host", "::", "--some-arg", "some-value"}))
666+
})
667+
668+
It("should use specific IPv4 address when specified", func() {
669+
server := &PrefectServer{
670+
Spec: PrefectServerSpec{
671+
Host: ptr.To("127.0.0.1"),
672+
},
673+
}
674+
675+
args := server.EntrypointArguments()
676+
Expect(args).To(Equal([]string{"prefect", "server", "start", "--host", "127.0.0.1"}))
677+
})
678+
679+
It("should maintain backward compatibility with deprecated EntrypointArugments method", func() {
680+
server := &PrefectServer{
681+
Spec: PrefectServerSpec{
682+
Host: ptr.To("::"),
683+
},
684+
}
685+
686+
// Test that the deprecated method still works
687+
args := server.EntrypointArugments()
688+
Expect(args).To(Equal([]string{"prefect", "server", "start", "--host", "::"}))
689+
})
690+
})
623691
})
624692
})

api/v1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deploy/charts/prefect-operator/crds/prefect.io_prefectservers.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1661,6 +1661,12 @@ spec:
16611661
- port
16621662
type: object
16631663
type: array
1664+
host:
1665+
description: |-
1666+
Host defines the host address to bind the Prefect Server to.
1667+
Defaults to "0.0.0.0" for IPv4 compatibility.
1668+
Use "::" for IPv6-only environments, or "" for dual-stack.
1669+
type: string
16641670
image:
16651671
description: Image defines the exact image to deploy for the Prefect
16661672
Server, overriding Version
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: prefect.io/v1
2+
kind: PrefectServer
3+
metadata:
4+
name: prefect-dualstack
5+
labels:
6+
app.kubernetes.io/name: prefect-operator
7+
app.kubernetes.io/managed-by: kustomize
8+
spec:
9+
host: "" # Empty string for dual-stack (uvicorn binds to all interfaces)
10+
sqlite:
11+
storageClassName: standard
12+
size: 1Gi
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
apiVersion: prefect.io/v1
2+
kind: PrefectServer
3+
metadata:
4+
name: prefect-ipv6
5+
labels:
6+
app.kubernetes.io/name: prefect-operator
7+
app.kubernetes.io/managed-by: kustomize
8+
spec:
9+
host: "::" # IPv6-only binding
10+
ephemeral: {}

internal/controller/prefectserver_controller.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ func (r *PrefectServerReconciler) ephemeralDeploymentSpec(server *prefectiov1.Pr
374374
Image: server.Image(),
375375
ImagePullPolicy: corev1.PullIfNotPresent,
376376

377-
Args: server.EntrypointArugments(),
377+
Args: server.EntrypointArguments(),
378378
VolumeMounts: []corev1.VolumeMount{
379379
{
380380
Name: "prefect-data",
@@ -458,7 +458,7 @@ func (r *PrefectServerReconciler) sqliteDeploymentSpec(server *prefectiov1.Prefe
458458
Image: server.Image(),
459459
ImagePullPolicy: corev1.PullIfNotPresent,
460460

461-
Args: server.EntrypointArugments(),
461+
Args: server.EntrypointArguments(),
462462
VolumeMounts: []corev1.VolumeMount{
463463
{
464464
Name: "prefect-data",
@@ -514,7 +514,7 @@ func (r *PrefectServerReconciler) postgresDeploymentSpec(server *prefectiov1.Pre
514514
Image: server.Image(),
515515
ImagePullPolicy: corev1.PullIfNotPresent,
516516

517-
Args: server.EntrypointArugments(),
517+
Args: server.EntrypointArguments(),
518518
Env: server.ToEnvVars(),
519519
Ports: []corev1.ContainerPort{
520520
{

internal/controller/prefectserver_controller_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,76 @@ var _ = Describe("PrefectServer controller", func() {
644644
Expect(container.Command).To(BeNil())
645645
Expect(container.Args).To(Equal([]string{"prefect", "server", "start", "--host", "0.0.0.0", "--some-arg", "some-value"}))
646646
})
647+
648+
It("should create a Deployment with custom IPv6 host", func() {
649+
name := types.NamespacedName{
650+
Namespace: namespaceName,
651+
Name: "prefect-ipv6-server",
652+
}
653+
654+
prefectserver := &prefectiov1.PrefectServer{
655+
ObjectMeta: metav1.ObjectMeta{
656+
Namespace: namespaceName,
657+
Name: "prefect-ipv6-server",
658+
},
659+
Spec: prefectiov1.PrefectServerSpec{
660+
Host: ptr.To("::"),
661+
},
662+
}
663+
Expect(k8sClient.Create(ctx, prefectserver)).To(Succeed())
664+
665+
controllerReconciler := &PrefectServerReconciler{
666+
Client: k8sClient,
667+
Scheme: k8sClient.Scheme(),
668+
}
669+
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
670+
NamespacedName: name,
671+
})
672+
Expect(err).NotTo(HaveOccurred())
673+
674+
deployment := &appsv1.Deployment{}
675+
Eventually(func() error {
676+
return k8sClient.Get(ctx, name, deployment)
677+
}).Should(Succeed())
678+
679+
container := deployment.Spec.Template.Spec.Containers[0]
680+
Expect(container.Args).To(Equal([]string{"prefect", "server", "start", "--host", "::"}))
681+
})
682+
683+
It("should create a Deployment with dual-stack host (empty string)", func() {
684+
name := types.NamespacedName{
685+
Namespace: namespaceName,
686+
Name: "prefect-dualstack-server",
687+
}
688+
689+
prefectserver := &prefectiov1.PrefectServer{
690+
ObjectMeta: metav1.ObjectMeta{
691+
Namespace: namespaceName,
692+
Name: "prefect-dualstack-server",
693+
},
694+
Spec: prefectiov1.PrefectServerSpec{
695+
Host: ptr.To(""),
696+
},
697+
}
698+
Expect(k8sClient.Create(ctx, prefectserver)).To(Succeed())
699+
700+
controllerReconciler := &PrefectServerReconciler{
701+
Client: k8sClient,
702+
Scheme: k8sClient.Scheme(),
703+
}
704+
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
705+
NamespacedName: name,
706+
})
707+
Expect(err).NotTo(HaveOccurred())
708+
709+
deployment := &appsv1.Deployment{}
710+
Eventually(func() error {
711+
return k8sClient.Get(ctx, name, deployment)
712+
}).Should(Succeed())
713+
714+
container := deployment.Spec.Template.Spec.Containers[0]
715+
Expect(container.Args).To(Equal([]string{"prefect", "server", "start", "--host", ""}))
716+
})
647717
})
648718

649719
Context("When evaluating changes with any server", func() {

0 commit comments

Comments
 (0)