diff --git a/api/core/v1alpha1/catalog_types.go b/api/core/v1alpha1/catalog_types.go index c3f77526..b91b803c 100644 --- a/api/core/v1alpha1/catalog_types.go +++ b/api/core/v1alpha1/catalog_types.go @@ -30,10 +30,12 @@ const ( TypeUnpacked = "Unpacked" - ReasonUnpackPending = "UnpackPending" - ReasonUnpacking = "Unpacking" - ReasonUnpackSuccessful = "UnpackSuccessful" - ReasonUnpackFailed = "UnpackFailed" + ReasonUnpackPending = "UnpackPending" + ReasonUnpacking = "Unpacking" + ReasonUnpackSuccessful = "UnpackSuccessful" + ReasonUnpackFailed = "UnpackFailed" + ReasonStorageFailed = "FailedToStore" + ReasonStorageDeleteFailed = "FailedToDelete" PhasePending = "Pending" PhaseUnpacking = "Unpacking" diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 0214b526..30ec040d 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -39,6 +39,7 @@ import ( corecontrollers "github.com/operator-framework/catalogd/pkg/controllers/core" "github.com/operator-framework/catalogd/pkg/features" "github.com/operator-framework/catalogd/pkg/profile" + "github.com/operator-framework/catalogd/pkg/storage" //+kubebuilder:scaffold:imports "github.com/operator-framework/catalogd/api/core/v1alpha1" @@ -65,6 +66,7 @@ func main() { profiling bool catalogdVersion bool systemNamespace string + storageDir string ) flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -74,6 +76,7 @@ func main() { // TODO: should we move the unpacker to some common place? Or... hear me out... should catalogd just be a rukpak provisioner? flag.StringVar(&unpackImage, "unpack-image", "quay.io/operator-framework/rukpak:v0.12.0", "The unpack image to use when unpacking catalog images") flag.StringVar(&systemNamespace, "system-namespace", "", "The namespace catalogd uses for internal state, configuration, and workloads") + flag.StringVar(&storageDir, "catalogs-storage-dir", "/var/cache/catalogs", "The directory in the filesystem where unpacked catalog content will be stored and served from") flag.BoolVar(&profiling, "profiling", false, "enable profiling endpoints to allow for using pprof") flag.BoolVar(&catalogdVersion, "version", false, "print the catalogd version and exit") opts := zap.Options{ @@ -116,9 +119,13 @@ func main() { os.Exit(1) } + if err := os.MkdirAll(storageDir, 0700); err != nil { + setupLog.Error(err, "unable to create storage directory for catalogs") + } if err = (&corecontrollers.CatalogReconciler{ Client: mgr.GetClient(), Unpacker: unpacker, + Storage: storage.LocalDir{RootDir: storageDir}, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Catalog") os.Exit(1) diff --git a/config/default/manager_auth_proxy_patch.yaml b/config/default/manager_auth_proxy_patch.yaml index 817eabf3..738d7068 100644 --- a/config/default/manager_auth_proxy_patch.yaml +++ b/config/default/manager_auth_proxy_patch.yaml @@ -50,4 +50,4 @@ spec: - "--health-probe-bind-address=:8081" - "--metrics-bind-address=127.0.0.1:8080" - "--leader-elect" - - "--feature-gates=CatalogMetadataAPI=true" + - "--feature-gates=CatalogMetadataAPI=true,HTTPServer=false" diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 73d00426..d0881797 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -47,8 +47,12 @@ spec: - "./manager" args: - --leader-elect + - "--catalogs-storage-dir=/var/cache/catalogs" image: controller:latest name: manager + volumeMounts: + - name: catalog-cache + mountPath: /var/cache/catalogs securityContext: allowPrivilegeEscalation: false capabilities: @@ -73,3 +77,6 @@ spec: imagePullPolicy: IfNotPresent serviceAccountName: controller-manager terminationGracePeriodSeconds: 10 + volumes: + - name: catalog-cache + emptyDir: {} diff --git a/go.mod b/go.mod index bd971964..34200f46 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,13 @@ go 1.20 require ( github.com/blang/semver/v4 v4.0.0 + github.com/google/go-cmp v0.5.9 github.com/nlepage/go-tarfs v1.1.0 github.com/onsi/ginkgo/v2 v2.9.7 github.com/onsi/gomega v1.27.7 github.com/operator-framework/operator-registry v1.27.1 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.1 k8s.io/api v0.26.1 k8s.io/apimachinery v0.26.1 k8s.io/client-go v0.26.1 @@ -19,15 +20,17 @@ require ( ) require ( + github.com/acomagu/bufpipe v1.0.3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-git/gcfg v1.5.0 // indirect - github.com/go-git/go-billy/v5 v5.1.0 // indirect - github.com/go-git/go-git/v5 v5.3.0 // indirect + github.com/go-git/go-billy/v5 v5.4.1 // indirect + github.com/go-git/go-git/v5 v5.4.2 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/zapr v1.2.3 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect @@ -38,13 +41,12 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/google/gofuzz v1.1.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/uuid v1.3.0 // indirect github.com/h2non/filetype v1.1.1 // indirect github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c // indirect - github.com/imdario/mergo v0.3.12 // indirect + github.com/imdario/mergo v0.3.13 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/joelanford/ignore v0.0.0-20210607151042-0d25dc18b62d // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/go.sum b/go.sum index 130830a4..cda71e23 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,9 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= +github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= +github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -73,7 +76,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= -github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= @@ -83,11 +87,16 @@ github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-billy/v5 v5.1.0 h1:4pl5BV4o7ZG/lterP4S6WzJ6xr49Ba5ET9ygheTYahk= github.com/go-git/go-billy/v5 v5.1.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= +github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= -github.com/go-git/go-git/v5 v5.3.0 h1:8WKMtJR2j8RntEXR/uvTKagfEt4GYlwQ7mntE4+0GWc= +github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw= +github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= +github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -166,8 +175,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= -github.com/google/gofuzz v1.1.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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -192,8 +201,9 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -230,6 +240,8 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= +github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -301,6 +313,7 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -308,8 +321,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/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 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -340,6 +354,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U 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/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -469,6 +484,7 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -476,6 +492,7 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -654,6 +671,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/controllers/core/catalog_controller.go b/pkg/controllers/core/catalog_controller.go index 4d478489..fc876e79 100644 --- a/pkg/controllers/core/catalog_controller.go +++ b/pkg/controllers/core/catalog_controller.go @@ -36,6 +36,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -43,14 +44,16 @@ import ( "github.com/operator-framework/catalogd/internal/k8sutil" "github.com/operator-framework/catalogd/internal/source" "github.com/operator-framework/catalogd/pkg/features" + "github.com/operator-framework/catalogd/pkg/storage" ) -// TODO (everettraven): Add unit tests for the CatalogReconciler +const fbcDeletionFinalizer = "catalogd.operatorframework.io/delete-server-cache" // CatalogReconciler reconciles a Catalog object type CatalogReconciler struct { client.Client Unpacker source.Unpacker + Storage storage.Instance } //+kubebuilder:rbac:groups=catalogd.operatorframework.io,resources=catalogs,verbs=get;list;watch;create;update;patch;delete @@ -73,7 +76,6 @@ func (r *CatalogReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if err := r.Client.Get(ctx, req.NamespacedName, &existingCatsrc); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - reconciledCatsrc := existingCatsrc.DeepCopy() res, reconcileErr := r.reconcile(ctx, reconciledCatsrc) @@ -121,6 +123,19 @@ func (r *CatalogReconciler) SetupWithManager(mgr ctrl.Manager) error { // linting from the linter that was fussing about this. // nolint:unparam func (r *CatalogReconciler) reconcile(ctx context.Context, catalog *v1alpha1.Catalog) (ctrl.Result, error) { + if features.CatalogdFeatureGate.Enabled(features.HTTPServer) && catalog.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(catalog, fbcDeletionFinalizer) { + controllerutil.AddFinalizer(catalog, fbcDeletionFinalizer) + if err := r.Update(ctx, catalog); err != nil { + return ctrl.Result{}, err + } + } + if features.CatalogdFeatureGate.Enabled(features.HTTPServer) && !catalog.DeletionTimestamp.IsZero() { + if err := r.Storage.Delete(catalog.Name); err != nil { + return ctrl.Result{}, updateStatusStorageDeleteError(&catalog.Status, err) + } + controllerutil.RemoveFinalizer(catalog, fbcDeletionFinalizer) + return ctrl.Result{}, r.Update(ctx, catalog) + } unpackResult, err := r.Unpacker.Unpack(ctx, catalog) if err != nil { return ctrl.Result{}, updateStatusUnpackFailing(&catalog.Status, fmt.Errorf("source bundle content: %v", err)) @@ -137,6 +152,11 @@ func (r *CatalogReconciler) reconcile(ctx context.Context, catalog *v1alpha1.Cat // TODO: We should check to see if the unpacked result has the same content // as the already unpacked content. If it does, we should skip this rest // of the unpacking steps. + if features.CatalogdFeatureGate.Enabled(features.HTTPServer) { + if err := r.Storage.Store(catalog.Name, unpackResult.FS); err != nil { + return ctrl.Result{}, updateStatusStorageError(&catalog.Status, fmt.Errorf("error storing fbc: %v", err)) + } + } if features.CatalogdFeatureGate.Enabled(features.CatalogMetadataAPI) { if err = r.syncCatalogMetadata(ctx, unpackResult.FS, catalog); err != nil { return ctrl.Result{}, updateStatusUnpackFailing(&catalog.Status, fmt.Errorf("create catalog metadata objects: %v", err)) @@ -195,6 +215,28 @@ func updateStatusUnpackFailing(status *v1alpha1.CatalogStatus, err error) error return err } +func updateStatusStorageError(status *v1alpha1.CatalogStatus, err error) error { + status.ResolvedSource = nil + status.Phase = v1alpha1.PhaseFailing + meta.SetStatusCondition(&status.Conditions, metav1.Condition{ + Type: v1alpha1.TypeUnpacked, + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReasonStorageFailed, + Message: fmt.Sprintf("failed to store bundle: %s", err.Error()), + }) + return err +} + +func updateStatusStorageDeleteError(status *v1alpha1.CatalogStatus, err error) error { + meta.SetStatusCondition(&status.Conditions, metav1.Condition{ + Type: v1alpha1.TypeUnpacked, + Status: metav1.ConditionFalse, + Reason: v1alpha1.ReasonStorageDeleteFailed, + Message: fmt.Sprintf("failed to delete storage: %s", err.Error()), + }) + return err +} + // syncCatalogMetadata will sync all of the catalog contents to `CatalogMetadata` objects // by creating, updating and deleting the objects as necessary. Returns an // error if any are encountered. diff --git a/pkg/controllers/core/catalog_controller_test.go b/pkg/controllers/core/catalog_controller_test.go index c91d40b9..890150b4 100644 --- a/pkg/controllers/core/catalog_controller_test.go +++ b/pkg/controllers/core/catalog_controller_test.go @@ -4,9 +4,12 @@ import ( "context" "errors" "fmt" + "io/fs" "os" "testing/fstest" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/format" @@ -15,12 +18,14 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/rand" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" "github.com/operator-framework/catalogd/api/core/v1alpha1" "github.com/operator-framework/catalogd/internal/source" "github.com/operator-framework/catalogd/pkg/controllers/core" "github.com/operator-framework/catalogd/pkg/features" + "github.com/operator-framework/catalogd/pkg/storage" ) var _ source.Unpacker = &MockSource{} @@ -42,16 +47,38 @@ func (ms *MockSource) Unpack(_ context.Context, _ *v1alpha1.Catalog) (*source.Re return ms.result, nil } +var _ storage.Instance = &MockStore{} + +type MockStore struct { + shouldError bool +} + +func (m MockStore) Store(_ string, _ fs.FS) error { + if m.shouldError { + return errors.New("mockstore store error") + } + return nil +} + +func (m MockStore) Delete(_ string) error { + if m.shouldError { + return errors.New("mockstore delete error") + } + return nil +} + var _ = Describe("Catalogd Controller Test", func() { format.MaxLength = 0 var ( ctx context.Context reconciler *core.CatalogReconciler mockSource *MockSource + mockStore *MockStore ) BeforeEach(func() { ctx = context.Background() mockSource = &MockSource{} + mockStore = &MockStore{} reconciler = &core.CatalogReconciler{ Client: cl, Unpacker: source.NewUnpacker( @@ -59,9 +86,9 @@ var _ = Describe("Catalogd Controller Test", func() { v1alpha1.SourceTypeImage: mockSource, }, ), + Storage: mockStore, } }) - When("the catalog does not exist", func() { It("returns no error", func() { res, err := reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: types.NamespacedName{Name: "non-existent"}}) @@ -147,7 +174,7 @@ var _ = Describe("Catalogd Controller Test", func() { AfterEach(func() { By("tearing down cluster state") - Expect(cl.Delete(ctx, catalog)).To(Succeed()) + Expect(client.IgnoreNotFound(cl.Delete(ctx, catalog))).To(Succeed()) }) When("unpacker returns source.Result with state == 'Pending'", func() { @@ -248,7 +275,6 @@ var _ = Describe("Catalogd Controller Test", func() { FS: filesys, } }) - It("should set unpacking status to 'unpacked'", func() { // reconcile res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: catalogKey}) @@ -266,10 +292,79 @@ var _ = Describe("Catalogd Controller Test", func() { Expect(cond.Status).To(Equal(metav1.ConditionTrue)) }) + When("HTTPServer feature gate is enabled", func() { + BeforeEach(func() { + Expect(features.CatalogdFeatureGate.SetFromMap(map[string]bool{ + string(features.CatalogMetadataAPI): false, + string(features.HTTPServer): true, + })).NotTo(HaveOccurred()) + }) + When("there is no error in storing the fbc", func() { + BeforeEach(func() { + By("setting up mockStore to return no error", func() { + mockStore.shouldError = false + }) + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: catalogKey}) + Expect(res).To(Equal(ctrl.Result{})) + Expect(err).ToNot(HaveOccurred()) + }) + It("should reflect in the status condition", func() { + cat := &v1alpha1.Catalog{} + Expect(cl.Get(ctx, catalogKey, cat)).To(Succeed()) + Expect(cat.Status.Phase).To(Equal(v1alpha1.PhaseUnpacked)) + diff := cmp.Diff(meta.FindStatusCondition(cat.Status.Conditions, v1alpha1.TypeUnpacked), &metav1.Condition{ + Reason: v1alpha1.ReasonUnpackSuccessful, + Status: metav1.ConditionTrue, + }, cmpopts.IgnoreFields(metav1.Condition{}, "Type", "ObservedGeneration", "LastTransitionTime", "Message")) + Expect(diff).To(Equal("")) + }) + + // When("the catalog is deleted but there is an error deleting the stored FBC", func() { + // BeforeEach(func() { + // By("setting up mockStore to return an error", func() { + // mockStore.shouldError = true + // }) + // Expect(cl.Delete(ctx, catalog)).To(Not(Succeed())) + // }) + // It("should set status condition to reflect the error", func() { + // // get the catalog and ensure status is set properly + // cat := &v1alpha1.Catalog{} + // Expect(cl.Get(ctx, catalogKey, cat)).To(Succeed()) + // cond := meta.FindStatusCondition(cat.Status.Conditions, v1alpha1.TypeStorage) + // Expect(cond).To(Not(BeNil())) + // Expect(cond.Reason).To(Equal(v1alpha1.ReasonStorageDeleteFailed)) + // Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + // }) + // }) + }) + + When("there is an error storing the fbc", func() { + BeforeEach(func() { + By("setting up mockStore to return an error", func() { + mockStore.shouldError = true + }) + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: catalogKey}) + Expect(res).To(Equal(ctrl.Result{})) + Expect(err).To(HaveOccurred()) + }) + It("should set status condition to reflect that storage error", func() { + cat := &v1alpha1.Catalog{} + Expect(cl.Get(ctx, catalogKey, cat)).To(Succeed()) + Expect(cat.Status.ResolvedSource).To(BeNil()) + Expect(cat.Status.Phase).To(Equal(v1alpha1.PhaseFailing)) + diff := cmp.Diff(meta.FindStatusCondition(cat.Status.Conditions, v1alpha1.TypeUnpacked), &metav1.Condition{ + Reason: v1alpha1.ReasonStorageFailed, + Status: metav1.ConditionFalse, + }, cmpopts.IgnoreFields(metav1.Condition{}, "Type", "ObservedGeneration", "LastTransitionTime", "Message")) + Expect(diff).To(Equal("")) + }) + }) + }) When("the CatalogMetadataAPI feature gate is enabled", func() { BeforeEach(func() { Expect(features.CatalogdFeatureGate.SetFromMap(map[string]bool{ string(features.CatalogMetadataAPI): true, + string(features.HTTPServer): false, })).NotTo(HaveOccurred()) // reconcile diff --git a/pkg/features/features.go b/pkg/features/features.go index 21a0db16..8204f388 100644 --- a/pkg/features/features.go +++ b/pkg/features/features.go @@ -10,6 +10,7 @@ const ( // Ex: SomeFeature featuregate.Feature = "SomeFeature" CatalogMetadataAPI featuregate.Feature = "CatalogMetadataAPI" + HTTPServer featuregate.Feature = "HTTPServer" ) var catalogdFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ @@ -19,6 +20,7 @@ var catalogdFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ // Marking the CatalogMetadataAPI feature gate as Deprecated in the interest of introducing // the HTTP Server functionality in the future and use it as a default method of serving the catalog contents. CatalogMetadataAPI: {Default: false, PreRelease: featuregate.Deprecated}, + HTTPServer: {Default: false, PreRelease: featuregate.Alpha}, } var CatalogdFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate() diff --git a/pkg/storage/localdir.go b/pkg/storage/localdir.go new file mode 100644 index 00000000..edd56ba0 --- /dev/null +++ b/pkg/storage/localdir.go @@ -0,0 +1,52 @@ +package storage + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/operator-framework/operator-registry/alpha/declcfg" +) + +// LocalDir is a storage Instance. When Storing a new FBC contained in +// fs.FS, the content is first written to a temporary file, after which +// it is copied to it's final destination in RootDir/catalogName/. This is +// done so that clients accessing the content stored in RootDir/catalogName have +// atomic view of the content for a catalog. +type LocalDir struct { + RootDir string +} + +func (s LocalDir) Store(catalog string, fsys fs.FS) error { + tmpDir := filepath.Join(s.RootDir, catalog+"-temp") + if err := os.MkdirAll(tmpDir, 0700); err != nil { + return err + } + defer os.RemoveAll(tmpDir) + fbcDir := filepath.Join(s.RootDir, catalog) + if err := os.MkdirAll(fbcDir, 0700); err != nil { + return err + } + tempFile, err := os.OpenFile(filepath.Join(tmpDir, "all.json"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer tempFile.Close() + err = declcfg.WalkMetasFS(fsys, func(path string, meta *declcfg.Meta, err error) error { + if err != nil { + return fmt.Errorf("error in parsing catalog content files in the filesystem: %w", err) + } + _, err = tempFile.Write(meta.Blob) + return err + }) + if err != nil { + return err + } + fbcFile := filepath.Join(fbcDir, "all.json") + return os.Rename(tempFile.Name(), fbcFile) +} + +func (s LocalDir) Delete(catalog string) error { + return os.RemoveAll(filepath.Join(s.RootDir, catalog)) +} diff --git a/pkg/storage/localdir_test.go b/pkg/storage/localdir_test.go new file mode 100644 index 00000000..8600d7c3 --- /dev/null +++ b/pkg/storage/localdir_test.go @@ -0,0 +1,108 @@ +package storage + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "testing/fstest" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/operator-framework/operator-registry/alpha/declcfg" +) + +var _ = Describe("LocalDir Storage Test", func() { + var ( + catalog = "test-catalog" + store Instance + rootDir string + testBundleName = "bundle.v0.0.1" + testBundleImage = "quaydock.io/namespace/bundle:0.0.3" + testBundleRelatedImageName = "test" + testBundleRelatedImageImage = "testimage:latest" + testBundleObjectData = "dW5pbXBvcnRhbnQK" + testPackageDefaultChannel = "preview_test" + testPackageName = "webhook_operator_test" + testChannelName = "preview_test" + testPackage = fmt.Sprintf(testPackageTemplate, testPackageDefaultChannel, testPackageName) + testBundle = fmt.Sprintf(testBundleTemplate, testBundleImage, testBundleName, testPackageName, testBundleRelatedImageName, testBundleRelatedImageImage, testBundleObjectData) + testChannel = fmt.Sprintf(testChannelTemplate, testPackageName, testChannelName, testBundleName) + + unpackResultFS fs.FS + ) + BeforeEach(func() { + d, err := os.MkdirTemp(GinkgoT().TempDir(), "cache") + rootDir = d + Expect(err).ToNot(HaveOccurred()) + + store = LocalDir{RootDir: rootDir} + unpackResultFS = &fstest.MapFS{ + "bundle.yaml": &fstest.MapFile{Data: []byte(testBundle), Mode: os.ModePerm}, + "package.yaml": &fstest.MapFile{Data: []byte(testPackage), Mode: os.ModePerm}, + "channel.yaml": &fstest.MapFile{Data: []byte(testChannel), Mode: os.ModePerm}, + } + }) + When("An unpacked FBC is stored using LocalDir", func() { + BeforeEach(func() { + err := store.Store(catalog, unpackResultFS) + Expect(err).To(Not(HaveOccurred())) + }) + It("should store the content in the RootDir correctly", func() { + fbcFile := filepath.Join(rootDir, catalog, "all.json") + _, err := os.Stat(fbcFile) + Expect(err).To(Not(HaveOccurred())) + + gotConfig, err := declcfg.LoadFS(unpackResultFS) + Expect(err).To(Not(HaveOccurred())) + storedConfig, err := declcfg.LoadFile(os.DirFS(filepath.Join(rootDir, catalog)), "all.json") + Expect(err).To(Not(HaveOccurred())) + diff := cmp.Diff(gotConfig, storedConfig) + Expect(diff).To(Equal("")) + }) + When("The stored content is deleted", func() { + BeforeEach(func() { + err := store.Delete(catalog) + Expect(err).To(Not(HaveOccurred())) + }) + It("should delete the FBC from the cache directory", func() { + fbcFile := filepath.Join(rootDir, catalog) + _, err := os.Stat(fbcFile) + Expect(err).To(HaveOccurred()) + Expect(os.IsNotExist(err)).To(BeTrue()) + }) + }) + }) +}) + +const testBundleTemplate = `--- +image: %s +name: %s +schema: olm.bundle +package: %s +relatedImages: + - name: %s + image: %s +properties: + - type: olm.bundle.object + value: + data: %s + - type: some.other + value: + data: arbitrary-info +` + +const testPackageTemplate = `--- +defaultChannel: %s +name: %s +schema: olm.package +` + +const testChannelTemplate = `--- +schema: olm.channel +package: %s +name: %s +entries: + - name: %s +` diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go new file mode 100644 index 00000000..d0a6e111 --- /dev/null +++ b/pkg/storage/storage.go @@ -0,0 +1,11 @@ +package storage + +import "io/fs" + +// Instance is a storage instance that stores FBC content of catalogs +// added to a cluster. It can be used to Store or Delete FBC in the +// host's filesystem +type Instance interface { + Store(catalog string, fsys fs.FS) error + Delete(catalog string) error +} diff --git a/pkg/storage/suite_test.go b/pkg/storage/suite_test.go new file mode 100644 index 00000000..b0c512de --- /dev/null +++ b/pkg/storage/suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storage + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Storage Suite") +}