From adcbddc2d544feba114d5efc00c631fa3aad098e Mon Sep 17 00:00:00 2001 From: Chris Guidry Date: Mon, 12 Aug 2024 16:10:41 -0500 Subject: [PATCH] Adds the test suite for the PrefectServer controller (#24) --- Makefile | 8 +- api/v1/prefectserver_types.go | 4 +- go.mod | 24 +- go.sum | 50 +- .../controller/prefectserver_controller.go | 150 ++- .../prefectserver_controller_test.go | 1165 ++++++++++++++++- test/e2e/e2e_test.go | 2 +- 7 files changed, 1282 insertions(+), 121 deletions(-) diff --git a/Makefile b/Makefile index 5ba1de5..2ad1621 100644 --- a/Makefile +++ b/Makefile @@ -110,9 +110,15 @@ fmt: ## Run go fmt against code. vet: ## Run go vet against code. go vet ./... +GINKGO_OPTIONS ?= -v --skip-package test/e2e -coverprofile cover.out -coverpkg ./api/v1/,./internal/controller/ -r + .PHONY: test test: manifests generate fmt vet envtest ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" ginkgo run $(GINKGO_OPTIONS) + +.PHONY: watch +watch: manifests generate fmt vet envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" ginkgo watch $(GINKGO_OPTIONS) # Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. .PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up. diff --git a/api/v1/prefectserver_types.go b/api/v1/prefectserver_types.go index bb109cc..fe62198 100644 --- a/api/v1/prefectserver_types.go +++ b/api/v1/prefectserver_types.go @@ -230,8 +230,8 @@ func (s *PrefectServer) Image() string { if s.Spec.Image != nil && *s.Spec.Image != "" { return *s.Spec.Image } - if s.Spec.Version != nil && *s.Spec.Version == "" { - return "prefecthq/prefect:" + *s.Spec.Version + "-3.0.0rc15-python3.12" + if s.Spec.Version != nil && *s.Spec.Version != "" { + return "prefecthq/prefect:" + *s.Spec.Version + "-python3.12" } return DEFAULT_PREFECT_IMAGE } diff --git a/go.mod b/go.mod index b875a1d..305594a 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,9 @@ module github.com/PrefectHQ/prefect-operator go 1.21 require ( - github.com/onsi/ginkgo/v2 v2.14.0 - github.com/onsi/gomega v1.30.0 + github.com/onsi/ginkgo/v2 v2.19.0 + github.com/onsi/gomega v1.34.1 + k8s.io/api v0.29.2 k8s.io/apimachinery v0.29.2 k8s.io/client-go v0.29.2 sigs.k8s.io/controller-runtime v0.17.3 @@ -22,14 +23,14 @@ require ( github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect + github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect github.com/google/uuid v1.3.0 // indirect github.com/imdario/mergo v0.3.6 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -47,21 +48,20 @@ require ( github.com/spf13/pflag v1.0.5 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/net v0.19.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.12.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.16.1 // indirect + golang.org/x/tools v0.23.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.29.2 // indirect k8s.io/apiextensions-apiserver v0.29.2 // indirect k8s.io/component-base v0.29.2 // indirect k8s.io/klog/v2 v2.110.1 // indirect diff --git a/go.sum b/go.sum index 9b3607f..78e427b 100644 --- a/go.sum +++ b/go.sum @@ -2,9 +2,6 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -28,8 +25,8 @@ github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2Kv github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -47,11 +44,10 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -78,10 +74,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= -github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= -github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= -github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -102,7 +98,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -119,8 +114,8 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -128,8 +123,8 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -137,25 +132,24 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -166,8 +160,8 @@ google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/controller/prefectserver_controller.go b/internal/controller/prefectserver_controller.go index 6081946..bc74cfd 100644 --- a/internal/controller/prefectserver_controller.go +++ b/internal/controller/prefectserver_controller.go @@ -20,6 +20,7 @@ import ( "context" appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -28,7 +29,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" prefectiov1 "github.com/PrefectHQ/prefect-operator/api/v1" ) @@ -50,15 +51,10 @@ type PrefectServerReconciler struct { // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // -// TODO(user): Modify the Reconcile function to compare the state specified by -// the PrefectServer object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.3/pkg/reconcile func (r *PrefectServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) + log := ctrllog.FromContext(ctx) server := &prefectiov1.PrefectServer{} err := r.Get(ctx, req.NamespacedName, server) @@ -68,40 +64,65 @@ func (r *PrefectServerReconciler) Reconcile(ctx context.Context, req ctrl.Reques return ctrl.Result{}, err } - desiredDeployment, desiredPVC := r.prefectServerDeployment(server) + desiredDeployment, desiredPVC, desiredMigrationJob := r.prefectServerDeployment(server) desiredService := r.prefectServerService(server) + serverNamespacedName := types.NamespacedName{ + Namespace: server.Namespace, + Name: server.Name, + } + // Reconcile the PVC, if one is required if desiredPVC != nil { foundPVC := &corev1.PersistentVolumeClaim{} - err = r.Get(ctx, types.NamespacedName{Name: desiredPVC.Name, Namespace: server.Namespace}, foundPVC) + err = r.Get(ctx, types.NamespacedName{Namespace: server.Namespace, Name: desiredPVC.Name}, foundPVC) if errors.IsNotFound(err) { + log.Info("Creating PersistentVolumeClaim", "name", desiredPVC.Name) if err = r.Create(ctx, desiredPVC); err != nil { return ctrl.Result{}, err } - return ctrl.Result{Requeue: true}, nil } else if err != nil { return ctrl.Result{}, err } else if !metav1.IsControlledBy(foundPVC, server) { - return ctrl.Result{}, errors.NewBadRequest("PVC already exists and is not controlled by PrefectServer") + return ctrl.Result{}, errors.NewBadRequest("PersistentVolumeClaim already exists and is not controlled by PrefectServer " + server.Name) + } else { + // TODO: handle patching the PVC if there are meaningful updates that we can make, + // specifically the size request for a dynamically-provisioned PVC + } + } + + // Reconcile the migration job, if one is required + if desiredMigrationJob != nil { + foundMigrationJob := &batchv1.Job{} + err = r.Get(ctx, types.NamespacedName{Namespace: server.Namespace, Name: desiredMigrationJob.Name}, foundMigrationJob) + if errors.IsNotFound(err) { + log.Info("Creating migration Job", "name", desiredMigrationJob.Name) + if err = r.Create(ctx, desiredMigrationJob); err != nil { + return ctrl.Result{}, err + } + } else if err != nil { + return ctrl.Result{}, err + } else if !metav1.IsControlledBy(foundMigrationJob, server) { + return ctrl.Result{}, errors.NewBadRequest("Job already exists and is not controlled by PrefectServer " + server.Name) } else { - // TODO: handle patching the PVC if there are meaningful updates + // TODO: handle replacing the job } } // Reconcile the Deployment foundDeployment := &appsv1.Deployment{} - err = r.Get(ctx, types.NamespacedName{Name: server.Name, Namespace: server.Namespace}, foundDeployment) + err = r.Get(ctx, serverNamespacedName, foundDeployment) if errors.IsNotFound(err) { + log.Info("Creating Deployment", "name", desiredDeployment.Name) if err = r.Create(ctx, &desiredDeployment); err != nil { return ctrl.Result{}, err } - return ctrl.Result{Requeue: true}, nil } else if err != nil { return ctrl.Result{}, err } else if !metav1.IsControlledBy(foundDeployment, server) { - return ctrl.Result{}, errors.NewBadRequest("Deployment already exists and is not controlled by PrefectServer") + return ctrl.Result{}, errors.NewBadRequest("Deployment already exists and is not controlled by PrefectServer " + server.Name) } else { + log.Info("Updating Deployment", "name", desiredDeployment.Name) if err = r.Update(ctx, &desiredDeployment); err != nil { return ctrl.Result{}, err } @@ -109,17 +130,18 @@ func (r *PrefectServerReconciler) Reconcile(ctx context.Context, req ctrl.Reques // Reconcile the Service foundService := &corev1.Service{} - err = r.Get(ctx, types.NamespacedName{Name: server.Name, Namespace: server.Namespace}, foundService) + err = r.Get(ctx, serverNamespacedName, foundService) if errors.IsNotFound(err) { + log.Info("Creating Service", "name", desiredService.Name) if err = r.Create(ctx, &desiredService); err != nil { return ctrl.Result{}, err } - return ctrl.Result{Requeue: true}, nil } else if err != nil { return ctrl.Result{}, err } else if !metav1.IsControlledBy(foundService, server) { - return ctrl.Result{}, errors.NewBadRequest("Service already exists and is not controlled by PrefectServer") + return ctrl.Result{}, errors.NewBadRequest("Service already exists and is not controlled by PrefectServer " + server.Name) } else { + log.Info("Updating Service", "name", desiredService.Name) if err = r.Update(ctx, &desiredService); err != nil { return ctrl.Result{}, err } @@ -128,31 +150,16 @@ func (r *PrefectServerReconciler) Reconcile(ctx context.Context, req ctrl.Reques return ctrl.Result{}, nil } -func (r *PrefectServerReconciler) prefectServerDeployment(server *prefectiov1.PrefectServer) (appsv1.Deployment, *corev1.PersistentVolumeClaim) { +func (r *PrefectServerReconciler) prefectServerDeployment(server *prefectiov1.PrefectServer) (appsv1.Deployment, *corev1.PersistentVolumeClaim, *batchv1.Job) { var pvc *corev1.PersistentVolumeClaim + var migrationJob *batchv1.Job var deploymentSpec appsv1.DeploymentSpec if server.Spec.SQLite != nil { - pvc = &corev1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: server.Namespace, - Name: server.Name + "-data", - }, - Spec: corev1.PersistentVolumeClaimSpec{ - StorageClassName: &server.Spec.SQLite.StorageClassName, - AccessModes: []corev1.PersistentVolumeAccessMode{ - corev1.ReadWriteOnce, - }, - Resources: corev1.VolumeResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: server.Spec.SQLite.Size, - }, - }, - }, - } - + pvc = r.sqlitePersistentVolumeClaim(server) deploymentSpec = r.sqliteDeploymentSpec(server, pvc) } else if server.Spec.Postgres != nil { + migrationJob = r.postgresMigrationJob(server) deploymentSpec = r.postgresDeploymentSpec(server) } else { if server.Spec.Ephemeral == nil { @@ -174,7 +181,10 @@ func (r *PrefectServerReconciler) prefectServerDeployment(server *prefectiov1.Pr if pvc != nil { ctrl.SetControllerReference(server, pvc, r.Scheme) } - return *dep, pvc + if migrationJob != nil { + ctrl.SetControllerReference(server, migrationJob, r.Scheme) + } + return *dep, pvc, migrationJob } func (r *PrefectServerReconciler) ephemeralDeploymentSpec(server *prefectiov1.PrefectServer) appsv1.DeploymentSpec { @@ -200,7 +210,7 @@ func (r *PrefectServerReconciler) ephemeralDeploymentSpec(server *prefectiov1.Pr }, Containers: []corev1.Container{ { - Name: server.Name, + Name: "prefect-server", Image: server.Image(), Command: server.Command(), VolumeMounts: []corev1.VolumeMount{ @@ -231,6 +241,26 @@ func (r *PrefectServerReconciler) ephemeralDeploymentSpec(server *prefectiov1.Pr } } +func (r *PrefectServerReconciler) sqlitePersistentVolumeClaim(server *prefectiov1.PrefectServer) *corev1.PersistentVolumeClaim { + return &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: server.Namespace, + Name: server.Name + "-data", + }, + Spec: corev1.PersistentVolumeClaimSpec{ + StorageClassName: &server.Spec.SQLite.StorageClassName, + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: server.Spec.SQLite.Size, + }, + }, + }, + } +} + func (r *PrefectServerReconciler) sqliteDeploymentSpec(server *prefectiov1.PrefectServer, pvc *corev1.PersistentVolumeClaim) appsv1.DeploymentSpec { return appsv1.DeploymentSpec{ Strategy: appsv1.DeploymentStrategy{ @@ -246,7 +276,7 @@ func (r *PrefectServerReconciler) sqliteDeploymentSpec(server *prefectiov1.Prefe Spec: corev1.PodSpec{ Volumes: []corev1.Volume{ { - Name: pvc.Name, + Name: "prefect-data", VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ ClaimName: pvc.Name, @@ -256,12 +286,12 @@ func (r *PrefectServerReconciler) sqliteDeploymentSpec(server *prefectiov1.Prefe }, Containers: []corev1.Container{ { - Name: server.Name, + Name: "prefect-server", Image: server.Image(), Command: server.Command(), VolumeMounts: []corev1.VolumeMount{ { - Name: pvc.Name, + Name: "prefect-data", MountPath: "/var/lib/prefect/", }, }, @@ -302,7 +332,7 @@ func (r *PrefectServerReconciler) postgresDeploymentSpec(server *prefectiov1.Pre Spec: corev1.PodSpec{ Containers: []corev1.Container{ { - Name: server.Name, + Name: "prefect-server", Image: server.Image(), Command: server.Command(), Env: append(append([]corev1.EnvVar{ @@ -327,11 +357,43 @@ func (r *PrefectServerReconciler) postgresDeploymentSpec(server *prefectiov1.Pre } } +func (r *PrefectServerReconciler) postgresMigrationJob(server *prefectiov1.PrefectServer) *batchv1.Job { + return &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: server.Namespace, + Name: server.Name + "-migration", + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: server.ServerLabels(), + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "prefect-server-migration", + Image: server.Image(), + Command: []string{"prefect", "server", "database", "migrate", "--yes"}, + Env: append(append([]corev1.EnvVar{ + { + Name: "PREFECT_HOME", + Value: "/var/lib/prefect/", + }, + }, server.Spec.Postgres.ToEnvVars()...), server.Spec.Settings...), + }, + }, + RestartPolicy: corev1.RestartPolicyOnFailure, + }, + }, + }, + } +} + func (r *PrefectServerReconciler) prefectServerService(server *prefectiov1.PrefectServer) corev1.Service { service := corev1.Service{ ObjectMeta: metav1.ObjectMeta{ - Name: server.Name, Namespace: server.Namespace, + Name: server.Name, }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ diff --git a/internal/controller/prefectserver_controller_test.go b/internal/controller/prefectserver_controller_test.go index 1414f91..92fa8b5 100644 --- a/internal/controller/prefectserver_controller_test.go +++ b/internal/controller/prefectserver_controller_test.go @@ -18,67 +18,1166 @@ package controller import ( "context" + "fmt" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" prefectiov1 "github.com/PrefectHQ/prefect-operator/api/v1" ) -var _ = Describe("PrefectServer Controller", func() { - Context("When reconciling a resource", func() { - const resourceName = "test-resource" +var _ = Describe("PrefectServer controller", func() { + var ( + ctx context.Context + namespace *corev1.Namespace + namespaceName string + name types.NamespacedName + prefectserver *prefectiov1.PrefectServer + ) - ctx := context.Background() + Context("for any server", func() { + BeforeEach(func() { + ctx = context.Background() + namespaceName = fmt.Sprintf("any-ns-%d", time.Now().UnixNano()) - typeNamespacedName := types.NamespacedName{ - Name: resourceName, - Namespace: "default", // TODO(user):Modify as needed - } - prefectserver := &prefectiov1.PrefectServer{} + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: namespaceName}, + } + Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) + }) - BeforeEach(func() { - By("creating the custom resource for the Kind PrefectServer") - err := k8sClient.Get(ctx, typeNamespacedName, prefectserver) - if err != nil && errors.IsNotFound(err) { - resource := &prefectiov1.PrefectServer{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: "default", - }, - // TODO(user): Specify other spec details if needed. - } - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + It("should ignore removed PrefectServers", func() { + serverList := &prefectiov1.PrefectServerList{} + err := k8sClient.List(ctx, serverList, &client.ListOptions{Namespace: namespaceName}) + Expect(err).NotTo(HaveOccurred()) + Expect(serverList.Items).To(HaveLen(0)) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), } + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: namespaceName, + Name: "nonexistant-prefect", + }, + }) + Expect(err).NotTo(HaveOccurred()) }) - AfterEach(func() { - // TODO(user): Cleanup logic after each test, like removing the resource instance. - resource := &prefectiov1.PrefectServer{} - err := k8sClient.Get(ctx, typeNamespacedName, resource) + It("should allow specifying a full image name", func() { + prefectserver = &prefectiov1.PrefectServer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespaceName, + Name: "prefect-on-anything", + }, + Spec: prefectiov1.PrefectServerSpec{ + Image: ptr.To("prefecthq/prefect:custom-prefect-image"), + }, + } + Expect(k8sClient.Create(ctx, prefectserver)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-anything", + }, + }) Expect(err).NotTo(HaveOccurred()) - By("Cleanup the specific resource instance PrefectServer") - Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + deployment := &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-anything", + }, deployment) + }).Should(Succeed()) + + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + Expect(container.Image).To(Equal("prefecthq/prefect:custom-prefect-image")) }) - It("should successfully reconcile the resource", func() { - By("Reconciling the created resource") + + It("should allow specifying a Prefect version", func() { + prefectserver = &prefectiov1.PrefectServer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespaceName, + Name: "prefect-on-anything", + }, + Spec: prefectiov1.PrefectServerSpec{ + Version: ptr.To("3.3.3.3.3.3.3.3"), + }, + } + Expect(k8sClient.Create(ctx, prefectserver)).To(Succeed()) + controllerReconciler := &PrefectServerReconciler{ Client: k8sClient, Scheme: k8sClient.Scheme(), } _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, + NamespacedName: types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-anything", + }, }) Expect(err).NotTo(HaveOccurred()) - // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. - // Example: If you expect a certain status condition after reconciliation, verify it here. + + deployment := &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-anything", + }, deployment) + }).Should(Succeed()) + + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + Expect(container.Image).To(Equal("prefecthq/prefect:3.3.3.3.3.3.3.3-python3.12")) + }) + + Context("when creating any server", func() { + var deployment *appsv1.Deployment + var service *corev1.Service + + BeforeEach(func() { + name = types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-anything", + } + + prefectserver = &prefectiov1.PrefectServer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespaceName, + Name: "prefect-on-anything", + }, + } + Expect(k8sClient.Create(ctx, prefectserver)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).NotTo(HaveOccurred()) + + deployment = &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-anything", + }, deployment) + }).Should(Succeed()) + + service = &corev1.Service{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-anything", + }, service) + }).Should(Succeed()) + }) + + Describe("the Deployment", func() { + It("should be owned by the PrefectServer", func() { + Expect(deployment.OwnerReferences).To(ContainElement( + metav1.OwnerReference{ + APIVersion: "prefect.io/v1", + Kind: "PrefectServer", + Name: "prefect-on-anything", + UID: prefectserver.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + )) + }) + + It("should have appropriate labels", func() { + Expect(deployment.Spec.Selector.MatchLabels).To(Equal(map[string]string{ + "app": "prefect-on-anything", + })) + }) + + It("should have a server container with the right image and command", func() { + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + + Expect(container.Name).To(Equal("prefect-server")) + Expect(container.Image).To(Equal("prefecthq/prefect:3.0.0rc15-python3.12")) + Expect(container.Command).To(Equal([]string{"prefect", "server", "start", "--host", "0.0.0.0"})) + }) + + It("should have an environment with PREFECT_HOME set", func() { + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + + Expect(container.Env).To(ContainElements([]corev1.EnvVar{ + {Name: "PREFECT_HOME", Value: "/var/lib/prefect/"}, + })) + }) + + It("should expose the Prefect server on port 4200", func() { + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + + Expect(container.Ports).To(ConsistOf([]corev1.ContainerPort{ + {Name: "api", ContainerPort: 4200, Protocol: corev1.ProtocolTCP}, + })) + }) + }) + + Describe("the Service", func() { + It("should be owned by the PrefectServer", func() { + Expect(service.OwnerReferences).To(ContainElement( + metav1.OwnerReference{ + APIVersion: "prefect.io/v1", + Kind: "PrefectServer", + Name: "prefect-on-anything", + UID: prefectserver.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + )) + }) + + It("should have matching labels", func() { + Expect(service.Spec.Selector).To(Equal(map[string]string{ + "app": "prefect-on-anything", + })) + }) + + It("should expose the API port", func() { + Expect(service.Spec.Ports).To(ConsistOf(corev1.ServicePort{ + Name: "api", + Protocol: corev1.ProtocolTCP, + Port: 4200, + TargetPort: intstr.FromString("api"), + })) + }) + }) + }) + + Context("When updating any server", func() { + BeforeEach(func() { + name = types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-anything", + } + + prefectserver = &prefectiov1.PrefectServer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespaceName, + Name: "prefect-on-anything", + }, + } + Expect(k8sClient.Create(ctx, prefectserver)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // Reconcile once to create the server + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).NotTo(HaveOccurred()) + + prefectserver.Spec.Settings = []corev1.EnvVar{ + {Name: "PREFECT_SOME_SETTING", Value: "some-value"}, + } + Expect(k8sClient.Update(ctx, prefectserver)).To(Succeed()) + + // Reconcile again to update the server + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should update the Deployment with the new setting", func() { + deployment := &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-anything", + }, deployment) + }).Should(Succeed()) + + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + Expect(container.Env).To(ContainElement(corev1.EnvVar{ + Name: "PREFECT_SOME_SETTING", + Value: "some-value", + })) + }) + + It("should not attempt to update a Deployment that it does not own", func() { + deployment := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-anything", + }, deployment)).To(Succeed()) + + deployment.OwnerReferences = nil + Expect(k8sClient.Update(ctx, deployment)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).To(MatchError("Deployment already exists and is not controlled by PrefectServer prefect-on-anything")) + }) + + It("should not attempt to update a service that it does not own", func() { + service := &corev1.Service{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-anything", + }, service)).To(Succeed()) + + service.OwnerReferences = nil + Expect(k8sClient.Update(ctx, service)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).To(MatchError("Service already exists and is not controlled by PrefectServer prefect-on-anything")) + }) + }) + }) + + Context("for ephemeral servers", func() { + BeforeEach(func() { + ctx = context.Background() + namespaceName = fmt.Sprintf("ephemeral-ns-%d", time.Now().UnixNano()) + + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: namespaceName}, + } + Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) + + name = types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-ephemeral", + } + }) + + Context("when creating an ephemeral server", func() { + var deployment *appsv1.Deployment + var service *corev1.Service + + BeforeEach(func() { + prefectserver = &prefectiov1.PrefectServer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespaceName, + Name: "prefect-on-ephemeral", + }, + } + Expect(k8sClient.Create(ctx, prefectserver)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).NotTo(HaveOccurred()) + + deployment = &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-ephemeral", + }, deployment) + }).Should(Succeed()) + + service = &corev1.Service{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-ephemeral", + }, service) + }).Should(Succeed()) + }) + + Describe("the Deployment", func() { + It("should use ephemeral storage", func() { + Expect(deployment.Spec.Template.Spec.Volumes).To(ContainElement( + corev1.Volume{ + Name: "prefect-data", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + )) + + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + Expect(container.VolumeMounts).To(ContainElement( + corev1.VolumeMount{ + Name: "prefect-data", + MountPath: "/var/lib/prefect/", + }, + )) + }) + + It("should have an environment pointing to the ephemeral database", func() { + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + + Expect(container.Env).To(ContainElements([]corev1.EnvVar{ + {Name: "PREFECT_API_DATABASE_DRIVER", Value: "sqlite+aiosqlite"}, + {Name: "PREFECT_API_DATABASE_NAME", Value: "/var/lib/prefect/prefect.db"}, + {Name: "PREFECT_API_DATABASE_MIGRATE_ON_START", Value: "True"}, + })) + }) + }) + }) + + Context("When updating an ephemeral server", func() { + BeforeEach(func() { + prefectserver = &prefectiov1.PrefectServer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespaceName, + Name: "prefect-on-ephemeral", + }, + } + Expect(k8sClient.Create(ctx, prefectserver)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // Reconcile once to create the server + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).NotTo(HaveOccurred()) + + prefectserver.Spec.Settings = []corev1.EnvVar{ + {Name: "PREFECT_SOME_SETTING", Value: "some-value"}, + } + Expect(k8sClient.Update(ctx, prefectserver)).To(Succeed()) + + // Reconcile again to update the server + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should update the Deployment with the new setting", func() { + deployment := &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-ephemeral", + }, deployment) + }).Should(Succeed()) + + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + Expect(container.Env).To(ContainElement(corev1.EnvVar{ + Name: "PREFECT_SOME_SETTING", + Value: "some-value", + })) + }) + + It("should not attempt to update a Deployment that it does not own", func() { + deployment := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-ephemeral", + }, deployment)).To(Succeed()) + + deployment.OwnerReferences = nil + Expect(k8sClient.Update(ctx, deployment)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).To(MatchError("Deployment already exists and is not controlled by PrefectServer prefect-on-ephemeral")) + }) + + It("should not attempt to update a service that it does not own", func() { + service := &corev1.Service{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-ephemeral", + }, service)).To(Succeed()) + + service.OwnerReferences = nil + Expect(k8sClient.Update(ctx, service)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).To(MatchError("Service already exists and is not controlled by PrefectServer prefect-on-ephemeral")) + }) + }) + }) + + Context("for SQLite servers", func() { + BeforeEach(func() { + ctx = context.Background() + namespaceName = fmt.Sprintf("sqlite-ns-%d", time.Now().UnixNano()) + + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: namespaceName}, + } + Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) + + name = types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-sqlite", + } + }) + + Context("When creating a server backed by SQLite", func() { + var persistentVolumeClaim *corev1.PersistentVolumeClaim + var deployment *appsv1.Deployment + var service *corev1.Service + + BeforeEach(func() { + prefectserver = &prefectiov1.PrefectServer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespaceName, + Name: "prefect-on-sqlite", + }, + Spec: prefectiov1.PrefectServerSpec{ + SQLite: &prefectiov1.SQLiteConfiguration{ + StorageClassName: "standard", + Size: resource.MustParse("512Mi"), + }, + }, + } + Expect(k8sClient.Create(ctx, prefectserver)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).NotTo(HaveOccurred()) + + persistentVolumeClaim = &corev1.PersistentVolumeClaim{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-sqlite-data", + }, persistentVolumeClaim) + }).Should(Succeed()) + + deployment = &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-sqlite", + }, deployment) + }).Should(Succeed()) + + service = &corev1.Service{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-sqlite", + }, service) + }).Should(Succeed()) + }) + + Describe("the Deployment", func() { + It("should use persistent storage", func() { + Expect(deployment.Spec.Template.Spec.Volumes).To(ContainElement( + corev1.Volume{ + Name: "prefect-data", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "prefect-on-sqlite-data", + }, + }, + }, + )) + + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + Expect(container.VolumeMounts).To(ContainElement( + corev1.VolumeMount{ + Name: "prefect-data", + MountPath: "/var/lib/prefect/", + }, + )) + }) + + It("should have an environment pointing to the persistent database", func() { + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + + Expect(container.Env).To(ConsistOf([]corev1.EnvVar{ + {Name: "PREFECT_HOME", Value: "/var/lib/prefect/"}, + {Name: "PREFECT_API_DATABASE_DRIVER", Value: "sqlite+aiosqlite"}, + {Name: "PREFECT_API_DATABASE_NAME", Value: "/var/lib/prefect/prefect.db"}, + {Name: "PREFECT_API_DATABASE_MIGRATE_ON_START", Value: "True"}, + })) + }) + }) + }) + + Context("When updating a server backed by SQLite", func() { + BeforeEach(func() { + prefectserver = &prefectiov1.PrefectServer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespaceName, + Name: "prefect-on-sqlite", + }, + Spec: prefectiov1.PrefectServerSpec{ + SQLite: &prefectiov1.SQLiteConfiguration{ + StorageClassName: "standard", + Size: resource.MustParse("512Mi"), + }, + }, + } + Expect(k8sClient.Create(ctx, prefectserver)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // Reconcile once to create the server + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).NotTo(HaveOccurred()) + + prefectserver.Spec.Settings = []corev1.EnvVar{ + {Name: "PREFECT_SOME_SETTING", Value: "some-value"}, + } + Expect(k8sClient.Update(ctx, prefectserver)).To(Succeed()) + + // Reconcile again to update the server + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should update the Deployment with the new setting", func() { + deployment := &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-sqlite", + }, deployment) + }).Should(Succeed()) + + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + Expect(container.Env).To(ContainElement(corev1.EnvVar{ + Name: "PREFECT_SOME_SETTING", + Value: "some-value", + })) + }) + + It("should not attempt to update a PersistentVolumeClaim that it does not own", func() { + pvc := &corev1.PersistentVolumeClaim{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-sqlite-data", + }, pvc)).To(Succeed()) + + pvc.OwnerReferences = nil + Expect(k8sClient.Update(ctx, pvc)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).To(MatchError("PersistentVolumeClaim already exists and is not controlled by PrefectServer prefect-on-sqlite")) + }) + + It("should not attempt to update a Deployment that it does not own", func() { + deployment := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-sqlite", + }, deployment)).To(Succeed()) + + deployment.OwnerReferences = nil + Expect(k8sClient.Update(ctx, deployment)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).To(MatchError("Deployment already exists and is not controlled by PrefectServer prefect-on-sqlite")) + }) + + It("should not attempt to update a service that it does not own", func() { + service := &corev1.Service{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-sqlite", + }, service)).To(Succeed()) + + service.OwnerReferences = nil + Expect(k8sClient.Update(ctx, service)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).To(MatchError("Service already exists and is not controlled by PrefectServer prefect-on-sqlite")) + }) + }) + }) + + Context("for PostgreSQL servers", func() { + BeforeEach(func() { + ctx = context.Background() + namespaceName = fmt.Sprintf("postgres-ns-%d", time.Now().UnixNano()) + + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: namespaceName}, + } + Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) + + name = types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-postgres", + } + }) + + Context("When creating a server backed by PostgreSQL", func() { + var deployment *appsv1.Deployment + var migrateJob *batchv1.Job + var service *corev1.Service + + BeforeEach(func() { + prefectserver = &prefectiov1.PrefectServer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespaceName, + Name: "prefect-on-postgres", + }, + Spec: prefectiov1.PrefectServerSpec{ + Postgres: &prefectiov1.PostgresConfiguration{ + Host: ptr.To("some-postgres-server"), + Port: ptr.To(15432), + User: ptr.To("a-prefect-user"), + Password: ptr.To("this-is-a-bad-idea"), + Database: ptr.To("some-prefect"), + }, + }, + } + Expect(k8sClient.Create(ctx, prefectserver)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).NotTo(HaveOccurred()) + + migrateJob = &batchv1.Job{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-postgres-migration", + }, migrateJob) + }).Should(Succeed()) + + deployment = &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-postgres", + }, deployment) + }).Should(Succeed()) + + service = &corev1.Service{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-postgres", + }, service) + }).Should(Succeed()) + }) + + Describe("the Deployment", func() { + It("should not use persistent storage", func() { + Expect(deployment.Spec.Template.Spec.Volumes).To(BeEmpty()) + + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + Expect(container.VolumeMounts).To(BeEmpty()) + }) + + It("should have an environment pointing to the PostgreSQL database", func() { + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + + Expect(container.Env).To(ConsistOf([]corev1.EnvVar{ + {Name: "PREFECT_HOME", Value: "/var/lib/prefect/"}, + {Name: "PREFECT_API_DATABASE_DRIVER", Value: "postgresql+asyncpg"}, + {Name: "PREFECT_API_DATABASE_HOST", Value: "some-postgres-server"}, + {Name: "PREFECT_API_DATABASE_PORT", Value: "15432"}, + {Name: "PREFECT_API_DATABASE_USER", Value: "a-prefect-user"}, + {Name: "PREFECT_API_DATABASE_PASSWORD", Value: "this-is-a-bad-idea"}, + {Name: "PREFECT_API_DATABASE_NAME", Value: "some-prefect"}, + {Name: "PREFECT_API_DATABASE_MIGRATE_ON_START", Value: "False"}, + })) + }) + }) + + Describe("the migration Job", func() { + It("should be owned by the PrefectServer", func() { + Expect(migrateJob.OwnerReferences).To(ContainElement( + metav1.OwnerReference{ + APIVersion: "prefect.io/v1", + Kind: "PrefectServer", + Name: "prefect-on-postgres", + UID: prefectserver.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + )) + }) + + It("should have an environment pointing to the PostgreSQL database", func() { + Expect(migrateJob.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := migrateJob.Spec.Template.Spec.Containers[0] + + Expect(container.Env).To(ConsistOf([]corev1.EnvVar{ + {Name: "PREFECT_HOME", Value: "/var/lib/prefect/"}, + {Name: "PREFECT_API_DATABASE_DRIVER", Value: "postgresql+asyncpg"}, + {Name: "PREFECT_API_DATABASE_HOST", Value: "some-postgres-server"}, + {Name: "PREFECT_API_DATABASE_PORT", Value: "15432"}, + {Name: "PREFECT_API_DATABASE_USER", Value: "a-prefect-user"}, + {Name: "PREFECT_API_DATABASE_PASSWORD", Value: "this-is-a-bad-idea"}, + {Name: "PREFECT_API_DATABASE_NAME", Value: "some-prefect"}, + {Name: "PREFECT_API_DATABASE_MIGRATE_ON_START", Value: "False"}, + })) + }) + }) + }) + + Context("When creating a server backed by PostgreSQL ConfigMaps and Secrets", func() { + var deployment *appsv1.Deployment + var migrateJob *batchv1.Job + var service *corev1.Service + + BeforeEach(func() { + prefectserver = &prefectiov1.PrefectServer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespaceName, + Name: "prefect-on-postgres", + }, + Spec: prefectiov1.PrefectServerSpec{ + Postgres: &prefectiov1.PostgresConfiguration{ + HostFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "postgres-config"}, + Key: "host", + }, + }, + PortFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "postgres-config"}, + Key: "host", + }, + }, + UserFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "postgres-secret"}, + Key: "user", + }, + }, + PasswordFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "postgres-secret"}, + Key: "password", + }, + }, + DatabaseFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.name", + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, prefectserver)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).NotTo(HaveOccurred()) + + migrateJob = &batchv1.Job{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-postgres-migration", + }, migrateJob) + }).Should(Succeed()) + + deployment = &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-postgres", + }, deployment) + }).Should(Succeed()) + + service = &corev1.Service{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-postgres", + }, service) + }).Should(Succeed()) + }) + + Describe("the Deployment", func() { + It("should not use persistent storage", func() { + Expect(deployment.Spec.Template.Spec.Volumes).To(BeEmpty()) + + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + Expect(container.VolumeMounts).To(BeEmpty()) + }) + + It("should have an environment pointing to the PostgreSQL database", func() { + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + + Expect(container.Env).To(ConsistOf([]corev1.EnvVar{ + {Name: "PREFECT_HOME", Value: "/var/lib/prefect/"}, + {Name: "PREFECT_API_DATABASE_DRIVER", Value: "postgresql+asyncpg"}, + {Name: "PREFECT_API_DATABASE_HOST", ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "postgres-config"}, + Key: "host", + }, + }}, + {Name: "PREFECT_API_DATABASE_PORT", ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "postgres-config"}, + Key: "host", + }, + }}, + {Name: "PREFECT_API_DATABASE_USER", ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "postgres-secret"}, + Key: "user", + }, + }}, + {Name: "PREFECT_API_DATABASE_PASSWORD", ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "postgres-secret"}, + Key: "password", + }, + }}, + {Name: "PREFECT_API_DATABASE_NAME", ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.name", + }, + }}, + {Name: "PREFECT_API_DATABASE_MIGRATE_ON_START", Value: "False"}, + })) + }) + }) + }) + + Context("When updating a server backed by PostgreSQL", func() { + BeforeEach(func() { + prefectserver = &prefectiov1.PrefectServer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespaceName, + Name: "prefect-on-postgres", + }, + Spec: prefectiov1.PrefectServerSpec{ + Postgres: &prefectiov1.PostgresConfiguration{ + Host: ptr.To("some-postgres-server"), + Port: ptr.To(15432), + User: ptr.To("a-prefect-user"), + Password: ptr.To("this-is-a-bad-idea"), + Database: ptr.To("some-prefect"), + }, + }, + } + Expect(k8sClient.Create(ctx, prefectserver)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // Reconcile once to create the server + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).NotTo(HaveOccurred()) + + prefectserver.Spec.Settings = []corev1.EnvVar{ + {Name: "PREFECT_SOME_SETTING", Value: "some-value"}, + } + Expect(k8sClient.Update(ctx, prefectserver)).To(Succeed()) + + // Reconcile again to update the server + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should update the Deployment with the new setting", func() { + deployment := &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-postgres", + }, deployment) + }).Should(Succeed()) + + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + container := deployment.Spec.Template.Spec.Containers[0] + Expect(container.Env).To(ContainElement(corev1.EnvVar{ + Name: "PREFECT_SOME_SETTING", + Value: "some-value", + })) + }) + + It("should not attempt to update a migration Job that it does not own", func() { + job := &batchv1.Job{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-postgres-migration", + }, job)).To(Succeed()) + + job.OwnerReferences = nil + Expect(k8sClient.Update(ctx, job)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).To(MatchError("Job already exists and is not controlled by PrefectServer prefect-on-postgres")) + }) + + It("should not attempt to update a Deployment that it does not own", func() { + deployment := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-postgres", + }, deployment)).To(Succeed()) + + deployment.OwnerReferences = nil + Expect(k8sClient.Update(ctx, deployment)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).To(MatchError("Deployment already exists and is not controlled by PrefectServer prefect-on-postgres")) + }) + + It("should not attempt to update a service that it does not own", func() { + service := &corev1.Service{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespaceName, + Name: "prefect-on-postgres", + }, service)).To(Succeed()) + + service.OwnerReferences = nil + Expect(k8sClient.Update(ctx, service)).To(Succeed()) + + controllerReconciler := &PrefectServerReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: name, + }) + Expect(err).To(MatchError("Service already exists and is not controlled by PrefectServer prefect-on-postgres")) + }) }) }) }) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 08f7cab..24f1548 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -60,7 +60,7 @@ var _ = Describe("controller", Ordered, func() { var err error // projectimage stores the name of the image used in the example - var projectimage = "example.com/prefect-operator:v0.0.1" + var projectimage = "PrefectHQ/prefect-operator:v0.0.1" By("building the manager(Operator) image") cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectimage))