diff --git a/go.mod b/go.mod
index afa9ff25f..e736a4cc9 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/operator-framework/api
+module github.com/grokspawn/api
go 1.18
@@ -8,11 +8,10 @@ require (
github.com/ghodss/yaml v1.0.0
github.com/go-bindata/go-bindata/v3 v3.1.3
github.com/google/cel-go v0.10.1
- github.com/google/go-cmp v0.5.6 // indirect
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.4.0
github.com/stretchr/testify v1.7.0
- google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368
+ google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac
k8s.io/api v0.24.0
k8s.io/apiextensions-apiserver v0.24.0
k8s.io/apimachinery v0.24.0
@@ -20,6 +19,14 @@ require (
sigs.k8s.io/controller-runtime v0.12.1
)
+require (
+ github.com/h2non/filetype v1.1.3
+ github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c
+ github.com/joelanford/ignore v0.0.0-20210610194209-63d4919d8fb2
+ github.com/operator-framework/operator-registry v1.26.2
+ sigs.k8s.io/yaml v1.3.0
+)
+
require (
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
@@ -29,6 +36,9 @@ require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.1 // 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-logr/logr v1.2.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
@@ -38,10 +48,11 @@ require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/gofuzz v1.1.0 // indirect
- github.com/google/uuid v1.1.2 // indirect
+ github.com/google/uuid v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kisielk/errcheck v1.5.0 // indirect
@@ -69,18 +80,19 @@ require (
go.opentelemetry.io/proto/otlp v0.7.0 // indirect
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
- golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
+ golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
- golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
+ golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
- golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717 // indirect
+ golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/grpc v1.40.0 // indirect
- google.golang.org/protobuf v1.27.1 // indirect
+ google.golang.org/grpc v1.45.0 // indirect
+ google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
+ gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/apiserver v0.24.0 // indirect
@@ -91,5 +103,4 @@ require (
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
- sigs.k8s.io/yaml v1.3.0 // indirect
)
diff --git a/go.sum b/go.sum
index c5357c982..ba6dec164 100644
--- a/go.sum
+++ b/go.sum
@@ -47,6 +47,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@@ -54,11 +56,13 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+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=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e h1:GCzyKMDDjSGnlpl3clrdAK7I1AaVoaiKDOYkUzChZzg=
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
@@ -95,7 +99,11 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
@@ -121,6 +129,7 @@ github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkg
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -128,11 +137,13 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -142,8 +153,17 @@ github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSy
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-bindata/go-bindata/v3 v3.1.3 h1:F0nVttLC3ws0ojc7p60veTurcOm//D4QBODNM7EGrCI=
github.com/go-bindata/go-bindata/v3 v3.1.3/go.mod h1:1/zrpXsLD8YDIbhZRqXzm1Ghc7NhEvIN9+Z6R5/xH4I=
+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-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/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw=
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=
@@ -228,7 +248,6 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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=
@@ -247,8 +266,9 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -261,6 +281,10 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
+github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
+github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c h1:fEE5/5VNnYUoBOj2I9TP8Jc+a7lge3QWn9DKE7NCwfc=
+github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c/go.mod h1:ObS/W+h8RYb1Y7fYivughjxojTmIu5iAIjSrSLCLeqE=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -289,6 +313,11 @@ 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/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+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.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
+github.com/joelanford/ignore v0.0.0-20210610194209-63d4919d8fb2 h1:MrNL6nf5H5dLVU3kRAPViSEDD3z/9hlbe674MeCH+IE=
+github.com/joelanford/ignore v0.0.0-20210610194209-63d4919d8fb2/go.mod h1:7HQupe4vyNxMKXmM5DFuwXHsqwMyglcYmZBtlDPIcZ8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -304,6 +333,7 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY=
@@ -315,6 +345,8 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -355,7 +387,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
@@ -370,11 +401,14 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/operator-framework/operator-registry v1.26.2 h1:kQToR/hPqdivljaRXM0olPllNIcc/GUk1VBoGwagJmk=
+github.com/operator-framework/operator-registry v1.26.2/go.mod h1:Z7XIb/3ZkhBQCvMD/rJphyuY4LmU/eWpZS+o0Mm1WAk=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -415,8 +449,10 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -454,6 +490,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -514,6 +551,7 @@ go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -521,6 +559,7 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/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-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -603,13 +642,15 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
+golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c=
+golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -652,6 +693,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -687,6 +729,7 @@ golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -698,8 +741,11 @@ golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
+golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -776,8 +822,9 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717 h1:hI3jKY4Hpf63ns040onEbB3dAkR/H/P83hw1TG8dD3Y=
golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
+golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
+golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
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=
@@ -857,8 +904,9 @@ google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 h1:Et6SkiuvnBn+SgrSYXs/BrUpGB4mbdwt4R3vaPIlicA=
google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac h1:qSNTkEN+L2mvWcLgJOR+8bdHX9rN/IdU3A1Ghpfb1Rg=
+google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -880,8 +928,9 @@ google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M=
+google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -894,14 +943,16 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
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.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
@@ -912,6 +963,8 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/pkg/lib/declcfg/declcfg.go b/pkg/lib/declcfg/declcfg.go
new file mode 100644
index 000000000..d575987b1
--- /dev/null
+++ b/pkg/lib/declcfg/declcfg.go
@@ -0,0 +1,109 @@
+package declcfg
+
+import (
+ "encoding/json"
+
+ "github.com/operator-framework/operator-registry/alpha/property"
+)
+
+const (
+ SchemaPackage = "olm.package"
+ SchemaChannel = "olm.channel"
+ SchemaBundle = "olm.bundle"
+)
+
+type DeclarativeConfig struct {
+ Packages []Package
+ Channels []Channel
+ Bundles []Bundle
+ Others []Meta
+}
+
+type Package struct {
+ Schema string `json:"schema"`
+ Name string `json:"name"`
+ DefaultChannel string `json:"defaultChannel"`
+ Icon *Icon `json:"icon,omitempty"`
+ Description string `json:"description,omitempty"`
+ Properties []property.Property `json:"properties,omitempty" hash:"set"`
+}
+
+type Icon struct {
+ Data []byte `json:"base64data"`
+ MediaType string `json:"mediatype"`
+}
+
+type Channel struct {
+ Schema string `json:"schema"`
+ Name string `json:"name"`
+ Package string `json:"package"`
+ Entries []ChannelEntry `json:"entries"`
+ Properties []property.Property `json:"properties,omitempty" hash:"set"`
+}
+
+type ChannelEntry struct {
+ Name string `json:"name"`
+ Replaces string `json:"replaces,omitempty"`
+ Skips []string `json:"skips,omitempty"`
+ SkipRange string `json:"skipRange,omitempty"`
+}
+
+// Bundle specifies all metadata and data of a bundle object.
+// Top-level fields are the source of truth, i.e. not CSV values.
+//
+// Notes:
+// - Any field slice type field or type containing a slice somewhere
+// where two types/fields are equal if their contents are equal regardless
+// of order must have a `hash:"set"` field tag for bundle comparison.
+// - Any fields that have a `json:"-"` tag must be included in the equality
+// evaluation in bundlesEqual().
+type Bundle struct {
+ Schema string `json:"schema"`
+ Name string `json:"name"`
+ Package string `json:"package"`
+ Image string `json:"image"`
+ Properties []property.Property `json:"properties,omitempty" hash:"set"`
+ RelatedImages []RelatedImage `json:"relatedImages,omitempty" hash:"set"`
+
+ // These fields are present so that we can continue serving
+ // the GRPC API the way packageserver expects us to in a
+ // backwards-compatible way. These are populated from
+ // any `olm.bundle.object` properties.
+ //
+ // These fields will never be persisted in the bundle blob as
+ // first class fields.
+ CsvJSON string `json:"-"`
+ Objects []string `json:"-"`
+}
+
+type RelatedImage struct {
+ Name string `json:"name"`
+ Image string `json:"image"`
+}
+
+type Meta struct {
+ Schema string
+ Package string
+
+ Blob json.RawMessage
+}
+
+func (m Meta) MarshalJSON() ([]byte, error) {
+ return m.Blob, nil
+}
+
+func (m *Meta) UnmarshalJSON(blob []byte) error {
+ type tmp struct {
+ Schema string `json:"schema"`
+ Package string `json:"package,omitempty"`
+ Properties []property.Property `json:"properties,omitempty"`
+ }
+ var t tmp
+ if err := json.Unmarshal(blob, &t); err != nil {
+ return err
+ }
+ m.Schema = t.Schema
+ m.Package = t.Package
+ m.Blob = blob
+ return nil
+}
diff --git a/pkg/lib/declcfg/declcfg_to_model.go b/pkg/lib/declcfg/declcfg_to_model.go
new file mode 100644
index 000000000..965229768
--- /dev/null
+++ b/pkg/lib/declcfg/declcfg_to_model.go
@@ -0,0 +1,187 @@
+package declcfg
+
+import (
+ "fmt"
+
+ "github.com/blang/semver/v4"
+ "k8s.io/apimachinery/pkg/util/sets"
+
+ "github.com/operator-framework/operator-registry/alpha/model"
+ "github.com/operator-framework/operator-registry/alpha/property"
+)
+
+func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) {
+ mpkgs := model.Model{}
+ defaultChannels := map[string]string{}
+ for _, p := range cfg.Packages {
+ if p.Name == "" {
+ return nil, fmt.Errorf("config contains package with no name")
+ }
+
+ if _, ok := mpkgs[p.Name]; ok {
+ return nil, fmt.Errorf("duplicate package %q", p.Name)
+ }
+
+ mpkg := &model.Package{
+ Name: p.Name,
+ Description: p.Description,
+ Channels: map[string]*model.Channel{},
+ }
+ if p.Icon != nil {
+ mpkg.Icon = &model.Icon{
+ Data: p.Icon.Data,
+ MediaType: p.Icon.MediaType,
+ }
+ }
+ defaultChannels[p.Name] = p.DefaultChannel
+ mpkgs[p.Name] = mpkg
+ }
+
+ channelDefinedEntries := map[string]sets.String{}
+ for _, c := range cfg.Channels {
+ mpkg, ok := mpkgs[c.Package]
+ if !ok {
+ return nil, fmt.Errorf("unknown package %q for channel %q", c.Package, c.Name)
+ }
+
+ if c.Name == "" {
+ return nil, fmt.Errorf("package %q contains channel with no name", c.Package)
+ }
+
+ if _, ok := mpkg.Channels[c.Name]; ok {
+ return nil, fmt.Errorf("package %q has duplicate channel %q", c.Package, c.Name)
+ }
+
+ mch := &model.Channel{
+ Package: mpkg,
+ Name: c.Name,
+ Bundles: map[string]*model.Bundle{},
+ // NOTICE: The field Properties of the type Channel is for internal use only.
+ // DO NOT use it for any public-facing functionalities.
+ // This API is in alpha stage and it is subject to change.
+ Properties: c.Properties,
+ }
+
+ cde := sets.NewString()
+ for _, entry := range c.Entries {
+ if _, ok := mch.Bundles[entry.Name]; ok {
+ return nil, fmt.Errorf("invalid package %q, channel %q: duplicate entry %q", c.Package, c.Name, entry.Name)
+ }
+ cde = cde.Insert(entry.Name)
+ mch.Bundles[entry.Name] = &model.Bundle{
+ Package: mpkg,
+ Channel: mch,
+ Name: entry.Name,
+ Replaces: entry.Replaces,
+ Skips: entry.Skips,
+ SkipRange: entry.SkipRange,
+ }
+ }
+ channelDefinedEntries[c.Package] = cde
+
+ mpkg.Channels[c.Name] = mch
+
+ defaultChannelName := defaultChannels[c.Package]
+ if defaultChannelName == c.Name {
+ mpkg.DefaultChannel = mch
+ }
+ }
+
+ // packageBundles tracks the set of bundle names for each package
+ // and is used to detect duplicate bundles.
+ packageBundles := map[string]sets.String{}
+
+ for _, b := range cfg.Bundles {
+ if b.Package == "" {
+ return nil, fmt.Errorf("package name must be set for bundle %q", b.Name)
+ }
+ mpkg, ok := mpkgs[b.Package]
+ if !ok {
+ return nil, fmt.Errorf("unknown package %q for bundle %q", b.Package, b.Name)
+ }
+
+ bundles, ok := packageBundles[b.Package]
+ if !ok {
+ bundles = sets.NewString()
+ }
+ if bundles.Has(b.Name) {
+ return nil, fmt.Errorf("package %q has duplicate bundle %q", b.Package, b.Name)
+ }
+ bundles.Insert(b.Name)
+ packageBundles[b.Package] = bundles
+
+ props, err := property.Parse(b.Properties)
+ if err != nil {
+ return nil, fmt.Errorf("parse properties for bundle %q: %v", b.Name, err)
+ }
+
+ if len(props.Packages) != 1 {
+ return nil, fmt.Errorf("package %q bundle %q must have exactly 1 %q property, found %d", b.Package, b.Name, property.TypePackage, len(props.Packages))
+ }
+
+ if b.Package != props.Packages[0].PackageName {
+ return nil, fmt.Errorf("package %q does not match %q property %q", b.Package, property.TypePackage, props.Packages[0].PackageName)
+ }
+
+ // Parse version from the package property.
+ rawVersion := props.Packages[0].Version
+ ver, err := semver.Parse(rawVersion)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing bundle %q version %q: %v", b.Name, rawVersion, err)
+ }
+
+ channelDefinedEntries[b.Package] = channelDefinedEntries[b.Package].Delete(b.Name)
+ found := false
+ for _, mch := range mpkg.Channels {
+ if mb, ok := mch.Bundles[b.Name]; ok {
+ found = true
+ mb.Image = b.Image
+ mb.Properties = b.Properties
+ mb.RelatedImages = relatedImagesToModelRelatedImages(b.RelatedImages)
+ mb.CsvJSON = b.CsvJSON
+ mb.Objects = b.Objects
+ mb.PropertiesP = props
+ mb.Version = ver
+ }
+ }
+ if !found {
+ return nil, fmt.Errorf("package %q, bundle %q not found in any channel entries", b.Package, b.Name)
+ }
+ }
+
+ for pkg, entries := range channelDefinedEntries {
+ if entries.Len() > 0 {
+ return nil, fmt.Errorf("no olm.bundle blobs found in package %q for olm.channel entries %s", pkg, entries.List())
+ }
+ }
+
+ for _, mpkg := range mpkgs {
+ defaultChannelName := defaultChannels[mpkg.Name]
+ if defaultChannelName != "" && mpkg.DefaultChannel == nil {
+ dch := &model.Channel{
+ Package: mpkg,
+ Name: defaultChannelName,
+ Bundles: map[string]*model.Bundle{},
+ }
+ mpkg.DefaultChannel = dch
+ mpkg.Channels[dch.Name] = dch
+ }
+ }
+
+ if err := mpkgs.Validate(); err != nil {
+ return nil, err
+ }
+ mpkgs.Normalize()
+ return mpkgs, nil
+}
+
+func relatedImagesToModelRelatedImages(in []RelatedImage) []model.RelatedImage {
+ var out []model.RelatedImage
+ for _, p := range in {
+ out = append(out, model.RelatedImage{
+ Name: p.Name,
+ Image: p.Image,
+ })
+ }
+ return out
+}
diff --git a/pkg/lib/declcfg/declcfg_to_model_test.go b/pkg/lib/declcfg/declcfg_to_model_test.go
new file mode 100644
index 000000000..b633c7cc3
--- /dev/null
+++ b/pkg/lib/declcfg/declcfg_to_model_test.go
@@ -0,0 +1,319 @@
+package declcfg
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/operator-framework/operator-registry/alpha/property"
+)
+
+func TestConvertToModel(t *testing.T) {
+ type spec struct {
+ name string
+ cfg DeclarativeConfig
+ assertion require.ErrorAssertionFunc
+ }
+
+ specs := []spec{
+ {
+ name: "Error/PackageNoName",
+ assertion: hasError(`config contains package with no name`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("", "alpha", svgSmallCircle)},
+ Bundles: []Bundle{{Name: "foo.v0.1.0"}},
+ },
+ },
+ {
+ name: "Error/BundleMissingPackageName",
+ assertion: hasError(`package name must be set for bundle "foo.v0.1.0"`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Bundles: []Bundle{{Name: "foo.v0.1.0"}},
+ },
+ },
+ {
+ name: "Error/BundleUnknownPackage",
+ assertion: hasError(`unknown package "bar" for bundle "bar.v0.1.0"`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Bundles: []Bundle{newTestBundle("bar", "0.1.0")},
+ },
+ },
+ {
+ name: "Error/BundleMissingChannel",
+ assertion: hasError(`package "foo", bundle "foo.v0.1.0" not found in any channel entries`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0")},
+ },
+ },
+ {
+ name: "Error/BundleInvalidProperties",
+ assertion: hasError(`parse properties for bundle "foo.v0.1.0": parse property[2] of type "olm.foo": unexpected end of JSON input`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0", func(b *Bundle) {
+ b.Properties = append(b.Properties, property.Property{
+ Type: "olm.foo",
+ Value: json.RawMessage("{"),
+ })
+ })},
+ },
+ },
+ {
+ name: "Error/BundlePackageMismatch",
+ assertion: hasError(`package "foo" does not match "olm.package" property "foooperator"`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0", func(b *Bundle) {
+ b.Properties = []property.Property{
+ property.MustBuildPackage("foooperator", "0.1.0"),
+ }
+ })},
+ },
+ },
+ {
+ name: "Error/BundleInvalidVersion",
+ assertion: hasError(`error parsing bundle "foo.v0.1.0" version "0.1.0.1": Invalid character(s) found in patch number "0.1"`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0", func(b *Bundle) {
+ b.Properties = []property.Property{
+ property.MustBuildPackage("foo", "0.1.0.1"),
+ }
+ })},
+ },
+ },
+ {
+ name: "Error/BundleMissingVersion",
+ assertion: hasError(`error parsing bundle "foo.v" version "": Version string empty`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Bundles: []Bundle{newTestBundle("foo", "", func(b *Bundle) {})},
+ },
+ },
+ {
+ name: "Error/PackageMissingDefaultChannel",
+ assertion: hasError(`invalid index:
+└── invalid package "foo":
+ └── default channel must be set`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "", svgSmallCircle)},
+ Channels: []Channel{newTestChannel("foo", "bar", ChannelEntry{Name: testBundleName("foo", "0.1.0")})},
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0")},
+ },
+ },
+ {
+ name: "Error/PackageNonExistentDefaultChannel",
+ assertion: hasError(`invalid index:
+└── invalid package "foo":
+ └── invalid channel "bar":
+ └── channel must contain at least one bundle`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "bar", svgSmallCircle)},
+ Channels: []Channel{newTestChannel("foo", "bar")},
+ },
+ },
+ {
+ name: "Error/BundleMissingPackageProperty",
+ assertion: hasError(`package "foo" bundle "foo.v0.1.0" must have exactly 1 "olm.package" property, found 0`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0", withNoProperties())},
+ },
+ },
+ {
+ name: "Error/BundleMultiplePackageProperty",
+ assertion: hasError(`package "foo" bundle "foo.v0.1.0" must have exactly 1 "olm.package" property, found 2`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0", func(b *Bundle) {
+ b.Properties = []property.Property{
+ property.MustBuildPackage("foo", "0.1.0"),
+ property.MustBuildPackage("foo", "0.1.0"),
+ }
+ })},
+ },
+ },
+ {
+ name: "Success/BundleWithDataButMissingImage",
+ assertion: require.NoError,
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: testBundleName("foo", "0.1.0")})},
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0", withNoBundleImage())},
+ },
+ },
+ {
+ name: "Error/ChannelEntryWithoutBundle",
+ assertion: hasError(`no olm.bundle blobs found in package "foo" for olm.channel entries [foo.v0.1.0]`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: "foo.v0.1.0"})},
+ },
+ },
+ {
+ name: "Error/BundleWithoutChannelEntry",
+ assertion: hasError(`package "foo", bundle "foo.v0.2.0" not found in any channel entries`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: "foo.v0.1.0"})},
+ Bundles: []Bundle{newTestBundle("foo", "0.2.0")},
+ },
+ },
+ {
+ name: "Error/ChannelMissingName",
+ assertion: hasError(`package "foo" contains channel with no name`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Channels: []Channel{newTestChannel("foo", "", ChannelEntry{Name: "foo.v0.1.0"})},
+ Bundles: []Bundle{newTestBundle("foo", "0.2.0")},
+ },
+ },
+ {
+ name: "Error/ChannelMissingPackageName",
+ assertion: hasError(`unknown package "" for channel "alpha"`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Channels: []Channel{newTestChannel("", "alpha", ChannelEntry{Name: "foo.v0.1.0"})},
+ Bundles: []Bundle{newTestBundle("foo", "0.2.0")},
+ },
+ },
+ {
+ name: "Error/ChannelNonExistentPackage",
+ assertion: hasError(`unknown package "non-existent" for channel "alpha"`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Channels: []Channel{newTestChannel("non-existent", "alpha", ChannelEntry{Name: "foo.v0.1.0"})},
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0")},
+ },
+ },
+ {
+ name: "Error/ChannelDuplicateEntry",
+ assertion: hasError(`invalid package "foo", channel "alpha": duplicate entry "foo.v0.1.0"`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Channels: []Channel{newTestChannel("foo", "alpha",
+ ChannelEntry{Name: "foo.v0.1.0"},
+ ChannelEntry{Name: "foo.v0.1.0"},
+ )},
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0")},
+ },
+ },
+ {
+ name: "Error/DuplicatePackage",
+ assertion: hasError(`duplicate package "foo"`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{
+ newTestPackage("foo", "alpha", svgSmallCircle),
+ newTestPackage("foo", "alpha", svgSmallCircle),
+ },
+ Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: "foo.v0.1.0"})},
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0")},
+ },
+ },
+ {
+ name: "Error/DuplicateChannel",
+ assertion: hasError(`package "foo" has duplicate channel "alpha"`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Channels: []Channel{
+ newTestChannel("foo", "alpha", ChannelEntry{Name: "foo.v0.1.0"}),
+ newTestChannel("foo", "alpha", ChannelEntry{Name: "foo.v0.1.0"}),
+ },
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0")},
+ },
+ },
+ {
+ name: "Error/DuplicateBundle",
+ assertion: hasError(`package "foo" has duplicate bundle "foo.v0.1.0"`),
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: "foo.v0.1.0"})},
+ Bundles: []Bundle{
+ newTestBundle("foo", "0.1.0"),
+ newTestBundle("foo", "0.1.0"),
+ },
+ },
+ },
+ {
+ name: "Success/ValidModel",
+ assertion: require.NoError,
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: "foo.v0.1.0"})},
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0")},
+ },
+ },
+ {
+ name: "Success/ValidModelWithChannelProperties",
+ assertion: require.NoError,
+ cfg: DeclarativeConfig{
+ Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
+ Channels: []Channel{
+ addChannelProperties(
+ newTestChannel("foo", "alpha", ChannelEntry{Name: "foo.v0.1.0"}),
+ []property.Property{
+ {Type: "user", Value: json.RawMessage("{\"group\":\"xyz.com\",\"name\":\"account\"}")},
+ },
+ ),
+ },
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0")},
+ },
+ },
+ {
+ name: "Success/ValidModelWithPackageProperties",
+ assertion: require.NoError,
+ cfg: DeclarativeConfig{
+ Packages: []Package{
+ addPackageProperties(
+ newTestPackage("foo", "alpha", svgSmallCircle),
+ []property.Property{
+ {Type: "owner", Value: json.RawMessage("{\"group\":\"abc.com\",\"name\":\"admin\"}")},
+ },
+ ),
+ },
+ Channels: []Channel{newTestChannel("foo", "alpha", ChannelEntry{Name: "foo.v0.1.0"})},
+ Bundles: []Bundle{newTestBundle("foo", "0.1.0")},
+ },
+ },
+ }
+
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ _, err := ConvertToModel(s.cfg)
+ s.assertion(t, err)
+ })
+ }
+}
+
+func TestConvertToModelRoundtrip(t *testing.T) {
+ expected := buildValidDeclarativeConfig(true)
+
+ m, err := ConvertToModel(expected)
+ require.NoError(t, err)
+ actual := ConvertFromModel(m)
+
+ removeJSONWhitespace(&expected)
+ removeJSONWhitespace(&actual)
+
+ assert.Equal(t, expected.Packages, actual.Packages)
+ assert.Equal(t, expected.Bundles, actual.Bundles)
+ assert.Len(t, actual.Others, 0, "expected unrecognized schemas not to make the roundtrip")
+}
+
+func hasError(expectedError string) require.ErrorAssertionFunc {
+ return func(t require.TestingT, actualError error, args ...interface{}) {
+ if stdt, ok := t.(*testing.T); ok {
+ stdt.Helper()
+ }
+ if actualError != nil && actualError.Error() == expectedError {
+ return
+ }
+ t.Errorf("expected error to be `%s`, got `%s`", expectedError, actualError)
+ t.FailNow()
+ }
+}
diff --git a/pkg/lib/declcfg/helpers_test.go b/pkg/lib/declcfg/helpers_test.go
new file mode 100644
index 000000000..cfeb41983
--- /dev/null
+++ b/pkg/lib/declcfg/helpers_test.go
@@ -0,0 +1,325 @@
+package declcfg
+
+import (
+ "encoding/json"
+ "fmt"
+ "path/filepath"
+ "sort"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/operator-framework/operator-registry/alpha/model"
+ "github.com/operator-framework/operator-registry/alpha/property"
+)
+
+func buildValidDeclarativeConfig(includeUnrecognized bool) DeclarativeConfig {
+ a001 := newTestBundle("anakin", "0.0.1")
+ a010 := newTestBundle("anakin", "0.1.0")
+ a011 := newTestBundle("anakin", "0.1.1")
+ b1 := newTestBundle("boba-fett", "1.0.0")
+ b2 := newTestBundle("boba-fett", "2.0.0")
+
+ var others []Meta
+ if includeUnrecognized {
+ others = []Meta{
+ {Schema: "custom.1", Blob: json.RawMessage(`{"schema": "custom.1"}`)},
+ {Schema: "custom.2", Blob: json.RawMessage(`{"schema": "custom.2"}`)},
+ {Schema: "custom.3", Package: "anakin", Blob: json.RawMessage(`{
+ "myField": "foobar",
+ "package": "anakin",
+ "schema": "custom.3"
+ }`)},
+ {Schema: "custom.3", Package: "boba-fett", Blob: json.RawMessage(`{
+ "myField": "foobar",
+ "package": "boba-fett",
+ "schema": "custom.3"
+ }`)},
+ }
+ }
+
+ return DeclarativeConfig{
+ Packages: []Package{
+ newTestPackage("anakin", "dark", svgSmallCircle),
+ newTestPackage("boba-fett", "mando", svgBigCircle),
+ },
+ Channels: []Channel{
+ newTestChannel("anakin", "dark",
+ ChannelEntry{
+ Name: testBundleName("anakin", "0.0.1"),
+ },
+ ChannelEntry{
+ Name: testBundleName("anakin", "0.1.0"),
+ Replaces: testBundleName("anakin", "0.0.1"),
+ },
+ ChannelEntry{
+ Name: testBundleName("anakin", "0.1.1"),
+ Replaces: testBundleName("anakin", "0.0.1"),
+ Skips: []string{testBundleName("anakin", "0.1.0")},
+ },
+ ),
+ newTestChannel("anakin", "light",
+ ChannelEntry{
+ Name: testBundleName("anakin", "0.0.1"),
+ },
+ ChannelEntry{
+ Name: testBundleName("anakin", "0.1.0"),
+ Replaces: testBundleName("anakin", "0.0.1"),
+ },
+ ),
+ newTestChannel("boba-fett", "mando",
+ ChannelEntry{
+ Name: testBundleName("boba-fett", "1.0.0"),
+ },
+ ChannelEntry{
+ Name: testBundleName("boba-fett", "2.0.0"),
+ Replaces: testBundleName("boba-fett", "1.0.0"),
+ },
+ ),
+ },
+ Bundles: []Bundle{
+ a001, a010, a011,
+ b1, b2,
+ },
+ Others: others,
+ }
+}
+
+type bundleOpt func(*Bundle)
+
+func withNoProperties() func(*Bundle) {
+ return func(b *Bundle) {
+ b.Properties = []property.Property{}
+ }
+}
+
+func withNoBundleImage() func(*Bundle) {
+ return func(b *Bundle) {
+ b.Image = ""
+ }
+}
+
+func withNoBundleData() func(*Bundle) {
+ return func(b *Bundle) {
+ b.Objects = []string{}
+ b.CsvJSON = ""
+ }
+}
+
+func newTestBundle(packageName, version string, opts ...bundleOpt) Bundle {
+ csvJson := fmt.Sprintf(`{"kind": "ClusterServiceVersion", "apiVersion": "operators.coreos.com/v1alpha1", "metadata":{"name":%q}}`, testBundleName(packageName, version))
+ b := Bundle{
+ Schema: SchemaBundle,
+ Name: testBundleName(packageName, version),
+ Package: packageName,
+ Image: testBundleImage(packageName, version),
+ Properties: []property.Property{
+ property.MustBuildPackage(packageName, version),
+ property.MustBuildBundleObjectRef(filepath.Join("objects", testBundleName(packageName, version)+".csv.yaml")),
+ property.MustBuildBundleObjectData([]byte(`{"kind": "CustomResourceDefinition", "apiVersion": "apiextensions.k8s.io/v1"}`)),
+ },
+ RelatedImages: []RelatedImage{
+ {
+ Name: "bundle",
+ Image: testBundleImage(packageName, version),
+ },
+ },
+ CsvJSON: csvJson,
+ Objects: []string{
+ csvJson,
+ `{"kind": "CustomResourceDefinition", "apiVersion": "apiextensions.k8s.io/v1"}`,
+ },
+ }
+ for _, opt := range opts {
+ opt(&b)
+ }
+ sort.Slice(b.Properties, func(i, j int) bool {
+ if b.Properties[i].Type != b.Properties[j].Type {
+ return b.Properties[i].Type < b.Properties[j].Type
+ }
+ return string(b.Properties[i].Value) < string(b.Properties[j].Value)
+ })
+ return b
+}
+
+const (
+ svgSmallCircle = ``
+ svgBigCircle = ``
+)
+
+func newTestPackage(packageName, defaultChannel, svgData string) Package {
+ p := Package{
+ Schema: SchemaPackage,
+ Name: packageName,
+ DefaultChannel: defaultChannel,
+ Icon: &Icon{Data: []byte(svgData), MediaType: "image/svg+xml"},
+ Description: testPackageDescription(packageName),
+ }
+ return p
+}
+
+func addPackageProperties(in Package, p []property.Property) Package {
+ in.Properties = p
+ return in
+}
+
+func newTestChannel(packageName, channelName string, entries ...ChannelEntry) Channel {
+ return Channel{
+ Schema: SchemaChannel,
+ Name: channelName,
+ Package: packageName,
+ Entries: entries,
+ }
+}
+
+func addChannelProperties(in Channel, p []property.Property) Channel {
+ in.Properties = p
+ return in
+}
+
+func buildTestModel() model.Model {
+ return model.Model{
+ "anakin": buildAnakinPkgModel(),
+ "boba-fett": buildBobaFettPkgModel(),
+ }
+}
+
+func getBundle(pkg *model.Package, ch *model.Channel, version, replaces string, skips ...string) *model.Bundle {
+ return &model.Bundle{
+ Package: pkg,
+ Channel: ch,
+ Name: testBundleName(pkg.Name, version),
+ Image: testBundleImage(pkg.Name, version),
+ Properties: []property.Property{
+ property.MustBuildPackage(pkg.Name, version),
+ property.MustBuildBundleObjectRef(filepath.Join("objects", testBundleName(pkg.Name, version)+".csv.yaml")),
+ property.MustBuildBundleObjectData([]byte(getCRDJSON())),
+ },
+ Replaces: replaces,
+ Skips: skips,
+ RelatedImages: []model.RelatedImage{{
+ Name: "bundle",
+ Image: testBundleImage(pkg.Name, version),
+ }},
+ CsvJSON: getCSVJson(pkg.Name, version),
+ Objects: []string{
+ getCSVJson(pkg.Name, version),
+ getCRDJSON(),
+ },
+ }
+}
+
+func getCSVJson(pkgName, version string) string {
+ return fmt.Sprintf(`{"kind": "ClusterServiceVersion", "apiVersion": "operators.coreos.com/v1alpha1", "metadata":{"name":%q}}`, testBundleName(pkgName, version))
+}
+
+func getCRDJSON() string {
+ return `{"kind": "CustomResourceDefinition", "apiVersion": "apiextensions.k8s.io/v1"}`
+}
+
+func buildAnakinPkgModel() *model.Package {
+ pkgName := "anakin"
+
+ pkg := &model.Package{
+ Name: pkgName,
+ Description: testPackageDescription(pkgName),
+ Icon: &model.Icon{
+ Data: []byte(svgSmallCircle),
+ MediaType: "image/svg+xml",
+ },
+ Channels: map[string]*model.Channel{},
+ }
+
+ light := &model.Channel{
+ Package: pkg,
+ Name: "light",
+ Bundles: map[string]*model.Bundle{},
+ }
+
+ dark := &model.Channel{
+ Package: pkg,
+ Name: "dark",
+ Bundles: map[string]*model.Bundle{},
+ }
+ light.Bundles[testBundleName(pkgName, "0.0.1")] = getBundle(pkg, light, "0.0.1", "")
+ light.Bundles[testBundleName(pkgName, "0.1.0")] = getBundle(pkg, light, "0.1.0", testBundleName(pkgName, "0.0.1"))
+
+ dark.Bundles[testBundleName(pkgName, "0.0.1")] = getBundle(pkg, dark, "0.0.1", "")
+ dark.Bundles[testBundleName(pkgName, "0.1.0")] = getBundle(pkg, dark, "0.1.0", testBundleName(pkgName, "0.0.1"))
+ dark.Bundles[testBundleName(pkgName, "0.1.1")] = getBundle(pkg, dark, "0.1.1", testBundleName(pkgName, "0.0.1"), testBundleName(pkgName, "0.1.0"))
+
+ pkg.Channels["light"] = light
+ pkg.Channels["dark"] = dark
+ pkg.DefaultChannel = pkg.Channels["dark"]
+ return pkg
+}
+
+func buildBobaFettPkgModel() *model.Package {
+ pkgName := "boba-fett"
+ pkg := &model.Package{
+ Name: pkgName,
+ Description: testPackageDescription(pkgName),
+ Icon: &model.Icon{
+ Data: []byte(svgBigCircle),
+ MediaType: "image/svg+xml",
+ },
+ Channels: map[string]*model.Channel{},
+ }
+ mando := &model.Channel{
+ Package: pkg,
+ Name: "mando",
+ Bundles: map[string]*model.Bundle{},
+ }
+ mando.Bundles[testBundleName(pkgName, "1.0.0")] = getBundle(pkg, mando, "1.0.0", "")
+ mando.Bundles[testBundleName(pkgName, "2.0.0")] = getBundle(pkg, mando, "2.0.0", testBundleName(pkgName, "1.0.0"))
+ pkg.Channels["mando"] = mando
+ pkg.DefaultChannel = mando
+ return pkg
+}
+
+func testPackageDescription(pkg string) string {
+ return fmt.Sprintf("%s operator", pkg)
+}
+
+func testBundleName(pkg, version string) string {
+ return fmt.Sprintf("%s.v%s", pkg, version)
+}
+
+func testBundleImage(pkg, version string) string {
+ return fmt.Sprintf("%s-bundle:v%s", pkg, version)
+}
+
+func equalsDeclarativeConfig(t *testing.T, expected, actual DeclarativeConfig) {
+ t.Helper()
+ removeJSONWhitespace(&expected)
+ removeJSONWhitespace(&actual)
+
+ assert.ElementsMatch(t, expected.Packages, actual.Packages)
+ assert.ElementsMatch(t, expected.Others, actual.Others)
+
+ // When comparing bundles, the order of properties doesn't matter.
+ // Unfortunately, assert.ElementsMatch() only ignores ordering of
+ // root elements, so we need to manually sort bundles and use
+ // assert.ElementsMatch on the properties fields between
+ // expected and actual.
+ require.Equal(t, len(expected.Bundles), len(actual.Bundles))
+ sort.SliceStable(expected.Bundles, func(i, j int) bool {
+ return expected.Bundles[i].Name < expected.Bundles[j].Name
+ })
+ sort.SliceStable(actual.Bundles, func(i, j int) bool {
+ return actual.Bundles[i].Name < actual.Bundles[j].Name
+ })
+ for i := range expected.Bundles {
+ assert.ElementsMatch(t, expected.Bundles[i].Properties, actual.Bundles[i].Properties)
+ expected.Bundles[i].Properties, actual.Bundles[i].Properties = nil, nil
+ assert.Equal(t, expected.Bundles[i], actual.Bundles[i])
+ }
+
+ // In case new fields are added to the DeclarativeConfig struct in the future,
+ // test that the rest is Equal.
+ expected.Packages, actual.Packages = nil, nil
+ expected.Bundles, actual.Bundles = nil, nil
+ expected.Others, actual.Others = nil, nil
+ assert.Equal(t, expected, actual)
+}
diff --git a/pkg/lib/declcfg/load.go b/pkg/lib/declcfg/load.go
new file mode 100644
index 000000000..65c289780
--- /dev/null
+++ b/pkg/lib/declcfg/load.go
@@ -0,0 +1,188 @@
+package declcfg
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "path/filepath"
+ "strings"
+
+ "github.com/joelanford/ignore"
+ "github.com/operator-framework/api/pkg/operators"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/util/yaml"
+
+ "github.com/operator-framework/operator-registry/alpha/property"
+)
+
+const (
+ indexIgnoreFilename = ".indexignore"
+)
+
+type WalkFunc func(path string, cfg *DeclarativeConfig, err error) error
+
+// WalkFS walks root using a gitignore-style filename matcher to skip files
+// that match patterns found in .indexignore files found throughout the filesystem.
+// It calls walkFn for each declarative config file it finds. If WalkFS encounters
+// an error loading or parsing any file, the error will be immediately returned.
+func WalkFS(root fs.FS, walkFn WalkFunc) error {
+ if root == nil {
+ return fmt.Errorf("no declarative config filesystem provided")
+ }
+
+ matcher, err := ignore.NewMatcher(root, indexIgnoreFilename)
+ if err != nil {
+ return err
+ }
+
+ return fs.WalkDir(root, ".", func(path string, info fs.DirEntry, err error) error {
+ if err != nil {
+ return walkFn(path, nil, err)
+ }
+ // avoid validating a directory, an .indexignore file, or any file that matches
+ // an ignore pattern outlined in a .indexignore file.
+ if info.IsDir() || info.Name() == indexIgnoreFilename || matcher.Match(path, false) {
+ return nil
+ }
+
+ cfg, err := LoadFile(root, path)
+ if err != nil {
+ return walkFn(path, cfg, err)
+ }
+
+ return walkFn(path, cfg, err)
+ })
+}
+
+// LoadFS loads a declarative config from the provided root FS. LoadFS walks the
+// filesystem from root and uses a gitignore-style filename matcher to skip files
+// that match patterns found in .indexignore files found throughout the filesystem.
+// If LoadFS encounters an error loading or parsing any file, the error will be
+// immediately returned.
+func LoadFS(root fs.FS) (*DeclarativeConfig, error) {
+ cfg := &DeclarativeConfig{}
+ if err := WalkFS(root, func(path string, fcfg *DeclarativeConfig, err error) error {
+ if err != nil {
+ return err
+ }
+ cfg.Packages = append(cfg.Packages, fcfg.Packages...)
+ cfg.Channels = append(cfg.Channels, fcfg.Channels...)
+ cfg.Bundles = append(cfg.Bundles, fcfg.Bundles...)
+ cfg.Others = append(cfg.Others, fcfg.Others...)
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+ return cfg, nil
+}
+
+func readBundleObjects(bundles []Bundle, root fs.FS, path string) error {
+ for bi, b := range bundles {
+ props, err := property.Parse(b.Properties)
+ if err != nil {
+ return fmt.Errorf("package %q, bundle %q: parse properties: %v", b.Package, b.Name, err)
+ }
+ for oi, obj := range props.BundleObjects {
+ objID := fmt.Sprintf(" %q", obj.GetRef())
+ if !obj.IsRef() {
+ objID = fmt.Sprintf("[%d]", oi)
+ }
+
+ d, err := obj.GetData(root, filepath.Dir(path))
+ if err != nil {
+ return fmt.Errorf("package %q, bundle %q: get data for bundle object%s: %v", b.Package, b.Name, objID, err)
+ }
+ objJson, err := yaml.ToJSON(d)
+ if err != nil {
+ return fmt.Errorf("package %q, bundle %q: convert object%s to JSON: %v", b.Package, b.Name, objID, err)
+ }
+ bundles[bi].Objects = append(bundles[bi].Objects, string(objJson))
+ }
+ bundles[bi].CsvJSON = extractCSV(bundles[bi].Objects)
+ }
+ return nil
+}
+
+func extractCSV(objs []string) string {
+ for _, obj := range objs {
+ u := unstructured.Unstructured{}
+ if err := yaml.Unmarshal([]byte(obj), &u); err != nil {
+ continue
+ }
+ if u.GetKind() == operators.ClusterServiceVersionKind {
+ return obj
+ }
+ }
+ return ""
+}
+
+// LoadReader reads yaml or json from the passed in io.Reader and unmarshals it into a DeclarativeConfig struct.
+// Path references will not be de-referenced so callers are responsible for de-referencing if necessary.
+func LoadReader(r io.Reader) (*DeclarativeConfig, error) {
+ cfg := &DeclarativeConfig{}
+ dec := yaml.NewYAMLOrJSONDecoder(r, 4096)
+ for {
+ doc := json.RawMessage{}
+ if err := dec.Decode(&doc); err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ return nil, err
+ }
+ doc = []byte(strings.NewReplacer(`\u003c`, "<", `\u003e`, ">", `\u0026`, "&").Replace(string(doc)))
+
+ var in Meta
+ if err := json.Unmarshal(doc, &in); err != nil {
+ return nil, err
+ }
+
+ switch in.Schema {
+ case SchemaPackage:
+ var p Package
+ if err := json.Unmarshal(doc, &p); err != nil {
+ return nil, fmt.Errorf("parse package: %v", err)
+ }
+ cfg.Packages = append(cfg.Packages, p)
+ case SchemaChannel:
+ var c Channel
+ if err := json.Unmarshal(doc, &c); err != nil {
+ return nil, fmt.Errorf("parse channel: %v", err)
+ }
+ cfg.Channels = append(cfg.Channels, c)
+ case SchemaBundle:
+ var b Bundle
+ if err := json.Unmarshal(doc, &b); err != nil {
+ return nil, fmt.Errorf("parse bundle: %v", err)
+ }
+ cfg.Bundles = append(cfg.Bundles, b)
+ case "":
+ return nil, fmt.Errorf("object '%s' is missing root schema field", string(doc))
+ default:
+ cfg.Others = append(cfg.Others, in)
+ }
+ }
+ return cfg, nil
+}
+
+// LoadFile will unmarshall declarative config components from a single filename provided in 'path'
+// located at a filesystem hierarchy 'root'
+func LoadFile(root fs.FS, path string) (*DeclarativeConfig, error) {
+ file, err := root.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ cfg, err := LoadReader(file)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := readBundleObjects(cfg.Bundles, root, path); err != nil {
+ return nil, fmt.Errorf("read bundle objects: %v", err)
+ }
+
+ return cfg, nil
+}
diff --git a/pkg/lib/declcfg/load_test.go b/pkg/lib/declcfg/load_test.go
new file mode 100644
index 000000000..301002f16
--- /dev/null
+++ b/pkg/lib/declcfg/load_test.go
@@ -0,0 +1,908 @@
+package declcfg
+
+import (
+ "encoding/json"
+ "io/fs"
+ "os"
+ "testing"
+ "testing/fstest"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "k8s.io/apimachinery/pkg/util/yaml"
+
+ "github.com/operator-framework/operator-registry/alpha/property"
+)
+
+func TestLoadReader(t *testing.T) {
+ type spec struct {
+ name string
+ fsys fs.FS
+ path string
+ assertion require.ErrorAssertionFunc
+ expectNumPackages int
+ expectNumBundles int
+ expectNumOthers int
+ }
+ specs := []spec{
+ {
+ name: "Error/NotYAMLOrJSON",
+ fsys: invalidFS,
+ path: "invalid-format.txt",
+ assertion: require.Error,
+ },
+ {
+ name: "Error/NotJSONObject",
+ fsys: invalidFS,
+ path: "not-object.json",
+ assertion: require.Error,
+ },
+ {
+ name: "Error/NoSchema",
+ fsys: invalidFS,
+ path: "no-schema.yaml",
+ assertion: require.Error,
+ },
+ {
+ name: "Error/InvalidPackageJSON",
+ fsys: invalidFS,
+ path: "invalid-package.json",
+ assertion: require.Error,
+ },
+ {
+ name: "Error/InvalidBundleJSON",
+ fsys: invalidFS,
+ path: "invalid-bundle.json",
+ assertion: require.Error,
+ },
+ {
+ name: "Success/UnrecognizedSchema",
+ fsys: validFS,
+ path: "unrecognized-schema.json",
+ assertion: require.NoError,
+ expectNumPackages: 1,
+ expectNumBundles: 1,
+ expectNumOthers: 1,
+ },
+ {
+ name: "Success/ValidFile",
+ fsys: validFS,
+ path: "etcd.yaml",
+ assertion: require.NoError,
+ expectNumPackages: 1,
+ expectNumBundles: 6,
+ expectNumOthers: 0,
+ },
+ }
+
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ f, err := s.fsys.Open(s.path)
+ require.NoError(t, err)
+
+ cfg, err := LoadReader(f)
+ s.assertion(t, err)
+ if err == nil {
+ require.NotNil(t, cfg)
+ assert.Equal(t, len(cfg.Packages), s.expectNumPackages, "unexpected package count")
+ assert.Equal(t, len(cfg.Bundles), s.expectNumBundles, "unexpected bundle count")
+ assert.Equal(t, len(cfg.Others), s.expectNumOthers, "unexpected others count")
+ }
+ })
+ }
+}
+
+func TestLoadFS(t *testing.T) {
+ type spec struct {
+ name string
+ fsys fs.FS
+ assertion require.ErrorAssertionFunc
+ expected *DeclarativeConfig
+ }
+ specs := []spec{
+ {
+ name: "Error/NilFS",
+ fsys: nil,
+ assertion: require.Error,
+ },
+ {
+ name: "Error/NonExistentDir",
+ fsys: os.DirFS("non/existent/dir/"),
+ assertion: require.Error,
+ },
+ {
+ name: "Error/Invalid",
+ fsys: invalidFS,
+ assertion: require.Error,
+ },
+ {
+ name: "Success/ValidDir",
+ fsys: validFS,
+ assertion: require.NoError,
+ expected: &DeclarativeConfig{
+ Packages: []Package{
+ {Schema: "olm.package", Name: "cockroachdb", DefaultChannel: "stable-5.x", Icon: &Icon{Data: []uint8{0x3c, 0x73, 0x76, 0x67, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x32, 0x30, 0x30, 0x30, 0x2f, 0x73, 0x76, 0x67, 0x22, 0x20, 0x76, 0x69, 0x65, 0x77, 0x42, 0x6f, 0x78, 0x3d, 0x22, 0x30, 0x20, 0x30, 0x20, 0x33, 0x31, 0x2e, 0x38, 0x32, 0x20, 0x33, 0x32, 0x22, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3d, 0x22, 0x32, 0x34, 0x38, 0x36, 0x22, 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3d, 0x22, 0x32, 0x35, 0x30, 0x30, 0x22, 0x3e, 0x3c, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x3e, 0x43, 0x4c, 0x3c, 0x2f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x3e, 0x3c, 0x70, 0x61, 0x74, 0x68, 0x20, 0x64, 0x3d, 0x22, 0x4d, 0x31, 0x39, 0x2e, 0x34, 0x32, 0x20, 0x39, 0x2e, 0x31, 0x37, 0x61, 0x31, 0x35, 0x2e, 0x33, 0x39, 0x20, 0x31, 0x35, 0x2e, 0x33, 0x39, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x2d, 0x33, 0x2e, 0x35, 0x31, 0x2e, 0x34, 0x20, 0x31, 0x35, 0x2e, 0x34, 0x36, 0x20, 0x31, 0x35, 0x2e, 0x34, 0x36, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x2d, 0x33, 0x2e, 0x35, 0x31, 0x2d, 0x2e, 0x34, 0x20, 0x31, 0x35, 0x2e, 0x36, 0x33, 0x20, 0x31, 0x35, 0x2e, 0x36, 0x33, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x20, 0x33, 0x2e, 0x35, 0x31, 0x2d, 0x33, 0x2e, 0x39, 0x31, 0x20, 0x31, 0x35, 0x2e, 0x37, 0x31, 0x20, 0x31, 0x35, 0x2e, 0x37, 0x31, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x20, 0x33, 0x2e, 0x35, 0x31, 0x20, 0x33, 0x2e, 0x39, 0x31, 0x7a, 0x4d, 0x33, 0x30, 0x20, 0x2e, 0x35, 0x37, 0x41, 0x31, 0x37, 0x2e, 0x32, 0x32, 0x20, 0x31, 0x37, 0x2e, 0x32, 0x32, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x32, 0x35, 0x2e, 0x35, 0x39, 0x20, 0x30, 0x61, 0x31, 0x37, 0x2e, 0x34, 0x20, 0x31, 0x37, 0x2e, 0x34, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x2d, 0x39, 0x2e, 0x36, 0x38, 0x20, 0x32, 0x2e, 0x39, 0x33, 0x41, 0x31, 0x37, 0x2e, 0x33, 0x38, 0x20, 0x31, 0x37, 0x2e, 0x33, 0x38, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x36, 0x2e, 0x32, 0x33, 0x20, 0x30, 0x61, 0x31, 0x37, 0x2e, 0x32, 0x32, 0x20, 0x31, 0x37, 0x2e, 0x32, 0x32, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x2d, 0x34, 0x2e, 0x34, 0x34, 0x2e, 0x35, 0x37, 0x41, 0x31, 0x36, 0x2e, 0x32, 0x32, 0x20, 0x31, 0x36, 0x2e, 0x32, 0x32, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x2e, 0x31, 0x33, 0x61, 0x2e, 0x30, 0x37, 0x2e, 0x30, 0x37, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x2e, 0x30, 0x39, 0x20, 0x31, 0x37, 0x2e, 0x33, 0x32, 0x20, 0x31, 0x37, 0x2e, 0x33, 0x32, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x2e, 0x38, 0x33, 0x20, 0x31, 0x2e, 0x35, 0x37, 0x2e, 0x30, 0x37, 0x2e, 0x30, 0x37, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x2e, 0x30, 0x38, 0x20, 0x30, 0x20, 0x31, 0x36, 0x2e, 0x33, 0x39, 0x20, 0x31, 0x36, 0x2e, 0x33, 0x39, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x20, 0x31, 0x2e, 0x38, 0x31, 0x2d, 0x2e, 0x35, 0x34, 0x20, 0x31, 0x35, 0x2e, 0x36, 0x35, 0x20, 0x31, 0x35, 0x2e, 0x36, 0x35, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x20, 0x31, 0x31, 0x2e, 0x35, 0x39, 0x20, 0x31, 0x2e, 0x38, 0x38, 0x20, 0x31, 0x37, 0x2e, 0x35, 0x32, 0x20, 0x31, 0x37, 0x2e, 0x35, 0x32, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x2d, 0x33, 0x2e, 0x37, 0x38, 0x20, 0x34, 0x2e, 0x34, 0x38, 0x63, 0x2d, 0x2e, 0x32, 0x2e, 0x33, 0x32, 0x2d, 0x2e, 0x33, 0x37, 0x2e, 0x36, 0x35, 0x2d, 0x2e, 0x35, 0x35, 0x20, 0x31, 0x73, 0x2d, 0x2e, 0x32, 0x32, 0x2e, 0x34, 0x35, 0x2d, 0x2e, 0x33, 0x33, 0x2e, 0x36, 0x39, 0x2d, 0x2e, 0x33, 0x31, 0x2e, 0x37, 0x32, 0x2d, 0x2e, 0x34, 0x34, 0x20, 0x31, 0x2e, 0x30, 0x38, 0x61, 0x31, 0x37, 0x2e, 0x34, 0x36, 0x20, 0x31, 0x37, 0x2e, 0x34, 0x36, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x34, 0x2e, 0x32, 0x39, 0x20, 0x31, 0x38, 0x2e, 0x37, 0x63, 0x2e, 0x32, 0x36, 0x2e, 0x32, 0x35, 0x2e, 0x35, 0x33, 0x2e, 0x34, 0x39, 0x2e, 0x38, 0x31, 0x2e, 0x37, 0x33, 0x73, 0x2e, 0x34, 0x34, 0x2e, 0x33, 0x37, 0x2e, 0x36, 0x37, 0x2e, 0x35, 0x34, 0x2e, 0x35, 0x39, 0x2e, 0x34, 0x34, 0x2e, 0x38, 0x39, 0x2e, 0x36, 0x34, 0x61, 0x2e, 0x30, 0x37, 0x2e, 0x30, 0x37, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x2e, 0x30, 0x38, 0x20, 0x30, 0x63, 0x2e, 0x33, 0x2d, 0x2e, 0x32, 0x31, 0x2e, 0x36, 0x2d, 0x2e, 0x34, 0x32, 0x2e, 0x38, 0x39, 0x2d, 0x2e, 0x36, 0x34, 0x73, 0x2e, 0x34, 0x35, 0x2d, 0x2e, 0x33, 0x35, 0x2e, 0x36, 0x37, 0x2d, 0x2e, 0x35, 0x34, 0x2e, 0x35, 0x35, 0x2d, 0x2e, 0x34, 0x38, 0x2e, 0x38, 0x31, 0x2d, 0x2e, 0x37, 0x33, 0x61, 0x31, 0x37, 0x2e, 0x34, 0x35, 0x20, 0x31, 0x37, 0x2e, 0x34, 0x35, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x35, 0x2e, 0x33, 0x38, 0x2d, 0x31, 0x32, 0x2e, 0x36, 0x31, 0x20, 0x31, 0x37, 0x2e, 0x33, 0x39, 0x20, 0x31, 0x37, 0x2e, 0x33, 0x39, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x2d, 0x31, 0x2e, 0x30, 0x39, 0x2d, 0x36, 0x2e, 0x30, 0x39, 0x63, 0x2d, 0x2e, 0x31, 0x34, 0x2d, 0x2e, 0x33, 0x37, 0x2d, 0x2e, 0x32, 0x39, 0x2d, 0x2e, 0x37, 0x33, 0x2d, 0x2e, 0x34, 0x35, 0x2d, 0x31, 0x2e, 0x30, 0x39, 0x73, 0x2d, 0x2e, 0x32, 0x32, 0x2d, 0x2e, 0x34, 0x37, 0x2d, 0x2e, 0x33, 0x33, 0x2d, 0x2e, 0x36, 0x39, 0x2d, 0x2e, 0x33, 0x35, 0x2d, 0x2e, 0x36, 0x36, 0x2d, 0x2e, 0x35, 0x35, 0x2d, 0x31, 0x61, 0x31, 0x37, 0x2e, 0x36, 0x31, 0x20, 0x31, 0x37, 0x2e, 0x36, 0x31, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x2d, 0x33, 0x2e, 0x37, 0x38, 0x2d, 0x34, 0x2e, 0x34, 0x38, 0x20, 0x31, 0x35, 0x2e, 0x36, 0x35, 0x20, 0x31, 0x35, 0x2e, 0x36, 0x35, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x20, 0x31, 0x31, 0x2e, 0x36, 0x2d, 0x31, 0x2e, 0x38, 0x34, 0x20, 0x31, 0x36, 0x2e, 0x31, 0x33, 0x20, 0x31, 0x36, 0x2e, 0x31, 0x33, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x20, 0x31, 0x2e, 0x38, 0x31, 0x2e, 0x35, 0x34, 0x2e, 0x30, 0x37, 0x2e, 0x30, 0x37, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x2e, 0x30, 0x38, 0x20, 0x30, 0x71, 0x2e, 0x34, 0x34, 0x2d, 0x2e, 0x37, 0x36, 0x2e, 0x38, 0x32, 0x2d, 0x31, 0x2e, 0x35, 0x36, 0x61, 0x2e, 0x30, 0x37, 0x2e, 0x30, 0x37, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x2d, 0x2e, 0x30, 0x39, 0x41, 0x31, 0x36, 0x2e, 0x38, 0x39, 0x20, 0x31, 0x36, 0x2e, 0x38, 0x39, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x33, 0x30, 0x20, 0x2e, 0x35, 0x37, 0x7a, 0x22, 0x20, 0x66, 0x69, 0x6c, 0x6c, 0x3d, 0x22, 0x23, 0x31, 0x35, 0x31, 0x66, 0x33, 0x34, 0x22, 0x2f, 0x3e, 0x3c, 0x70, 0x61, 0x74, 0x68, 0x20, 0x64, 0x3d, 0x22, 0x4d, 0x32, 0x31, 0x2e, 0x38, 0x32, 0x20, 0x31, 0x37, 0x2e, 0x34, 0x37, 0x61, 0x31, 0x35, 0x2e, 0x35, 0x31, 0x20, 0x31, 0x35, 0x2e, 0x35, 0x31, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x2d, 0x34, 0x2e, 0x32, 0x35, 0x20, 0x31, 0x30, 0x2e, 0x36, 0x39, 0x20, 0x31, 0x35, 0x2e, 0x36, 0x36, 0x20, 0x31, 0x35, 0x2e, 0x36, 0x36, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x2d, 0x2e, 0x37, 0x32, 0x2d, 0x34, 0x2e, 0x36, 0x38, 0x20, 0x31, 0x35, 0x2e, 0x35, 0x20, 0x31, 0x35, 0x2e, 0x35, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x20, 0x34, 0x2e, 0x32, 0x35, 0x2d, 0x31, 0x30, 0x2e, 0x36, 0x39, 0x20, 0x31, 0x35, 0x2e, 0x36, 0x32, 0x20, 0x31, 0x35, 0x2e, 0x36, 0x32, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x20, 0x2e, 0x37, 0x32, 0x20, 0x34, 0x2e, 0x36, 0x38, 0x22, 0x20, 0x66, 0x69, 0x6c, 0x6c, 0x3d, 0x22, 0x23, 0x33, 0x34, 0x38, 0x35, 0x34, 0x30, 0x22, 0x2f, 0x3e, 0x3c, 0x70, 0x61, 0x74, 0x68, 0x20, 0x64, 0x3d, 0x22, 0x4d, 0x31, 0x35, 0x20, 0x32, 0x33, 0x2e, 0x34, 0x38, 0x61, 0x31, 0x35, 0x2e, 0x35, 0x35, 0x20, 0x31, 0x35, 0x2e, 0x35, 0x35, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x2d, 0x2e, 0x37, 0x32, 0x20, 0x34, 0x2e, 0x36, 0x38, 0x20, 0x31, 0x35, 0x2e, 0x35, 0x34, 0x20, 0x31, 0x35, 0x2e, 0x35, 0x34, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x2d, 0x33, 0x2e, 0x35, 0x33, 0x2d, 0x31, 0x35, 0x2e, 0x33, 0x37, 0x41, 0x31, 0x35, 0x2e, 0x35, 0x20, 0x31, 0x35, 0x2e, 0x35, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x20, 0x31, 0x35, 0x20, 0x32, 0x33, 0x2e, 0x34, 0x38, 0x22, 0x20, 0x66, 0x69, 0x6c, 0x6c, 0x3d, 0x22, 0x23, 0x37, 0x64, 0x62, 0x63, 0x34, 0x32, 0x22, 0x2f, 0x3e, 0x3c, 0x2f, 0x73, 0x76, 0x67, 0x3e}, MediaType: "image/svg+xml"}, Description: ""},
+ {Schema: "olm.package", Name: "etcd", DefaultChannel: "singlenamespace-alpha", Icon: &Icon{Data: []uint8{0x3c, 0x73, 0x76, 0x67, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3d, 0x22, 0x32, 0x35, 0x30, 0x30, 0x22, 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3d, 0x22, 0x32, 0x34, 0x32, 0x32, 0x22, 0x20, 0x76, 0x69, 0x65, 0x77, 0x42, 0x6f, 0x78, 0x3d, 0x22, 0x30, 0x20, 0x30, 0x20, 0x32, 0x35, 0x36, 0x20, 0x32, 0x34, 0x38, 0x22, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x32, 0x30, 0x30, 0x30, 0x2f, 0x73, 0x76, 0x67, 0x22, 0x20, 0x70, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x41, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x3d, 0x22, 0x78, 0x4d, 0x69, 0x64, 0x59, 0x4d, 0x69, 0x64, 0x22, 0x3e, 0x3c, 0x70, 0x61, 0x74, 0x68, 0x20, 0x64, 0x3d, 0x22, 0x4d, 0x32, 0x35, 0x32, 0x2e, 0x33, 0x38, 0x36, 0x20, 0x31, 0x32, 0x38, 0x2e, 0x30, 0x36, 0x34, 0x63, 0x2d, 0x31, 0x2e, 0x32, 0x30, 0x32, 0x2e, 0x31, 0x2d, 0x32, 0x2e, 0x34, 0x31, 0x2e, 0x31, 0x34, 0x37, 0x2d, 0x33, 0x2e, 0x36, 0x39, 0x33, 0x2e, 0x31, 0x34, 0x37, 0x2d, 0x37, 0x2e, 0x34, 0x34, 0x36, 0x20, 0x30, 0x2d, 0x31, 0x34, 0x2e, 0x36, 0x37, 0x2d, 0x31, 0x2e, 0x37, 0x34, 0x36, 0x2d, 0x32, 0x31, 0x2e, 0x31, 0x38, 0x37, 0x2d, 0x34, 0x2e, 0x39, 0x34, 0x34, 0x20, 0x32, 0x2e, 0x31, 0x37, 0x2d, 0x31, 0x32, 0x2e, 0x34, 0x34, 0x37, 0x20, 0x33, 0x2e, 0x30, 0x39, 0x32, 0x2d, 0x32, 0x34, 0x2e, 0x39, 0x38, 0x37, 0x20, 0x32, 0x2e, 0x38, 0x35, 0x2d, 0x33, 0x37, 0x2e, 0x34, 0x38, 0x31, 0x2d, 0x37, 0x2e, 0x30, 0x36, 0x35, 0x2d, 0x31, 0x30, 0x2e, 0x32, 0x32, 0x2d, 0x31, 0x35, 0x2e, 0x31, 0x34, 0x2d, 0x31, 0x39, 0x2e, 0x38, 0x36, 0x33, 0x2d, 0x32, 0x34, 0x2e, 0x32, 0x35, 0x36, 0x2d, 0x32, 0x38, 0x2e, 0x37, 0x34, 0x37, 0x20, 0x33, 0x2e, 0x39, 0x35, 0x35, 0x2d, 0x37, 0x2e, 0x34, 0x31, 0x35, 0x20, 0x39, 0x2e, 0x38, 0x30, 0x31, 0x2d, 0x31, 0x33, 0x2e, 0x37, 0x39, 0x35, 0x20, 0x31, 0x37, 0x2e, 0x31, 0x2d, 0x31, 0x38, 0x2e, 0x33, 0x31, 0x39, 0x6c, 0x33, 0x2e, 0x31, 0x33, 0x33, 0x2d, 0x31, 0x2e, 0x39, 0x33, 0x37, 0x2d, 0x32, 0x2e, 0x34, 0x34, 0x32, 0x2d, 0x32, 0x2e, 0x37, 0x35, 0x34, 0x63, 0x2d, 0x31, 0x32, 0x2e, 0x35, 0x38, 0x31, 0x2d, 0x31, 0x34, 0x2e, 0x31, 0x36, 0x37, 0x2d, 0x32, 0x37, 0x2e, 0x35, 0x39, 0x36, 0x2d, 0x32, 0x35, 0x2e, 0x31, 0x32, 0x2d, 0x34, 0x34, 0x2e, 0x36, 0x32, 0x2d, 0x33, 0x32, 0x2e, 0x35, 0x35, 0x32, 0x4c, 0x31, 0x37, 0x35, 0x2e, 0x38, 0x37, 0x36, 0x20, 0x30, 0x6c, 0x2d, 0x2e, 0x38, 0x36, 0x32, 0x20, 0x33, 0x2e, 0x35, 0x38, 0x38, 0x63, 0x2d, 0x32, 0x2e, 0x30, 0x33, 0x20, 0x38, 0x2e, 0x33, 0x36, 0x33, 0x2d, 0x36, 0x2e, 0x32, 0x37, 0x34, 0x20, 0x31, 0x35, 0x2e, 0x39, 0x30, 0x38, 0x2d, 0x31, 0x32, 0x2e, 0x31, 0x20, 0x32, 0x31, 0x2e, 0x39, 0x36, 0x32, 0x61, 0x31, 0x39, 0x33, 0x2e, 0x38, 0x34, 0x32, 0x20, 0x31, 0x39, 0x33, 0x2e, 0x38, 0x34, 0x32, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x2d, 0x33, 0x34, 0x2e, 0x39, 0x35, 0x36, 0x2d, 0x31, 0x34, 0x2e, 0x34, 0x30, 0x35, 0x41, 0x31, 0x39, 0x34, 0x2e, 0x30, 0x31, 0x32, 0x20, 0x31, 0x39, 0x34, 0x2e, 0x30, 0x31, 0x32, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x39, 0x33, 0x2e, 0x30, 0x35, 0x36, 0x20, 0x32, 0x35, 0x2e, 0x35, 0x32, 0x43, 0x38, 0x37, 0x2e, 0x32, 0x35, 0x34, 0x20, 0x31, 0x39, 0x2e, 0x34, 0x37, 0x33, 0x20, 0x38, 0x33, 0x2e, 0x30, 0x32, 0x20, 0x31, 0x31, 0x2e, 0x39, 0x34, 0x37, 0x20, 0x38, 0x30, 0x2e, 0x39, 0x39, 0x39, 0x20, 0x33, 0x2e, 0x36, 0x30, 0x38, 0x4c, 0x38, 0x30, 0x2e, 0x31, 0x33, 0x2e, 0x30, 0x32, 0x6c, 0x2d, 0x33, 0x2e, 0x33, 0x38, 0x32, 0x20, 0x31, 0x2e, 0x34, 0x37, 0x43, 0x35, 0x39, 0x2e, 0x39, 0x33, 0x39, 0x20, 0x38, 0x2e, 0x38, 0x31, 0x35, 0x20, 0x34, 0x34, 0x2e, 0x35, 0x31, 0x20, 0x32, 0x30, 0x2e, 0x30, 0x36, 0x35, 0x20, 0x33, 0x32, 0x2e, 0x31, 0x33, 0x35, 0x20, 0x33, 0x34, 0x2e, 0x30, 0x32, 0x6c, 0x2d, 0x32, 0x2e, 0x34, 0x34, 0x39, 0x20, 0x32, 0x2e, 0x37, 0x36, 0x20, 0x33, 0x2e, 0x31, 0x33, 0x20, 0x31, 0x2e, 0x39, 0x33, 0x37, 0x63, 0x37, 0x2e, 0x32, 0x37, 0x36, 0x20, 0x34, 0x2e, 0x35, 0x30, 0x36, 0x20, 0x31, 0x33, 0x2e, 0x31, 0x30, 0x36, 0x20, 0x31, 0x30, 0x2e, 0x38, 0x34, 0x39, 0x20, 0x31, 0x37, 0x2e, 0x30, 0x35, 0x34, 0x20, 0x31, 0x38, 0x2e, 0x32, 0x32, 0x33, 0x2d, 0x39, 0x2e, 0x30, 0x38, 0x38, 0x20, 0x38, 0x2e, 0x38, 0x35, 0x2d, 0x31, 0x37, 0x2e, 0x31, 0x35, 0x34, 0x20, 0x31, 0x38, 0x2e, 0x34, 0x36, 0x32, 0x2d, 0x32, 0x34, 0x2e, 0x32, 0x31, 0x34, 0x20, 0x32, 0x38, 0x2e, 0x36, 0x33, 0x35, 0x2d, 0x2e, 0x32, 0x37, 0x35, 0x20, 0x31, 0x32, 0x2e, 0x34, 0x38, 0x39, 0x2e, 0x36, 0x20, 0x32, 0x35, 0x2e, 0x31, 0x32, 0x20, 0x32, 0x2e, 0x37, 0x38, 0x20, 0x33, 0x37, 0x2e, 0x37, 0x34, 0x2d, 0x36, 0x2e, 0x34, 0x38, 0x34, 0x20, 0x33, 0x2e, 0x31, 0x36, 0x37, 0x2d, 0x31, 0x33, 0x2e, 0x36, 0x36, 0x38, 0x20, 0x34, 0x2e, 0x38, 0x39, 0x34, 0x2d, 0x32, 0x31, 0x2e, 0x30, 0x36, 0x35, 0x20, 0x34, 0x2e, 0x38, 0x39, 0x34, 0x2d, 0x31, 0x2e, 0x32, 0x39, 0x38, 0x20, 0x30, 0x2d, 0x32, 0x2e, 0x35, 0x31, 0x33, 0x2d, 0x2e, 0x30, 0x34, 0x37, 0x2d, 0x33, 0x2e, 0x36, 0x39, 0x33, 0x2d, 0x2e, 0x31, 0x34, 0x35, 0x4c, 0x30, 0x20, 0x31, 0x32, 0x37, 0x2e, 0x37, 0x38, 0x35, 0x6c, 0x2e, 0x33, 0x34, 0x35, 0x20, 0x33, 0x2e, 0x36, 0x37, 0x31, 0x63, 0x31, 0x2e, 0x38, 0x30, 0x32, 0x20, 0x31, 0x38, 0x2e, 0x35, 0x37, 0x38, 0x20, 0x37, 0x2e, 0x35, 0x37, 0x20, 0x33, 0x36, 0x2e, 0x32, 0x34, 0x37, 0x20, 0x31, 0x37, 0x2e, 0x31, 0x35, 0x34, 0x20, 0x35, 0x32, 0x2e, 0x35, 0x32, 0x33, 0x6c, 0x31, 0x2e, 0x38, 0x37, 0x20, 0x33, 0x2e, 0x31, 0x37, 0x36, 0x20, 0x32, 0x2e, 0x38, 0x31, 0x2d, 0x32, 0x2e, 0x33, 0x38, 0x34, 0x61, 0x34, 0x38, 0x2e, 0x30, 0x34, 0x20, 0x34, 0x38, 0x2e, 0x30, 0x34, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x20, 0x32, 0x32, 0x2e, 0x37, 0x33, 0x37, 0x2d, 0x31, 0x30, 0x2e, 0x36, 0x35, 0x20, 0x31, 0x39, 0x34, 0x2e, 0x38, 0x36, 0x20, 0x31, 0x39, 0x34, 0x2e, 0x38, 0x36, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x39, 0x2e, 0x34, 0x36, 0x20, 0x33, 0x31, 0x2e, 0x36, 0x39, 0x36, 0x63, 0x31, 0x31, 0x2e, 0x38, 0x32, 0x38, 0x20, 0x34, 0x2e, 0x31, 0x33, 0x37, 0x20, 0x32, 0x34, 0x2e, 0x31, 0x35, 0x31, 0x20, 0x37, 0x2e, 0x32, 0x32, 0x35, 0x20, 0x33, 0x36, 0x2e, 0x38, 0x37, 0x38, 0x20, 0x39, 0x2e, 0x30, 0x36, 0x33, 0x20, 0x31, 0x2e, 0x32, 0x32, 0x20, 0x38, 0x2e, 0x34, 0x31, 0x37, 0x2e, 0x32, 0x34, 0x38, 0x20, 0x31, 0x37, 0x2e, 0x31, 0x32, 0x32, 0x2d, 0x33, 0x2e, 0x30, 0x37, 0x32, 0x20, 0x32, 0x35, 0x2e, 0x31, 0x37, 0x31, 0x6c, 0x2d, 0x31, 0x2e, 0x34, 0x20, 0x33, 0x2e, 0x34, 0x31, 0x31, 0x20, 0x33, 0x2e, 0x36, 0x2e, 0x37, 0x39, 0x33, 0x63, 0x39, 0x2e, 0x32, 0x32, 0x20, 0x32, 0x2e, 0x30, 0x32, 0x37, 0x20, 0x31, 0x38, 0x2e, 0x35, 0x32, 0x33, 0x20, 0x33, 0x2e, 0x30, 0x36, 0x20, 0x32, 0x37, 0x2e, 0x36, 0x33, 0x31, 0x20, 0x33, 0x2e, 0x30, 0x36, 0x6c, 0x32, 0x37, 0x2e, 0x36, 0x32, 0x33, 0x2d, 0x33, 0x2e, 0x30, 0x36, 0x20, 0x33, 0x2e, 0x36, 0x30, 0x34, 0x2d, 0x2e, 0x37, 0x39, 0x33, 0x2d, 0x31, 0x2e, 0x34, 0x30, 0x33, 0x2d, 0x33, 0x2e, 0x34, 0x31, 0x37, 0x63, 0x2d, 0x33, 0x2e, 0x33, 0x31, 0x32, 0x2d, 0x38, 0x2e, 0x30, 0x35, 0x2d, 0x34, 0x2e, 0x32, 0x38, 0x34, 0x2d, 0x31, 0x36, 0x2e, 0x37, 0x36, 0x35, 0x2d, 0x33, 0x2e, 0x30, 0x36, 0x33, 0x2d, 0x32, 0x35, 0x2e, 0x31, 0x38, 0x33, 0x20, 0x31, 0x32, 0x2e, 0x36, 0x37, 0x36, 0x2d, 0x31, 0x2e, 0x38, 0x34, 0x20, 0x32, 0x34, 0x2e, 0x39, 0x35, 0x34, 0x2d, 0x34, 0x2e, 0x39, 0x32, 0x20, 0x33, 0x36, 0x2e, 0x37, 0x33, 0x38, 0x2d, 0x39, 0x2e, 0x30, 0x34, 0x35, 0x61, 0x31, 0x39, 0x35, 0x2e, 0x31, 0x30, 0x38, 0x20, 0x31, 0x39, 0x35, 0x2e, 0x31, 0x30, 0x38, 0x20, 0x30, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x39, 0x2e, 0x34, 0x38, 0x32, 0x2d, 0x33, 0x31, 0x2e, 0x37, 0x32, 0x36, 0x20, 0x34, 0x38, 0x2e, 0x32, 0x35, 0x34, 0x20, 0x34, 0x38, 0x2e, 0x32, 0x35, 0x34, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x20, 0x32, 0x32, 0x2e, 0x38, 0x34, 0x38, 0x20, 0x31, 0x30, 0x2e, 0x36, 0x36, 0x6c, 0x32, 0x2e, 0x38, 0x30, 0x39, 0x20, 0x32, 0x2e, 0x33, 0x38, 0x20, 0x31, 0x2e, 0x38, 0x36, 0x32, 0x2d, 0x33, 0x2e, 0x31, 0x36, 0x38, 0x63, 0x39, 0x2e, 0x36, 0x2d, 0x31, 0x36, 0x2e, 0x32, 0x39, 0x37, 0x20, 0x31, 0x35, 0x2e, 0x33, 0x36, 0x38, 0x2d, 0x33, 0x33, 0x2e, 0x39, 0x36, 0x35, 0x20, 0x31, 0x37, 0x2e, 0x31, 0x34, 0x32, 0x2d, 0x35, 0x32, 0x2e, 0x35, 0x31, 0x33, 0x6c, 0x2e, 0x33, 0x34, 0x35, 0x2d, 0x33, 0x2e, 0x36, 0x36, 0x35, 0x2d, 0x33, 0x2e, 0x36, 0x31, 0x34, 0x2e, 0x32, 0x37, 0x39, 0x7a, 0x4d, 0x31, 0x36, 0x37, 0x2e, 0x34, 0x39, 0x20, 0x31, 0x37, 0x32, 0x2e, 0x39, 0x36, 0x63, 0x2d, 0x31, 0x33, 0x2e, 0x30, 0x36, 0x38, 0x20, 0x33, 0x2e, 0x35, 0x35, 0x34, 0x2d, 0x32, 0x36, 0x2e, 0x33, 0x34, 0x20, 0x35, 0x2e, 0x33, 0x34, 0x38, 0x2d, 0x33, 0x39, 0x2e, 0x35, 0x33, 0x32, 0x20, 0x35, 0x2e, 0x33, 0x34, 0x38, 0x2d, 0x31, 0x33, 0x2e, 0x32, 0x32, 0x38, 0x20, 0x30, 0x2d, 0x32, 0x36, 0x2e, 0x34, 0x38, 0x33, 0x2d, 0x31, 0x2e, 0x37, 0x39, 0x33, 0x2d, 0x33, 0x39, 0x2e, 0x35, 0x36, 0x33, 0x2d, 0x35, 0x2e, 0x33, 0x34, 0x38, 0x61, 0x31, 0x35, 0x33, 0x2e, 0x32, 0x35, 0x35, 0x20, 0x31, 0x35, 0x33, 0x2e, 0x32, 0x35, 0x35, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x2d, 0x31, 0x36, 0x2e, 0x39, 0x33, 0x32, 0x2d, 0x33, 0x35, 0x2e, 0x36, 0x37, 0x63, 0x2d, 0x34, 0x2e, 0x30, 0x36, 0x36, 0x2d, 0x31, 0x32, 0x2e, 0x35, 0x31, 0x37, 0x2d, 0x36, 0x2e, 0x34, 0x34, 0x35, 0x2d, 0x32, 0x35, 0x2e, 0x36, 0x33, 0x2d, 0x37, 0x2e, 0x31, 0x33, 0x35, 0x2d, 0x33, 0x39, 0x2e, 0x31, 0x33, 0x34, 0x20, 0x38, 0x2e, 0x34, 0x34, 0x36, 0x2d, 0x31, 0x30, 0x2e, 0x34, 0x34, 0x33, 0x20, 0x31, 0x38, 0x2e, 0x30, 0x35, 0x32, 0x2d, 0x31, 0x39, 0x2e, 0x35, 0x39, 0x31, 0x20, 0x32, 0x38, 0x2e, 0x36, 0x36, 0x35, 0x2d, 0x32, 0x37, 0x2e, 0x32, 0x39, 0x33, 0x61, 0x31, 0x35, 0x32, 0x2e, 0x36, 0x32, 0x20, 0x31, 0x35, 0x32, 0x2e, 0x36, 0x32, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x20, 0x33, 0x34, 0x2e, 0x39, 0x36, 0x35, 0x2d, 0x31, 0x39, 0x2e, 0x30, 0x31, 0x31, 0x20, 0x31, 0x35, 0x33, 0x2e, 0x32, 0x34, 0x32, 0x20, 0x31, 0x35, 0x33, 0x2e, 0x32, 0x34, 0x32, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x20, 0x33, 0x34, 0x2e, 0x38, 0x39, 0x38, 0x20, 0x31, 0x38, 0x2e, 0x39, 0x37, 0x63, 0x31, 0x30, 0x2e, 0x36, 0x35, 0x34, 0x20, 0x37, 0x2e, 0x37, 0x34, 0x33, 0x20, 0x32, 0x30, 0x2e, 0x33, 0x30, 0x32, 0x20, 0x31, 0x36, 0x2e, 0x39, 0x36, 0x32, 0x20, 0x32, 0x38, 0x2e, 0x37, 0x39, 0x20, 0x32, 0x37, 0x2e, 0x34, 0x37, 0x2d, 0x2e, 0x37, 0x32, 0x34, 0x20, 0x31, 0x33, 0x2e, 0x34, 0x32, 0x37, 0x2d, 0x33, 0x2e, 0x31, 0x33, 0x32, 0x20, 0x32, 0x36, 0x2e, 0x34, 0x36, 0x35, 0x2d, 0x37, 0x2e, 0x32, 0x30, 0x34, 0x20, 0x33, 0x38, 0x2e, 0x39, 0x36, 0x31, 0x61, 0x31, 0x35, 0x32, 0x2e, 0x37, 0x36, 0x37, 0x20, 0x31, 0x35, 0x32, 0x2e, 0x37, 0x36, 0x37, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x2d, 0x31, 0x36, 0x2e, 0x39, 0x35, 0x32, 0x20, 0x33, 0x35, 0x2e, 0x37, 0x30, 0x37, 0x7a, 0x6d, 0x2d, 0x32, 0x38, 0x2e, 0x37, 0x34, 0x2d, 0x36, 0x32, 0x2e, 0x39, 0x39, 0x38, 0x63, 0x30, 0x20, 0x39, 0x2e, 0x32, 0x33, 0x32, 0x20, 0x37, 0x2e, 0x34, 0x38, 0x32, 0x20, 0x31, 0x36, 0x2e, 0x37, 0x20, 0x31, 0x36, 0x2e, 0x37, 0x30, 0x32, 0x20, 0x31, 0x36, 0x2e, 0x37, 0x20, 0x39, 0x2e, 0x32, 0x31, 0x37, 0x20, 0x30, 0x20, 0x31, 0x36, 0x2e, 0x36, 0x39, 0x2d, 0x37, 0x2e, 0x34, 0x36, 0x36, 0x20, 0x31, 0x36, 0x2e, 0x36, 0x39, 0x2d, 0x31, 0x36, 0x2e, 0x37, 0x20, 0x30, 0x2d, 0x39, 0x2e, 0x31, 0x39, 0x36, 0x2d, 0x37, 0x2e, 0x34, 0x37, 0x33, 0x2d, 0x31, 0x36, 0x2e, 0x36, 0x39, 0x32, 0x2d, 0x31, 0x36, 0x2e, 0x36, 0x39, 0x2d, 0x31, 0x36, 0x2e, 0x36, 0x39, 0x32, 0x2d, 0x39, 0x2e, 0x32, 0x32, 0x20, 0x30, 0x2d, 0x31, 0x36, 0x2e, 0x37, 0x30, 0x31, 0x20, 0x37, 0x2e, 0x34, 0x39, 0x36, 0x2d, 0x31, 0x36, 0x2e, 0x37, 0x30, 0x31, 0x20, 0x31, 0x36, 0x2e, 0x36, 0x39, 0x32, 0x7a, 0x6d, 0x2d, 0x32, 0x31, 0x2e, 0x35, 0x37, 0x38, 0x20, 0x30, 0x63, 0x30, 0x20, 0x39, 0x2e, 0x32, 0x33, 0x32, 0x2d, 0x37, 0x2e, 0x34, 0x38, 0x20, 0x31, 0x36, 0x2e, 0x37, 0x2d, 0x31, 0x36, 0x2e, 0x37, 0x20, 0x31, 0x36, 0x2e, 0x37, 0x2d, 0x39, 0x2e, 0x32, 0x32, 0x36, 0x20, 0x30, 0x2d, 0x31, 0x36, 0x2e, 0x36, 0x38, 0x35, 0x2d, 0x37, 0x2e, 0x34, 0x36, 0x36, 0x2d, 0x31, 0x36, 0x2e, 0x36, 0x38, 0x35, 0x2d, 0x31, 0x36, 0x2e, 0x37, 0x20, 0x30, 0x2d, 0x39, 0x2e, 0x31, 0x39, 0x33, 0x20, 0x37, 0x2e, 0x34, 0x36, 0x2d, 0x31, 0x36, 0x2e, 0x36, 0x38, 0x39, 0x20, 0x31, 0x36, 0x2e, 0x36, 0x38, 0x36, 0x2d, 0x31, 0x36, 0x2e, 0x36, 0x38, 0x39, 0x20, 0x39, 0x2e, 0x32, 0x32, 0x20, 0x30, 0x20, 0x31, 0x36, 0x2e, 0x37, 0x20, 0x37, 0x2e, 0x34, 0x39, 0x36, 0x20, 0x31, 0x36, 0x2e, 0x37, 0x20, 0x31, 0x36, 0x2e, 0x36, 0x39, 0x7a, 0x22, 0x20, 0x66, 0x69, 0x6c, 0x6c, 0x3d, 0x22, 0x23, 0x34, 0x31, 0x39, 0x45, 0x44, 0x41, 0x22, 0x2f, 0x3e, 0x3c, 0x2f, 0x73, 0x76, 0x67, 0x3e, 0xa}, MediaType: "image/svg+xml"}, Description: "A message about etcd operator, a description of channels"},
+ {Schema: "olm.package", Name: "", DefaultChannel: "", Icon: nil, Description: ""},
+ },
+ Bundles: []Bundle{
+ {
+ Schema: "olm.bundle",
+ Name: "cockroachdb.v2.0.9",
+ Package: "cockroachdb",
+ Image: "quay.io/openshift-community-operators/cockroachdb:v2.0.9",
+ Properties: []property.Property{
+ {Type: "olm.channel", Value: json.RawMessage(`{"name":"stable"}`)},
+ {Type: "olm.package", Value: json.RawMessage(`{"packageName":"cockroachdb","version":"2.0.9"}`)},
+ },
+ },
+ {
+ Schema: "olm.bundle",
+ Name: "cockroachdb.v2.1.11",
+ Package: "cockroachdb",
+ Image: "quay.io/openshift-community-operators/cockroachdb:v2.1.11",
+ Properties: []property.Property{
+ {Type: "olm.channel", Value: json.RawMessage(`{"name":"stable","replaces":"cockroachdb.v2.1.1"}`)},
+ {Type: "olm.package", Value: json.RawMessage(`{"packageName":"cockroachdb","version":"2.1.11"}`)},
+ },
+ },
+ {
+ Schema: "olm.bundle",
+ Name: "cockroachdb.v2.1.1",
+ Package: "cockroachdb",
+ Image: "quay.io/openshift-community-operators/cockroachdb:v2.1.1",
+ Properties: []property.Property{
+ {Type: "olm.channel", Value: json.RawMessage(`{"name":"stable","replaces":"cockroachdb.v2.0.9"}`)},
+ {Type: "olm.package", Value: json.RawMessage(`{"packageName":"cockroachdb","version":"2.1.1"}`)},
+ },
+ },
+ {
+ Schema: "olm.bundle",
+ Name: "cockroachdb.v3.0.7",
+ Package: "cockroachdb",
+ Image: "quay.io/openshift-community-operators/cockroachdb:v3.0.7",
+ Properties: []property.Property{
+ {Type: "olm.channel", Value: json.RawMessage(`{"name":"stable-3.x"}`)},
+ {Type: "olm.package", Value: json.RawMessage(`{"packageName":"cockroachdb","version":"3.0.7"}`)},
+ },
+ },
+ {
+ Schema: "olm.bundle",
+ Name: "cockroachdb.v5.0.3",
+ Package: "cockroachdb",
+ Image: "quay.io/openshift-community-operators/cockroachdb:v5.0.3",
+ Properties: []property.Property{
+ {Type: "olm.channel", Value: json.RawMessage(`{"name":"stable-5.x"}`)},
+ {Type: "olm.package", Value: json.RawMessage(`{"packageName":"cockroachdb","version":"5.0.3"}`)},
+ },
+ },
+ {
+ Schema: "olm.bundle",
+ Name: "etcdoperator-community.v0.6.1",
+ Package: "etcd",
+ Image: "quay.io/operatorhubio/etcd:v0.6.1",
+ Properties: []property.Property{
+ {Type: "olm.package", Value: json.RawMessage(`{"packageName":"etcd","version":"0.6.1"}`)},
+ {Type: "olm.gvk", Value: json.RawMessage(`{"group":"etcd.database.coreos.com","kind":"EtcdCluster","version":"v1beta2"}`)},
+ {Type: "olm.channel", Value: json.RawMessage(`{"name":"alpha"}`)},
+ {Type: "olm.skipRange", Value: json.RawMessage(`"<0.6.1"`)},
+ {Type: "olm.bundle.object", Value: json.RawMessage(`{"ref":"etcdoperator.v0.6.1.clusterserviceversion.yaml"}`)},
+ },
+ RelatedImages: []RelatedImage{{Name: "etcdv0.6.1", Image: "quay.io/coreos/etcd-operator@sha256:bd944a211eaf8f31da5e6d69e8541e7cada8f16a9f7a5a570b22478997819943"}},
+ Objects: []string{toJSON(t, etcdCSV.Data)},
+ CsvJSON: toJSON(t, etcdCSV.Data),
+ },
+ {
+ Schema: "olm.bundle",
+ Name: "etcdoperator.v0.9.0",
+ Package: "etcd",
+ Image: "quay.io/operatorhubio/etcd:v0.9.0",
+ Properties: []property.Property{
+ {Type: "olm.package", Value: json.RawMessage(`{"packageName":"etcd","version":"0.9.0"}`)},
+ {Type: "olm.gvk", Value: json.RawMessage(`{"group":"etcd.database.coreos.com","kind":"EtcdBackup","version":"v1beta2"}`)},
+ {Type: "olm.channel", Value: json.RawMessage(`{"name":"singlenamespace-alpha"}`)},
+ {Type: "olm.channel", Value: json.RawMessage(`{"name":"clusterwide-alpha"}`)},
+ },
+ RelatedImages: []RelatedImage{{Name: "etcdv0.9.0", Image: "quay.io/coreos/etcd-operator@sha256:db563baa8194fcfe39d1df744ed70024b0f1f9e9b55b5923c2f3a413c44dc6b8"}},
+ },
+ {
+ Schema: "olm.bundle",
+ Name: "etcdoperator.v0.9.2",
+ Package: "etcd",
+ Image: "quay.io/operatorhubio/etcd:v0.9.2",
+ Properties: []property.Property{
+ {Type: "olm.package", Value: json.RawMessage(`{"packageName":"etcd","version":"0.9.2"}`)},
+ {Type: "olm.gvk", Value: json.RawMessage(`{"group":"etcd.database.coreos.com","kind":"EtcdRestore","version":"v1beta2"}`)},
+ {Type: "olm.channel", Value: json.RawMessage(`{"name":"singlenamespace-alpha","replaces":"etcdoperator.v0.9.0"}`)},
+ },
+ RelatedImages: []RelatedImage{{Name: "etcdv0.9.2", Image: "quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2"}},
+ },
+ {
+ Schema: "olm.bundle",
+ Name: "etcdoperator.v0.9.2-clusterwide",
+ Package: "etcd",
+ Image: "quay.io/operatorhubio/etcd:v0.9.2-clusterwide",
+ Properties: []property.Property{
+ {Type: "olm.package", Value: json.RawMessage(`{"packageName":"etcd","version":"0.9.2-clusterwide"}`)},
+ {Type: "olm.gvk", Value: json.RawMessage(`{"group":"etcd.database.coreos.com","kind":"EtcdBackup","version":"v1beta2"}`)},
+ {Type: "olm.skipRange", Value: json.RawMessage(`">=0.9.0 <=0.9.1"`)},
+ {Type: "olm.skips", Value: json.RawMessage(`"etcdoperator.v0.6.1"`)},
+ {Type: "olm.skips", Value: json.RawMessage(`"etcdoperator.v0.9.0"`)},
+ {Type: "olm.channel", Value: json.RawMessage(`{"name":"clusterwide-alpha","replaces":"etcdoperator.v0.9.0"}`)},
+ },
+ RelatedImages: []RelatedImage{{Name: "etcdv0.9.2", Image: "quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2"}},
+ },
+ {
+ Schema: "olm.bundle",
+ Name: "etcdoperator.v0.9.4",
+ Package: "etcd",
+ Image: "quay.io/operatorhubio/etcd:v0.9.4",
+ Properties: []property.Property{
+ {Type: "olm.package", Value: json.RawMessage(`{"packageName":"etcd","version":"0.9.4"}`)},
+ {Type: "olm.package.required", Value: json.RawMessage(`{"packageName":"test","versionRange":">=1.2.3 <2.0.0-0"}`)},
+ {Type: "olm.gvk", Value: json.RawMessage(`{"group":"etcd.database.coreos.com","kind":"EtcdBackup","version":"v1beta2"}`)},
+ {Type: "olm.gvk.required", Value: json.RawMessage(`{"group":"testapi.coreos.com","kind":"Testapi","version":"v1"}`)},
+ {Type: "olm.channel", Value: json.RawMessage(`{"name":"singlenamespace-alpha","replaces":"etcdoperator.v0.9.2"}`)},
+ },
+ RelatedImages: []RelatedImage{{Name: "etcdv0.9.2", Image: "quay.io/coreos/etcd-operator@sha256:66a37fd61a06a43969854ee6d3e21087a98b93838e284a6086b13917f96b0d9b"}},
+ },
+ {
+ Schema: "olm.bundle",
+ Name: "etcdoperator.v0.9.4-clusterwide",
+ Package: "etcd",
+ Image: "quay.io/operatorhubio/etcd:v0.9.4-clusterwide",
+ Properties: []property.Property{
+ {Type: "olm.package", Value: json.RawMessage(`{"packageName":"etcd","version":"0.9.4-clusterwide"}`)},
+ {Type: "olm.gvk", Value: json.RawMessage(`{"group":"etcd.database.coreos.com","kind":"EtcdBackup","version":"v1beta2"}`)},
+ {Type: "olm.channel", Value: json.RawMessage(`{"name":"clusterwide-alpha","replaces":"etcdoperator.v0.9.2-clusterwide"}`)},
+ },
+ RelatedImages: []RelatedImage{{Name: "etcdv0.9.2", Image: "quay.io/coreos/etcd-operator@sha256:66a37fd61a06a43969854ee6d3e21087a98b93838e284a6086b13917f96b0d9b"}},
+ },
+ {
+ Schema: "olm.bundle",
+ },
+ },
+ Others: []Meta{
+ {Schema: "unexpected", Package: "", Blob: json.RawMessage(`{ "schema": "unexpected" }`)},
+ },
+ },
+ },
+ }
+
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ cfg, err := LoadFS(s.fsys)
+ s.assertion(t, err)
+ if err == nil {
+ require.NotNil(t, cfg)
+ equalsDeclarativeConfig(t, *s.expected, *cfg)
+ }
+ })
+ }
+}
+
+func toJSON(t *testing.T, in []byte) string {
+ t.Helper()
+ out, err := yaml.ToJSON(in)
+ if err != nil {
+ t.Fatalf("failed converting testdata to JSON: %v", err)
+ }
+ return string(out)
+}
+
+var (
+ invalidBundle = &fstest.MapFile{
+ Data: []byte(`{"schema": "olm.bundle","relatedImages": {}}`),
+ }
+ invalidPackage = &fstest.MapFile{
+ Data: []byte(`{"schema": "olm.package","name": {}}`),
+ }
+ noSchema = &fstest.MapFile{
+ Data: []byte(`hello: world`),
+ }
+ invalidFormat = &fstest.MapFile{
+ Data: []byte(`[This is not yaml or json.}`),
+ }
+ notObject = &fstest.MapFile{
+ Data: []byte(`[]`),
+ }
+ invalidFS = fstest.MapFS{
+ "invalid-bundle.json": invalidBundle,
+ "invalid-package.json": invalidPackage,
+ "no-schema.yaml": noSchema,
+ "invalid-format.txt": invalidFormat,
+ "not-object.json": notObject,
+ }
+
+ indexIgnore = &fstest.MapFile{
+ Data: []byte(`*
+!*.json
+!*.yaml
+
+*.clusterserviceversion.yaml`),
+ }
+ cockroachdb = &fstest.MapFile{
+ Data: []byte(`{
+ "schema": "olm.package",
+ "name": "cockroachdb",
+ "defaultChannel": "stable-5.x",
+ "icon": {
+ "base64data": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMS44MiAzMiIgd2lkdGg9IjI0ODYiIGhlaWdodD0iMjUwMCI+PHRpdGxlPkNMPC90aXRsZT48cGF0aCBkPSJNMTkuNDIgOS4xN2ExNS4zOSAxNS4zOSAwIDAgMS0zLjUxLjQgMTUuNDYgMTUuNDYgMCAwIDEtMy41MS0uNCAxNS42MyAxNS42MyAwIDAgMSAzLjUxLTMuOTEgMTUuNzEgMTUuNzEgMCAwIDEgMy41MSAzLjkxek0zMCAuNTdBMTcuMjIgMTcuMjIgMCAwIDAgMjUuNTkgMGExNy40IDE3LjQgMCAwIDAtOS42OCAyLjkzQTE3LjM4IDE3LjM4IDAgMCAwIDYuMjMgMGExNy4yMiAxNy4yMiAwIDAgMC00LjQ0LjU3QTE2LjIyIDE2LjIyIDAgMCAwIDAgMS4xM2EuMDcuMDcgMCAwIDAgMCAuMDkgMTcuMzIgMTcuMzIgMCAwIDAgLjgzIDEuNTcuMDcuMDcgMCAwIDAgLjA4IDAgMTYuMzkgMTYuMzkgMCAwIDEgMS44MS0uNTQgMTUuNjUgMTUuNjUgMCAwIDEgMTEuNTkgMS44OCAxNy41MiAxNy41MiAwIDAgMC0zLjc4IDQuNDhjLS4yLjMyLS4zNy42NS0uNTUgMXMtLjIyLjQ1LS4zMy42OS0uMzEuNzItLjQ0IDEuMDhhMTcuNDYgMTcuNDYgMCAwIDAgNC4yOSAxOC43Yy4yNi4yNS41My40OS44MS43M3MuNDQuMzcuNjcuNTQuNTkuNDQuODkuNjRhLjA3LjA3IDAgMCAwIC4wOCAwYy4zLS4yMS42LS40Mi44OS0uNjRzLjQ1LS4zNS42Ny0uNTQuNTUtLjQ4LjgxLS43M2ExNy40NSAxNy40NSAwIDAgMCA1LjM4LTEyLjYxIDE3LjM5IDE3LjM5IDAgMCAwLTEuMDktNi4wOWMtLjE0LS4zNy0uMjktLjczLS40NS0xLjA5cy0uMjItLjQ3LS4zMy0uNjktLjM1LS42Ni0uNTUtMWExNy42MSAxNy42MSAwIDAgMC0zLjc4LTQuNDggMTUuNjUgMTUuNjUgMCAwIDEgMTEuNi0xLjg0IDE2LjEzIDE2LjEzIDAgMCAxIDEuODEuNTQuMDcuMDcgMCAwIDAgLjA4IDBxLjQ0LS43Ni44Mi0xLjU2YS4wNy4wNyAwIDAgMCAwLS4wOUExNi44OSAxNi44OSAwIDAgMCAzMCAuNTd6IiBmaWxsPSIjMTUxZjM0Ii8+PHBhdGggZD0iTTIxLjgyIDE3LjQ3YTE1LjUxIDE1LjUxIDAgMCAxLTQuMjUgMTAuNjkgMTUuNjYgMTUuNjYgMCAwIDEtLjcyLTQuNjggMTUuNSAxNS41IDAgMCAxIDQuMjUtMTAuNjkgMTUuNjIgMTUuNjIgMCAwIDEgLjcyIDQuNjgiIGZpbGw9IiMzNDg1NDAiLz48cGF0aCBkPSJNMTUgMjMuNDhhMTUuNTUgMTUuNTUgMCAwIDEtLjcyIDQuNjggMTUuNTQgMTUuNTQgMCAwIDEtMy41My0xNS4zN0ExNS41IDE1LjUgMCAwIDEgMTUgMjMuNDgiIGZpbGw9IiM3ZGJjNDIiLz48L3N2Zz4=",
+ "mediatype": "image/svg+xml"
+ }
+}
+{
+ "schema": "olm.bundle",
+ "name": "cockroachdb.v2.0.9",
+ "package": "cockroachdb",
+ "image": "quay.io/openshift-community-operators/cockroachdb:v2.0.9",
+ "properties": [
+ {
+ "type": "olm.channel",
+ "value": {
+ "name": "stable"
+ }
+ },
+ {
+ "type": "olm.package",
+ "value": {
+ "packageName": "cockroachdb",
+ "version": "2.0.9"
+ }
+ }
+ ]
+}
+{
+ "schema": "olm.bundle",
+ "name": "cockroachdb.v2.1.11",
+ "package": "cockroachdb",
+ "image": "quay.io/openshift-community-operators/cockroachdb:v2.1.11",
+ "properties": [
+ {
+ "type": "olm.channel",
+ "value": {
+ "name": "stable",
+ "replaces": "cockroachdb.v2.1.1"
+ }
+ },
+ {
+ "type": "olm.package",
+ "value": {
+ "packageName": "cockroachdb",
+ "version": "2.1.11"
+ }
+ }
+ ]
+}
+{
+ "schema": "olm.bundle",
+ "name": "cockroachdb.v2.1.1",
+ "package": "cockroachdb",
+ "image": "quay.io/openshift-community-operators/cockroachdb:v2.1.1",
+ "properties": [
+ {
+ "type": "olm.channel",
+ "value": {
+ "name": "stable",
+ "replaces": "cockroachdb.v2.0.9"
+ }
+ },
+ {
+ "type": "olm.package",
+ "value": {
+ "packageName": "cockroachdb",
+ "version": "2.1.1"
+ }
+ }
+ ]
+}
+{
+ "schema": "olm.bundle",
+ "name": "cockroachdb.v3.0.7",
+ "package": "cockroachdb",
+ "image": "quay.io/openshift-community-operators/cockroachdb:v3.0.7",
+ "properties": [
+ {
+ "type": "olm.channel",
+ "value": {
+ "name": "stable-3.x"
+ }
+ },
+ {
+ "type": "olm.package",
+ "value": {
+ "packageName": "cockroachdb",
+ "version": "3.0.7"
+ }
+ }
+ ]
+}
+{
+ "schema": "olm.bundle",
+ "name": "cockroachdb.v5.0.3",
+ "package": "cockroachdb",
+ "image": "quay.io/openshift-community-operators/cockroachdb:v5.0.3",
+ "properties": [
+ {
+ "type": "olm.channel",
+ "value": {
+ "name": "stable-5.x"
+ }
+ },
+ {
+ "type": "olm.package",
+ "value": {
+ "packageName": "cockroachdb",
+ "version": "5.0.3"
+ }
+ }
+ ]
+}`),
+ }
+ etcd = &fstest.MapFile{
+ Data: []byte(`---
+schema: olm.package
+name: etcd
+defaultChannel: singlenamespace-alpha
+description: A message about etcd operator, a description of channels
+icon:
+ base64data: PHN2ZyB3aWR0aD0iMjUwMCIgaGVpZ2h0PSIyNDIyIiB2aWV3Qm94PSIwIDAgMjU2IDI0OCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTI1Mi4zODYgMTI4LjA2NGMtMS4yMDIuMS0yLjQxLjE0Ny0zLjY5My4xNDctNy40NDYgMC0xNC42Ny0xLjc0Ni0yMS4xODctNC45NDQgMi4xNy0xMi40NDcgMy4wOTItMjQuOTg3IDIuODUtMzcuNDgxLTcuMDY1LTEwLjIyLTE1LjE0LTE5Ljg2My0yNC4yNTYtMjguNzQ3IDMuOTU1LTcuNDE1IDkuODAxLTEzLjc5NSAxNy4xLTE4LjMxOWwzLjEzMy0xLjkzNy0yLjQ0Mi0yLjc1NGMtMTIuNTgxLTE0LjE2Ny0yNy41OTYtMjUuMTItNDQuNjItMzIuNTUyTDE3NS44NzYgMGwtLjg2MiAzLjU4OGMtMi4wMyA4LjM2My02LjI3NCAxNS45MDgtMTIuMSAyMS45NjJhMTkzLjg0MiAxOTMuODQyIDAgMCAwLTM0Ljk1Ni0xNC40MDVBMTk0LjAxMiAxOTQuMDEyIDAgMCAwIDkzLjA1NiAyNS41MkM4Ny4yNTQgMTkuNDczIDgzLjAyIDExLjk0NyA4MC45OTkgMy42MDhMODAuMTMuMDJsLTMuMzgyIDEuNDdDNTkuOTM5IDguODE1IDQ0LjUxIDIwLjA2NSAzMi4xMzUgMzQuMDJsLTIuNDQ5IDIuNzYgMy4xMyAxLjkzN2M3LjI3NiA0LjUwNiAxMy4xMDYgMTAuODQ5IDE3LjA1NCAxOC4yMjMtOS4wODggOC44NS0xNy4xNTQgMTguNDYyLTI0LjIxNCAyOC42MzUtLjI3NSAxMi40ODkuNiAyNS4xMiAyLjc4IDM3Ljc0LTYuNDg0IDMuMTY3LTEzLjY2OCA0Ljg5NC0yMS4wNjUgNC44OTQtMS4yOTggMC0yLjUxMy0uMDQ3LTMuNjkzLS4xNDVMMCAxMjcuNzg1bC4zNDUgMy42NzFjMS44MDIgMTguNTc4IDcuNTcgMzYuMjQ3IDE3LjE1NCA1Mi41MjNsMS44NyAzLjE3NiAyLjgxLTIuMzg0YTQ4LjA0IDQ4LjA0IDAgMCAxIDIyLjczNy0xMC42NSAxOTQuODYgMTk0Ljg2IDAgMCAwIDE5LjQ2IDMxLjY5NmMxMS44MjggNC4xMzcgMjQuMTUxIDcuMjI1IDM2Ljg3OCA5LjA2MyAxLjIyIDguNDE3LjI0OCAxNy4xMjItMy4wNzIgMjUuMTcxbC0xLjQgMy40MTEgMy42Ljc5M2M5LjIyIDIuMDI3IDE4LjUyMyAzLjA2IDI3LjYzMSAzLjA2bDI3LjYyMy0zLjA2IDMuNjA0LS43OTMtMS40MDMtMy40MTdjLTMuMzEyLTguMDUtNC4yODQtMTYuNzY1LTMuMDYzLTI1LjE4MyAxMi42NzYtMS44NCAyNC45NTQtNC45MiAzNi43MzgtOS4wNDVhMTk1LjEwOCAxOTUuMTA4IDAgMCAwIDE5LjQ4Mi0zMS43MjYgNDguMjU0IDQ4LjI1NCAwIDAgMSAyMi44NDggMTAuNjZsMi44MDkgMi4zOCAxLjg2Mi0zLjE2OGM5LjYtMTYuMjk3IDE1LjM2OC0zMy45NjUgMTcuMTQyLTUyLjUxM2wuMzQ1LTMuNjY1LTMuNjE0LjI3OXpNMTY3LjQ5IDE3Mi45NmMtMTMuMDY4IDMuNTU0LTI2LjM0IDUuMzQ4LTM5LjUzMiA1LjM0OC0xMy4yMjggMC0yNi40ODMtMS43OTMtMzkuNTYzLTUuMzQ4YTE1My4yNTUgMTUzLjI1NSAwIDAgMS0xNi45MzItMzUuNjdjLTQuMDY2LTEyLjUxNy02LjQ0NS0yNS42My03LjEzNS0zOS4xMzQgOC40NDYtMTAuNDQzIDE4LjA1Mi0xOS41OTEgMjguNjY1LTI3LjI5M2ExNTIuNjIgMTUyLjYyIDAgMCAxIDM0Ljk2NS0xOS4wMTEgMTUzLjI0MiAxNTMuMjQyIDAgMCAxIDM0Ljg5OCAxOC45N2MxMC42NTQgNy43NDMgMjAuMzAyIDE2Ljk2MiAyOC43OSAyNy40Ny0uNzI0IDEzLjQyNy0zLjEzMiAyNi40NjUtNy4yMDQgMzguOTYxYTE1Mi43NjcgMTUyLjc2NyAwIDAgMS0xNi45NTIgMzUuNzA3em0tMjguNzQtNjIuOTk4YzAgOS4yMzIgNy40ODIgMTYuNyAxNi43MDIgMTYuNyA5LjIxNyAwIDE2LjY5LTcuNDY2IDE2LjY5LTE2LjcgMC05LjE5Ni03LjQ3My0xNi42OTItMTYuNjktMTYuNjkyLTkuMjIgMC0xNi43MDEgNy40OTYtMTYuNzAxIDE2LjY5MnptLTIxLjU3OCAwYzAgOS4yMzItNy40OCAxNi43LTE2LjcgMTYuNy05LjIyNiAwLTE2LjY4NS03LjQ2Ni0xNi42ODUtMTYuNyAwLTkuMTkzIDcuNDYtMTYuNjg5IDE2LjY4Ni0xNi42ODkgOS4yMiAwIDE2LjcgNy40OTYgMTYuNyAxNi42OXoiIGZpbGw9IiM0MTlFREEiLz48L3N2Zz4K
+ mediatype: image/svg+xml
+
+---
+schema: olm.bundle
+package: etcd
+name: etcdoperator-community.v0.6.1
+image: quay.io/operatorhubio/etcd:v0.6.1
+properties:
+ - type: olm.package
+ value:
+ packageName: etcd
+ version: 0.6.1
+ - type: olm.gvk
+ value:
+ group: etcd.database.coreos.com
+ kind: EtcdCluster
+ version: v1beta2
+ - type: olm.channel
+ value:
+ name: alpha
+ - type: olm.skipRange
+ value: <0.6.1
+ - type: olm.bundle.object
+ value:
+ ref: etcdoperator.v0.6.1.clusterserviceversion.yaml
+relatedImages:
+ - image: quay.io/coreos/etcd-operator@sha256:bd944a211eaf8f31da5e6d69e8541e7cada8f16a9f7a5a570b22478997819943
+ name: etcdv0.6.1
+
+---
+schema: olm.bundle
+package: etcd
+name: etcdoperator.v0.9.0
+image: quay.io/operatorhubio/etcd:v0.9.0
+properties:
+ - type: olm.package
+ value:
+ packageName: etcd
+ version: 0.9.0
+ - type: olm.gvk
+ value:
+ group: etcd.database.coreos.com
+ kind: EtcdBackup
+ version: v1beta2
+ - type: olm.channel
+ value:
+ name: singlenamespace-alpha
+ - type: olm.channel
+ value:
+ name: clusterwide-alpha
+relatedImages:
+ - image: quay.io/coreos/etcd-operator@sha256:db563baa8194fcfe39d1df744ed70024b0f1f9e9b55b5923c2f3a413c44dc6b8
+ name: etcdv0.9.0
+
+---
+schema: olm.bundle
+package: etcd
+name: etcdoperator.v0.9.2
+image: quay.io/operatorhubio/etcd:v0.9.2
+properties:
+ - type: olm.package
+ value:
+ packageName: etcd
+ version: 0.9.2
+ - type: olm.gvk
+ value:
+ group: etcd.database.coreos.com
+ kind: EtcdRestore
+ version: v1beta2
+ - type: olm.channel
+ value:
+ name: singlenamespace-alpha
+ replaces: etcdoperator.v0.9.0
+relatedImages:
+ - image: quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2
+ name: etcdv0.9.2
+
+---
+schema: olm.bundle
+package: etcd
+name: etcdoperator.v0.9.2-clusterwide
+image: quay.io/operatorhubio/etcd:v0.9.2-clusterwide
+properties:
+ - type: olm.package
+ value:
+ packageName: etcd
+ version: 0.9.2-clusterwide
+ - type: olm.gvk
+ value:
+ group: etcd.database.coreos.com
+ kind: EtcdBackup
+ version: v1beta2
+ - type: olm.skipRange
+ value: '>=0.9.0 <=0.9.1'
+ - type: olm.skips
+ value: etcdoperator.v0.6.1
+ - type: olm.skips
+ value: etcdoperator.v0.9.0
+ - type: olm.channel
+ value:
+ name: clusterwide-alpha
+ replaces: etcdoperator.v0.9.0
+relatedImages:
+ - image: quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2
+ name: etcdv0.9.2
+
+---
+schema: olm.bundle
+package: etcd
+name: etcdoperator.v0.9.4
+image: quay.io/operatorhubio/etcd:v0.9.4
+properties:
+ - type: olm.package
+ value:
+ packageName: etcd
+ version: 0.9.4
+ - type: olm.package.required
+ value:
+ packageName: test
+ versionRange: '>=1.2.3 <2.0.0-0'
+ - type: olm.gvk
+ value:
+ group: etcd.database.coreos.com
+ kind: EtcdBackup
+ version: v1beta2
+ - type: olm.gvk.required
+ value:
+ group: testapi.coreos.com
+ kind: Testapi
+ version: v1
+ - type: olm.channel
+ value:
+ name: singlenamespace-alpha
+ replaces: etcdoperator.v0.9.2
+relatedImages:
+ - image: quay.io/coreos/etcd-operator@sha256:66a37fd61a06a43969854ee6d3e21087a98b93838e284a6086b13917f96b0d9b
+ name: etcdv0.9.2
+
+---
+schema: olm.bundle
+package: etcd
+name: etcdoperator.v0.9.4-clusterwide
+image: quay.io/operatorhubio/etcd:v0.9.4-clusterwide
+properties:
+ - type: olm.package
+ value:
+ packageName: etcd
+ version: 0.9.4-clusterwide
+ - type: olm.gvk
+ value:
+ group: etcd.database.coreos.com
+ kind: EtcdBackup
+ version: v1beta2
+ - type: olm.channel
+ value:
+ name: clusterwide-alpha
+ replaces: etcdoperator.v0.9.2-clusterwide
+relatedImages:
+ - image: quay.io/coreos/etcd-operator@sha256:66a37fd61a06a43969854ee6d3e21087a98b93838e284a6086b13917f96b0d9b
+ name: etcdv0.9.2`),
+ }
+ etcdCSV = &fstest.MapFile{
+ Data: []byte(`apiVersion: operators.coreos.com/v1alpha1
+kind: ClusterServiceVersion
+metadata:
+ annotations:
+ capabilities: Full Lifecycle
+ description: etcd is a distributed key value store providing a reliable way to
+ store data across a cluster of machines.
+ tectonic-visibility: ocs
+ name: etcdoperator.v0.6.1
+ namespace: placeholder
+spec:
+ customresourcedefinitions:
+ owned:
+ - description: Represents a cluster of etcd nodes.
+ displayName: etcd Cluster
+ kind: EtcdCluster
+ name: etcdclusters.etcd.database.coreos.com
+ resources:
+ - kind: Service
+ version: v1
+ - kind: Pod
+ version: v1
+ specDescriptors:
+ - description: The desired number of member Pods for the etcd cluster.
+ displayName: Size
+ path: size
+ x-descriptors:
+ - urn:alm:descriptor:com.tectonic.ui:podCount
+ statusDescriptors:
+ - description: The status of each of the member Pods for the etcd cluster.
+ displayName: Member Status
+ path: members
+ x-descriptors:
+ - urn:alm:descriptor:com.tectonic.ui:podStatuses
+ - description: The service at which the running etcd cluster can be accessed.
+ displayName: Service
+ path: service
+ x-descriptors:
+ - urn:alm:descriptor:io.kubernetes:Service
+ - description: The current size of the etcd cluster.
+ displayName: Cluster Size
+ path: size
+ - description: The current version of the etcd cluster.
+ displayName: Current Version
+ path: currentVersion
+ - description: The target version of the etcd cluster, after upgrading.
+ displayName: Target Version
+ path: targetVersion
+ - description: The current status of the etcd cluster.
+ displayName: Status
+ path: phase
+ x-descriptors:
+ - urn:alm:descriptor:io.kubernetes.phase
+ - description: Explanation for the current status of the cluster.
+ displayName: Status Details
+ path: reason
+ x-descriptors:
+ - urn:alm:descriptor:io.kubernetes.phase:reason
+ version: v1beta2
+ description: "etcd is a distributed key value store that provides a reliable way\
+ \ to store data across a cluster of machines. It\xE2\u20AC\u2122s open-source\
+ \ and available on GitHub. etcd gracefully handles leader elections during network\
+ \ partitions and will tolerate machine failure, including the leader. Your applications\
+ \ can read and write data into etcd.\nA simple use-case is to store database connection\
+ \ details or feature flags within etcd as key value pairs. These values can be\
+ \ watched, allowing your app to reconfigure itself when they change. Advanced\
+ \ uses take advantage of the consistency guarantees to implement database leader\
+ \ elections or do distributed locking across a cluster of workers.\n\n_The etcd\
+ \ Open Cloud Service is Public Alpha. The goal before Beta is to fully implement\
+ \ backup features._\n\n### Reading and writing to etcd\n\nCommunicate with etcd\
+ \ though its command line utility ` + "`etcdctl`" + ` or with the API using the automatically\
+ \ generated Kubernetes Service.\n\n[Read the complete guide to using the etcd\
+ \ Open Cloud Service](https://coreos.com/tectonic/docs/latest/alm/etcd-ocs.html)\n\
+ \n### Supported Features\n**High availability**\nMultiple instances of etcd are\
+ \ networked together and secured. Individual failures or networking issues are\
+ \ transparently handled to keep your cluster up and running.\n**Automated updates**\n\
+ Rolling out a new etcd version works like all Kubernetes rolling updates. Simply\
+ \ declare the desired version, and the etcd service starts a safe rolling update\
+ \ to the new version automatically.\n**Backups included**\nComing soon, the ability\
+ \ to schedule backups to happen on or off cluster.\n"
+ displayName: etcd
+ icon:
+ - base64data: iVBORw0KGgoAAAANSUhEUgAAAOEAAADZCAYAAADWmle6AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAEKlJREFUeNrsndt1GzkShmEev4sTgeiHfRYdgVqbgOgITEVgOgLTEQydwIiKwFQCayoCU6+7DyYjsBiBFyVVz7RkXvqCSxXw/+f04XjGQ6IL+FBVuL769euXgZ7r39f/G9iP0X+u/jWDNZzZdGI/Ftama1jjuV4BwmcNpbAf1Fgu+V/9YRvNAyzT2a59+/GT/3hnn5m16wKWedJrmOCxkYztx9Q+py/+E0GJxtJdReWfz+mxNt+QzS2Mc0AI+HbBBwj9QViKbH5t64DsP2fvmGXUkWU4WgO+Uve2YQzBUGd7r+zH2ZG/tiUQc4QxKwgbwFfVGwwmdLL5wH78aPC/ZBem9jJpCAX3xtcNASSNgJLzUPSQyjB1zQNl8IQJ9MIU4lx2+Jo72ysXYKl1HSzN02BMa/vbZ5xyNJIshJzwf3L0dQhJw4Sih/SFw9Tk8sVeghVPoefaIYCkMZCKbrcP9lnZuk0uPUjGE/KE8JQry7W2tgfuC3vXgvNV+qSQbyFtAtyWk7zWiYevvuUQ9QEQCvJ+5mmu6dTjz1zFHLFj8Eb87MtxaZh/IQFIHom+9vgTWwZxAQjT9X4vtbEVPojwjiV471s00mhAckpwGuCn1HtFtRDaSh6y9zsL+LNBvCG/24ThcxHObdlWc1v+VQJe8LcO0jwtuF8BwnAAUgP9M8JPU2Me+Oh12auPGT6fHuTePE3bLDy+x9pTLnhMn+07TQGh//Bz1iI0c6kvtqInjvPZcYR3KsPVmUsPYt9nFig9SCY8VQNhpPBzn952bbgcsk2EvM89wzh3UEffBbyPqvBUBYQ8ODGPFOLsa7RF096WJ69L+E4EmnpjWu5o4ChlKaRTKT39RMMaVPEQRsz/nIWlDN80chjdJlSd1l0pJCAMVZsniobQVuxceMM9OFoaMd9zqZtjMEYYDW38Drb8Y0DYPLShxn0pvIFuOSxd7YCPet9zk452wsh54FJoeN05hcgSQoG5RR0Qh9Q4E4VvL4wcZq8UACgaRFEQKgSwWrkr5WFnGxiHSutqJGlXjBgIOayhwYBTA0ER0oisIVSUV0AAMT0IASCUO4hRIQSAEECMCCEPwqyQA0JCQBzEGjWNAqHiUVAoXUWbvggOIQCEAOJzxTjoaQ4AIaE64/aZridUsBYUgkhB15oGg1DBIl8IqirYwV6hPSGBSFteMCUBSVXwfYixBmamRubeMyjzMJQBDDowE3OesDD+zwqFoDqiEwXoXJpljB+PvWJGy75BKF1FPxhKygJuqUdYQGlLxNEXkrYyjQ0GbaAwEnUIlLRNvVjQDYUAsJB0HKLE4y0AIpQNgCIhBIhQTgCKhZBBpAN/v6LtQI50JfUgYOnnjmLUFHKhjxbAmdTCaTiBm3ovLPqG2urWAij6im0Nd9aTN9ygLUEt9LgSRnohxUPIKxlGaE+/6Y7znFf0yX+GnkvFFWmarkab2o9PmTeq8sbd2a7DaysXz7i64VeznN4jCQhN9gdDbRiuWrfrsq0mHIrlaq+hlotCtd3Um9u0BYWY8y5D67wccJoZjFca7iUs9VqZcfsZwTd1sbWGG+OcYaTnPAP7rTQVVlM4Sg3oGvB1tmNh0t/HKXZ1jFoIMwCQjtqbhNxUmkGYqgZEDZP11HN/S3gAYRozf0l8C5kKEKUvW0t1IfeWG/5MwgheZTT1E0AEhDkAePQO+Ig2H3DncAkQM4cwUQCD530dU4B5Yvmi2LlDqXfWrxMCcMth51RToRMNUXFnfc2KJ0+Ryl0VNOUwlhh6NoxK5gnViTgQpUG4SqSyt5z3zRJpuKmt3Q1614QaCBPaN6je+2XiFcWAKOXcUfIYKRyL/1lb7pe5VxSxxjQ6hImshqGRt5GWZVKO6q2wHwujfwDtIvaIdexj8Cm8+a68EqMfox6x/voMouZF4dHnEGNeCDMwT6vdNfekH1MafMk4PI06YtqLVGl95aEM9Z5vAeCTOA++YLtoVJRrsqNCaJ6WRmkdYaNec5BT/lcTRMqrhmwfjbpkj55+OKp8IEbU/JLgPJE6Wa3TTe9sHS+ShVD5QIyqIxMEwKh12olC6mHIed5ewEop80CNlfIOADYOT2nd6ZXCop+Ebqchc0JqxKcKASxChycJgUh1rnHA5ow9eTrhqNI7JWiAYYwBGGdpyNLoGw0Pkh96h1BpHihyywtATDM/7Hk2fN9EnH8BgKJCU4ooBkbXFMZJiPbrOyecGl3zgQDQL4hk10IZiOe+5w99Q/gBAEIJgPhJM4QAEEoFREAIAAEiIASAkD8Qt4AQAEIAERAGFlX4CACKAXGVM4ivMwWwCLFAlyeoaa70QePKm5Dlp+/n+ye/5dYgva6YsUaVeMa+tzNFeJtWwc+udbJ0Fg399kLielQJ5Ze61c2+7ytA6EZetiPxZC6tj22yJCv6jUwOyj/zcbqAxOMyAKEbfeHtNa7DtYXptjsk2kJxR+eIeim/tHNofUKYy8DMrQcAKWz6brpvzyIAlpwPhQ49l6b7skJf5Z+YTOYQc4FwLDxvoTDwaygQK+U/kVr+ytSFBG01Q3gnJJR4cNiAhx4HDub8/b5DULXlj6SVZghFiE+LdvE9vo/o8Lp1RmH5hzm0T6wdbZ6n+D6i44zDRc3ln6CpAEJfXiRU45oqLz8gFAThWsh7ughrRibc0QynHgZpNJa/ENJ+loCwu/qOGnFIjYR/n7TfgycULhcQhu6VC+HfF+L3BoAQ4WiZTw1M+FPCnA2gKC6/FAhXgDC+ojQGh3NuWsvfF1L/D5ohlCKtl1j2ldu9a/nPAKFwN56Bst10zCG0CPleXN/zXPgHQZXaZaBgrbzyY5V/mUA+6F0hwtGN9rwu5DVZPuwWqfxdFz1LWbJ2lwKEa+0Qsm4Dl3fp+Pu0lV97PgwIPfSsS+UQhj5Oo+vvFULazRIQyvGEcxPuNLCth2MvFsrKn8UOilAQShkh7TTczYNMoS6OdP47msrPi82lXKGWhCdMZYS0bFy+vcnGAjP1CIfvgbKNA9glecEH9RD6Ol4wRuWyN/G9MHnksS6o/GPf5XcwNSUlHzQhDuAKtWJmkwKElU7lylP5rgIcsquh/FI8YZCDpkJBuE4FQm7Icw8N+SrUGaQKyi8FwiDt1ve5o+Vu7qYHy/psgK8cvh+FTYuO77bhEC7GuaPiys/L1X4IgXDL+e3M5+ovLxBy5VLuIebw1oqcHoPfoaMJUsHays878r8KbDc3xtPx/84gZPBG/JwaufrsY/SRG/OY3//8QMNdsvdZCFtbW6f8pFuf5bflILAlX7O+4fdfugKyFYS8T2zAsXthdG0VurPGKwI06oF5vkBgHWkNp6ry29+lsPZMU3vijnXFNmoclr+6+Ou/FIb8yb30sS8YGjmTqCLyQsi5N/6ZwKs0Yenj68pfPjF6N782Dp2FzV9CTyoSeY8mLK16qGxIkLI8oa1n8tz9juP40DlK0epxYEbojbq+9QfurBeVIlCO9D2396bxiV4lkYQ3hOAFw2pbhqMGISkkQOMcQ9EqhDmGZZdo92JC0YHRNTfoSg+5e0IT+opqCKHoIU+4ztQIgBD1EFNrQAgIpYSil9lDmPHqkROPt+JC6AgPquSuumJmg0YARVCuneDfvPVeJokZ6pIXDkNxQtGzTF9/BQjRG0tQznfb74RwCQghpALBtIQnfK4zhxdyQvVCUeknMIT3hLyY+T5jo0yABqKPQNpUNw/09tGZod5jgCaYFxyYvJcNPkv9eof+I3pnCFEHIETjSM8L9tHZHYCQT9PaZGycU6yg8S4akDnJ+P03L0+t23XGzCLzRgII/Wqa+fv/xlfvmKvMUOcOrlCDdoei1MGdZm6G5VEIfRzzjd4aQs69n699Rx7ewhvCGzr2gmTPs8zNsJOrXt24FbkhhOjCfT4ICA/rPbyhUy94Dks0gJCX1NzCZui9YUd3oei+c257TalFbgg19ILHrlrL2gvWgXAL26EX76gZTNASQnad8Ibwhl284NhgXpB0c+jKhWO3Ms1hP9ihJYB9eMF6qd1BCPk0qA1s+LimFIu7m4nsdQIzPK4VbQ8hYvrnuSH2G9b2ggP78QmWqBdF9Vx8SSY6QYdUW7BTA1schZATyhvY8lHvcRbNUS9YGFy2U+qmzh2YPVc0I7yAOFyHfRpyUwtCSzOdPXMHmz7qDIM0e0V2wZTEk+6Ym6N63eBLp/b5Bts+2cKCSJ/LuoZO3ANSiE5hKAZjnvNSS4931jcw9jpwT0feV/qSJ1pVtCyfHKDkvK8Ejx7pUxGh2xFNSwx8QTi2H9ceC0/nni64MS/5N5dG39pDqvRV+WgGk71c9VFXF9b+xYvOw/d61iv7m3MvEHryhvecwC52jSSx4VIIgwnMNT/UsTxIgpPt3K/ARj15CptwL3Zd/ceDSATj2DGQjbxgWwhdeMMte7zpy5On9vymRm/YxBYljGVjKWF9VJf7I1+sex3wY8w/V1QPTborW/72gkdsRDaZMJBdbdHIC7aCkAu9atlLbtnrzerMnyToDaGwelOnk3/hHSem/ZK7e/t7jeeR20LYBgqa8J80gS8jbwi5F02Uj1u2NYJxap8PLkJfLxA2hIJyvnHX/AfeEPLpBfe0uSFHbnXaea3Qd5d6HcpYZ8L6M7lnFwMQ3MNg+RxUR1+6AshtbsVgfXTEg1sIGax9UND2p7f270wdG3eK9gXVGHdw2k5sOyZv+Nbs39Z308XR9DqWb2J+PwKDhuKHPobfuXf7gnYGHdCs7bhDDadD4entDug7LWNsnRNW4mYqwJ9dk+GGSTPBiA2j0G8RWNM5upZtcG4/3vMfP7KnbK2egx6CCnDPhRn7NgD3cghLIad5WcM2SO38iqHvvMOosyeMpQ5zlVCaaj06GVs9xUbHdiKoqrHWgquFEFMWUEWfXUxJAML23hAHFOctmjZQffKD2pywkhtSGHKNtpitLroscAeE7kCkSsC60vxEl6yMtL9EL5HKGCMszU5bk8gdkklAyEn5FO0yK419rIxBOIqwFMooDE0tHEVYijAUECIshRCGIhxFWIowFJ5QkEYIS5PTJrUwNGlPyN6QQPyKtpuM1E/K5+YJDV/MiA3AaehzqgAm7QnZG9IGYKo8bHnSK7VblLL3hOwNHziPuEGOqE5brrdR6i+atCfckyeWD47HkAkepRGLY/e8A8J0gCwYSNypF08bBm+e6zVz2UL4AshhBUjML/rXLefqC82bcQFhGC9JDwZ1uuu+At0S5gCETYHsV4DUeD9fDN2Zfy5OXaW2zAwQygCzBLJ8cvaW5OXKC1FxfTggFAHmoAJnSiOw2wps9KwRWgJCLaEswaj5NqkLwAYIU4BxqTSXbHXpJdRMPZgAOiAMqABCNGYIEEJutEK5IUAIwYMDQgiCACEEAcJs1Vda7gGqDhCmoiEghAAhBAHCrKXVo2C1DCBMRlp37uMIEECoX7xrX3P5C9QiINSuIcoPAUI0YkAICLNWgfJDh4T9hH7zqYH9+JHAq7zBqWjwhPAicTVCVQJCNF50JghHocahKK0X/ZnQKyEkhSdUpzG8OgQI42qC94EQjsYLRSmH+pbgq73L6bYkeEJ4DYTYmeg1TOBFc/usTTp3V9DdEuXJ2xDCUbXhaXk0/kAYmBvuMB4qkC35E5e5AMKkwSQgyxufyuPy6fMMgAFCSI73LFXU/N8AmEL9X4ABACNSKMHAgb34AAAAAElFTkSuQmCC
+ mediatype: image/png
+ install:
+ spec:
+ deployments:
+ - name: etcd-operator
+ spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ name: etcd-operator-alm-owned
+ template:
+ metadata:
+ labels:
+ name: etcd-operator-alm-owned
+ name: etcd-operator-alm-owned
+ spec:
+ containers:
+ - command:
+ - etcd-operator
+ - --create-crd=false
+ env:
+ - name: MY_POD_NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ - name: MY_POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ image: quay.io/coreos/etcd-operator@sha256:bd944a211eaf8f31da5e6d69e8541e7cada8f16a9f7a5a570b22478997819943
+ name: etcd-operator
+ serviceAccountName: etcd-operator
+ permissions:
+ - rules:
+ - apiGroups:
+ - etcd.database.coreos.com
+ resources:
+ - etcdclusters
+ verbs:
+ - '*'
+ - apiGroups:
+ - storage.k8s.io
+ resources:
+ - storageclasses
+ verbs:
+ - '*'
+ - apiGroups:
+ - ''
+ resources:
+ - pods
+ - services
+ - endpoints
+ - persistentvolumeclaims
+ - events
+ verbs:
+ - '*'
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs:
+ - '*'
+ - apiGroups:
+ - ''
+ resources:
+ - secrets
+ verbs:
+ - get
+ serviceAccountName: etcd-operator
+ strategy: deployment
+ installModes:
+ - supported: true
+ type: OwnNamespace
+ - supported: true
+ type: SingleNamespace
+ - supported: false
+ type: MultiNamespace
+ - supported: true
+ type: AllNamespaces
+ keywords:
+ - etcd
+ - key value
+ - database
+ - coreos
+ - open source
+ labels:
+ alm-owner-etcd: etcdoperator
+ alm-status-descriptors: etcdoperator.v0.6.1
+ operated-by: etcdoperator
+ links:
+ - name: Blog
+ url: https://coreos.com/etcd
+ - name: Documentation
+ url: https://coreos.com/operators/etcd/docs/latest/
+ - name: etcd Operator Source Code
+ url: https://github.com/coreos/etcd-operator
+ maintainers:
+ - email: support@coreos.com
+ name: CoreOS, Inc
+ maturity: alpha
+ provider:
+ name: CoreOS, Inc
+ selector:
+ matchLabels:
+ alm-owner-etcd: etcdoperator
+ operated-by: etcdoperator
+ version: 0.6.1
+`),
+ }
+ readme = &fstest.MapFile{
+ Data: []byte(`# Valid Declarative Config
+
+This is a README file about this declarative config. It should be ignored
+when loading this directory as declarative config due to the patterns
+present in the .indexignore file.`),
+ }
+ unrecognizedSchema = &fstest.MapFile{
+ Data: []byte(`{"schema":"olm.package"}{"schema":"unexpected"}{"schema":"olm.bundle"}`),
+ }
+
+ validFS = fstest.MapFS{
+ ".indexignore": indexIgnore,
+ "cockroachdb.json": cockroachdb,
+ "etcd.yaml": etcd,
+ "etcdoperator.v0.6.1.clusterserviceversion.yaml": etcdCSV,
+ "README.md": readme,
+ "unrecognized-schema.json": unrecognizedSchema,
+ }
+)
+
+func TestLoadFile(t *testing.T) {
+ type spec struct {
+ name string
+ fsys fs.FS
+ path string
+ assertion require.ErrorAssertionFunc
+ expectNumPackages int
+ expectNumBundles int
+ expectNumOthers int
+ }
+ specs := []spec{
+ {
+ name: "Error/NonExistentDir",
+ fsys: os.DirFS("non/existent/dir/"),
+ assertion: require.Error,
+ },
+ {
+ name: "Error/Invalid",
+ fsys: invalidFS,
+ assertion: require.Error,
+ },
+ {
+ name: "Error/NotYAMLOrJSON",
+ fsys: invalidFS,
+ path: "invalid-format.txt",
+ assertion: require.Error,
+ },
+ {
+ name: "Error/NotJSONObject",
+ fsys: invalidFS,
+ path: "not-object.json",
+ assertion: require.Error,
+ },
+ {
+ name: "Error/NoSchema",
+ fsys: invalidFS,
+ path: "no-schema.yaml",
+ assertion: require.Error,
+ },
+ {
+ name: "Error/InvalidPackageJSON",
+ fsys: invalidFS,
+ path: "invalid-package.json",
+ assertion: require.Error,
+ },
+ {
+ name: "Error/InvalidBundleJSON",
+ fsys: invalidFS,
+ path: "invalid-bundle.json",
+ assertion: require.Error,
+ },
+ {
+ name: "Success/UnrecognizedSchema",
+ fsys: validFS,
+ path: "unrecognized-schema.json",
+ assertion: require.NoError,
+ expectNumPackages: 1,
+ expectNumBundles: 1,
+ expectNumOthers: 1,
+ },
+ {
+ name: "Success/ValidFile",
+ fsys: validFS,
+ path: "etcd.yaml",
+ assertion: require.NoError,
+ expectNumPackages: 1,
+ expectNumBundles: 6,
+ expectNumOthers: 0,
+ },
+ }
+
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ cfg, err := LoadFile(s.fsys, s.path)
+ s.assertion(t, err)
+ if err == nil {
+ require.NotNil(t, cfg)
+ assert.Equal(t, len(cfg.Packages), s.expectNumPackages, "unexpected package count")
+ assert.Equal(t, len(cfg.Bundles), s.expectNumBundles, "unexpected bundle count")
+ assert.Equal(t, len(cfg.Others), s.expectNumOthers, "unexpected others count")
+ }
+ })
+ }
+}
diff --git a/pkg/lib/declcfg/model_to_declcfg.go b/pkg/lib/declcfg/model_to_declcfg.go
new file mode 100644
index 000000000..14424d9f0
--- /dev/null
+++ b/pkg/lib/declcfg/model_to_declcfg.go
@@ -0,0 +1,131 @@
+package declcfg
+
+import (
+ "sort"
+
+ "github.com/operator-framework/operator-registry/alpha/model"
+ "github.com/operator-framework/operator-registry/alpha/property"
+)
+
+func ConvertFromModel(mpkgs model.Model) DeclarativeConfig {
+ cfg := DeclarativeConfig{}
+ for _, mpkg := range mpkgs {
+ channels, bundles := traverseModelChannels(*mpkg)
+
+ var i *Icon
+ if mpkg.Icon != nil {
+ i = &Icon{
+ Data: mpkg.Icon.Data,
+ MediaType: mpkg.Icon.MediaType,
+ }
+ }
+ defaultChannel := ""
+ if mpkg.DefaultChannel != nil {
+ defaultChannel = mpkg.DefaultChannel.Name
+ }
+ cfg.Packages = append(cfg.Packages, Package{
+ Schema: SchemaPackage,
+ Name: mpkg.Name,
+ DefaultChannel: defaultChannel,
+ Icon: i,
+ Description: mpkg.Description,
+ })
+ cfg.Channels = append(cfg.Channels, channels...)
+ cfg.Bundles = append(cfg.Bundles, bundles...)
+ }
+
+ sort.Slice(cfg.Packages, func(i, j int) bool {
+ return cfg.Packages[i].Name < cfg.Packages[j].Name
+ })
+ sort.Slice(cfg.Channels, func(i, j int) bool {
+ if cfg.Channels[i].Package != cfg.Channels[j].Package {
+ return cfg.Channels[i].Package < cfg.Channels[j].Package
+ }
+ return cfg.Channels[i].Name < cfg.Channels[j].Name
+ })
+ sort.Slice(cfg.Bundles, func(i, j int) bool {
+ if cfg.Bundles[i].Package != cfg.Bundles[j].Package {
+ return cfg.Bundles[i].Package < cfg.Bundles[j].Package
+ }
+ return cfg.Bundles[i].Name < cfg.Bundles[j].Name
+ })
+
+ return cfg
+}
+
+func traverseModelChannels(mpkg model.Package) ([]Channel, []Bundle) {
+ channels := []Channel{}
+ bundleMap := map[string]*Bundle{}
+
+ for _, ch := range mpkg.Channels {
+ // initialize channel
+ c := Channel{
+ Schema: SchemaChannel,
+ Name: ch.Name,
+ Package: ch.Package.Name,
+ Entries: []ChannelEntry{},
+ // NOTICE: The field Properties of the type Channel is for internal use only.
+ // DO NOT use it for any public-facing functionalities.
+ // This API is in alpha stage and it is subject to change.
+ Properties: ch.Properties,
+ }
+
+ for _, chb := range ch.Bundles {
+ // populate channel entry
+ c.Entries = append(c.Entries, ChannelEntry{
+ Name: chb.Name,
+ Replaces: chb.Replaces,
+ Skips: chb.Skips,
+ SkipRange: chb.SkipRange,
+ })
+
+ // create or update bundle
+ b, ok := bundleMap[chb.Name]
+ if !ok {
+ b = &Bundle{
+ Schema: SchemaBundle,
+ Name: chb.Name,
+ Package: chb.Package.Name,
+ Image: chb.Image,
+ RelatedImages: ModelRelatedImagesToRelatedImages(chb.RelatedImages),
+ CsvJSON: chb.CsvJSON,
+ Objects: chb.Objects,
+ }
+ bundleMap[b.Name] = b
+ }
+ b.Properties = append(b.Properties, chb.Properties...)
+ }
+
+ // sort channel entries by name
+ sort.Slice(c.Entries, func(i, j int) bool {
+ return c.Entries[i].Name < c.Entries[j].Name
+ })
+ channels = append(channels, c)
+ }
+
+ var bundles []Bundle
+ for _, b := range bundleMap {
+ b.Properties = property.Deduplicate(b.Properties)
+
+ sort.Slice(b.Properties, func(i, j int) bool {
+ if b.Properties[i].Type != b.Properties[j].Type {
+ return b.Properties[i].Type < b.Properties[j].Type
+ }
+ return string(b.Properties[i].Value) < string(b.Properties[j].Value)
+ })
+
+ bundles = append(bundles, *b)
+ }
+ return channels, bundles
+}
+
+func ModelRelatedImagesToRelatedImages(relatedImages []model.RelatedImage) []RelatedImage {
+ var out []RelatedImage
+ for _, ri := range relatedImages {
+ out = append(out, RelatedImage{
+ Name: ri.Name,
+ Image: ri.Image,
+ })
+ }
+ return out
+}
diff --git a/pkg/lib/declcfg/model_to_declcfg_test.go b/pkg/lib/declcfg/model_to_declcfg_test.go
new file mode 100644
index 000000000..a24a70661
--- /dev/null
+++ b/pkg/lib/declcfg/model_to_declcfg_test.go
@@ -0,0 +1,38 @@
+package declcfg
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/operator-framework/operator-registry/alpha/model"
+)
+
+func TestConvertFromModel(t *testing.T) {
+ type spec struct {
+ name string
+ m model.Model
+ expectCfg DeclarativeConfig
+ }
+
+ specs := []spec{
+ {
+ name: "Success",
+ m: buildTestModel(),
+ expectCfg: buildValidDeclarativeConfig(false),
+ },
+ }
+
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ s.m.Normalize()
+ assert.NoError(t, s.m.Validate())
+ actual := ConvertFromModel(s.m)
+
+ removeJSONWhitespace(&s.expectCfg)
+ removeJSONWhitespace(&actual)
+
+ assert.Equal(t, s.expectCfg, actual)
+ })
+ }
+}
diff --git a/pkg/lib/declcfg/write.go b/pkg/lib/declcfg/write.go
new file mode 100644
index 000000000..6eb616bf4
--- /dev/null
+++ b/pkg/lib/declcfg/write.go
@@ -0,0 +1,427 @@
+package declcfg
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "sort"
+ "strings"
+
+ "github.com/blang/semver/v4"
+ "github.com/operator-framework/operator-registry/alpha/property"
+ "k8s.io/apimachinery/pkg/util/sets"
+ "sigs.k8s.io/yaml"
+)
+
+type MermaidWriter struct {
+ MinEdgeName string
+ SpecifiedPackageName string
+}
+
+type MermaidOption func(*MermaidWriter)
+
+func NewMermaidWriter(opts ...MermaidOption) *MermaidWriter {
+ const (
+ minEdgeName = ""
+ specifiedPackageName = ""
+ )
+ m := &MermaidWriter{
+ MinEdgeName: minEdgeName,
+ SpecifiedPackageName: specifiedPackageName,
+ }
+
+ for _, opt := range opts {
+ opt(m)
+ }
+ return m
+}
+
+func WithMinEdgeName(minEdgeName string) MermaidOption {
+ return func(o *MermaidWriter) {
+ o.MinEdgeName = minEdgeName
+ }
+}
+
+func WithSpecifiedPackageName(specifiedPackageName string) MermaidOption {
+ return func(o *MermaidWriter) {
+ o.SpecifiedPackageName = specifiedPackageName
+ }
+}
+
+// writes out the channel edges of the declarative config graph in a mermaid format capable of being pasted into
+// mermaid renderers like github, mermaid.live, etc.
+// output is sorted lexicographically by package name, and then by channel name
+// if provided, minEdgeName will be used as the lower bound for edges in the output graph
+//
+// Example output:
+// graph LR
+//
+// %% package "neuvector-certified-operator-rhmp"
+// subgraph "neuvector-certified-operator-rhmp"
+// %% channel "beta"
+// subgraph neuvector-certified-operator-rhmp-beta["beta"]
+// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.8["neuvector-operator.v1.2.8"]
+// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.9["neuvector-operator.v1.2.9"]
+// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.3.0["neuvector-operator.v1.3.0"]
+// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.3.0["neuvector-operator.v1.3.0"]-- replaces --> neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.8["neuvector-operator.v1.2.8"]
+// neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.3.0["neuvector-operator.v1.3.0"]-- skips --> neuvector-certified-operator-rhmp-beta-neuvector-operator.v1.2.9["neuvector-operator.v1.2.9"]
+// end
+// end
+//
+// end
+func (writer *MermaidWriter) WriteChannels(cfg DeclarativeConfig, out io.Writer) error {
+ pkgs := map[string]*strings.Builder{}
+
+ sort.Slice(cfg.Channels, func(i, j int) bool {
+ return cfg.Channels[i].Name < cfg.Channels[j].Name
+ })
+
+ versionMap, err := getBundleVersions(&cfg)
+ if err != nil {
+ return err
+ }
+
+ // establish a 'floor' version, either specified by user or entirely open
+ minVersion := semver.Version{Major: 0, Minor: 0, Patch: 0}
+
+ if writer.MinEdgeName != "" {
+ if _, ok := versionMap[writer.MinEdgeName]; !ok {
+ return fmt.Errorf("unknown minimum edge name: %q", writer.MinEdgeName)
+ }
+ minVersion = versionMap[writer.MinEdgeName]
+ }
+
+ // build increasing-version-ordered bundle names, so we can meaningfully iterate over a range
+ orderedBundles := []string{}
+ for n, _ := range versionMap {
+ orderedBundles = append(orderedBundles, n)
+ }
+ sort.Slice(orderedBundles, func(i, j int) bool {
+ return versionMap[orderedBundles[i]].LT(versionMap[orderedBundles[j]])
+ })
+
+ minEdgePackage := writer.getMinEdgePackage(&cfg)
+
+ for _, c := range cfg.Channels {
+ filteredChannel := writer.filterChannel(&c, versionMap, minVersion, minEdgePackage)
+ if filteredChannel != nil {
+ pkgBuilder, ok := pkgs[c.Package]
+ if !ok {
+ pkgBuilder = &strings.Builder{}
+ pkgs[c.Package] = pkgBuilder
+ }
+
+ channelID := fmt.Sprintf("%s-%s", filteredChannel.Package, filteredChannel.Name)
+ pkgBuilder.WriteString(fmt.Sprintf(" %%%% channel %q\n", filteredChannel.Name))
+ pkgBuilder.WriteString(fmt.Sprintf(" subgraph %s[%q]\n", channelID, filteredChannel.Name))
+
+ for _, ce := range filteredChannel.Entries {
+ if versionMap[ce.Name].GE(minVersion) {
+ entryId := fmt.Sprintf("%s-%s", channelID, ce.Name)
+ pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]\n", entryId, ce.Name))
+
+ if len(ce.Replaces) > 0 {
+ replacesId := fmt.Sprintf("%s-%s", channelID, ce.Replaces)
+ pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- %s --> %s[%q]\n", entryId, ce.Name, "replaces", replacesId, ce.Replaces))
+ }
+ if len(ce.Skips) > 0 {
+ for _, s := range ce.Skips {
+ skipsId := fmt.Sprintf("%s-%s", channelID, s)
+ pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- %s --> %s[%q]\n", entryId, ce.Name, "skips", skipsId, s))
+ }
+ }
+ if len(ce.SkipRange) > 0 {
+ skipRange, err := semver.ParseRange(ce.SkipRange)
+ if err == nil {
+ for _, edgeName := range filteredChannel.Entries {
+ if skipRange(versionMap[edgeName.Name]) {
+ skipRangeId := fmt.Sprintf("%s-%s", channelID, edgeName.Name)
+ pkgBuilder.WriteString(fmt.Sprintf(" %s[%q]-- \"%s(%s)\" --> %s[%q]\n", entryId, ce.Name, "skipRange", ce.SkipRange, skipRangeId, edgeName.Name))
+ }
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "warning: ignoring invalid SkipRange for package/edge %q/%q: %v\n", c.Package, ce.Name, err)
+ }
+ }
+ }
+ }
+ pkgBuilder.WriteString(" end\n")
+ }
+ }
+
+ out.Write([]byte("graph LR\n"))
+ pkgNames := []string{}
+ for pname, _ := range pkgs {
+ pkgNames = append(pkgNames, pname)
+ }
+ sort.Slice(pkgNames, func(i, j int) bool {
+ return pkgNames[i] < pkgNames[j]
+ })
+ for _, pkgName := range pkgNames {
+ out.Write([]byte(fmt.Sprintf(" %%%% package %q\n", pkgName)))
+ out.Write([]byte(fmt.Sprintf(" subgraph %q\n", pkgName)))
+ out.Write([]byte(pkgs[pkgName].String()))
+ out.Write([]byte(" end\n"))
+ }
+
+ return nil
+}
+
+// filters the channel edges to include only those which are greater-than-or-equal to the edge named by startVersion
+// returns a nil channel if all edges are filtered out
+func (writer *MermaidWriter) filterChannel(c *Channel, versionMap map[string]semver.Version, minVersion semver.Version, minEdgePackage string) *Channel {
+ // short-circuit if no active filters
+ if writer.MinEdgeName == "" && writer.SpecifiedPackageName == "" {
+ return c
+ }
+
+ // short-circuit if channel's package doesn't match filter
+ if writer.SpecifiedPackageName != "" && c.Package != writer.SpecifiedPackageName {
+ return nil
+ }
+
+ // short-circuit if channel package is mismatch from filter
+ if minEdgePackage != "" && c.Package != minEdgePackage {
+ return nil
+ }
+
+ out := &Channel{Name: c.Name, Package: c.Package, Properties: c.Properties, Entries: []ChannelEntry{}}
+ for _, ce := range c.Entries {
+ filteredCe := ChannelEntry{Name: ce.Name}
+ if writer.MinEdgeName == "" {
+ // no minimum-edge specified
+ filteredCe.SkipRange = ce.SkipRange
+ filteredCe.Replaces = ce.Replaces
+ filteredCe.Skips = append(filteredCe.Skips, ce.Skips...)
+
+ // accumulate IFF there are any relevant skips/skipRange/replaces remaining or there never were any to begin with
+ // for the case where all skip/skipRange/replaces are retained, this is effectively the original edge with validated linkages
+ if len(filteredCe.Replaces) > 0 || len(filteredCe.Skips) > 0 || len(filteredCe.SkipRange) > 0 {
+ out.Entries = append(out.Entries, filteredCe)
+ } else {
+ if len(ce.Replaces) == 0 && len(ce.SkipRange) == 0 && len(ce.Skips) == 0 {
+ out.Entries = append(out.Entries, filteredCe)
+ }
+ }
+ } else {
+ if ce.Name == writer.MinEdgeName {
+ // edge is the 'floor', meaning that since all references are "backward references", and we don't want any references from this edge
+ // accumulate w/o references
+ out.Entries = append(out.Entries, filteredCe)
+ } else {
+ // edge needs to be filtered to determine if it is below the floor (bad) or on/above (good)
+ if len(ce.Replaces) > 0 && versionMap[ce.Replaces].GTE(minVersion) {
+ filteredCe.Replaces = ce.Replaces
+ }
+ if len(ce.Skips) > 0 {
+ filteredSkips := []string{}
+ for _, s := range ce.Skips {
+ if versionMap[s].GTE(minVersion) {
+ filteredSkips = append(filteredSkips, s)
+ }
+ }
+ if len(filteredSkips) > 0 {
+ filteredCe.Skips = filteredSkips
+ }
+ }
+ if len(ce.SkipRange) > 0 {
+ skipRange, err := semver.ParseRange(ce.SkipRange)
+ // if skipRange can't be parsed, just don't filter based on it
+ if err == nil && skipRange(minVersion) {
+ // specified range includes our floor
+ filteredCe.SkipRange = ce.SkipRange
+ }
+ }
+ // accumulate IFF there are any relevant skips/skipRange/replaces remaining, or there never were any to begin with (NOP)
+ // but the edge name satisfies the minimum-edge constraint
+ // for the case where all skip/skipRange/replaces are retained, this is effectively `ce` but with validated linkages
+ if len(filteredCe.Replaces) > 0 || len(filteredCe.Skips) > 0 || len(filteredCe.SkipRange) > 0 {
+ out.Entries = append(out.Entries, filteredCe)
+ } else {
+ if len(ce.Replaces) == 0 && len(ce.SkipRange) == 0 && len(ce.Skips) == 0 && versionMap[filteredCe.Name].GTE(minVersion) {
+ out.Entries = append(out.Entries, filteredCe)
+ }
+ }
+ }
+ }
+ }
+
+ if len(out.Entries) > 0 {
+ return out
+ } else {
+ return nil
+ }
+}
+
+func parseVersionProperty(b *Bundle) (*semver.Version, error) {
+ props, err := property.Parse(b.Properties)
+ if err != nil {
+ return nil, fmt.Errorf("parse properties for bundle %q: %v", b.Name, err)
+ }
+ if len(props.Packages) != 1 {
+ return nil, fmt.Errorf("bundle %q has multiple %q properties, expected exactly 1", b.Name, property.TypePackage)
+ }
+ v, err := semver.Parse(props.Packages[0].Version)
+ if err != nil {
+ return nil, fmt.Errorf("bundle %q has invalid version %q: %v", b.Name, props.Packages[0].Version, err)
+ }
+
+ return &v, nil
+}
+
+func getBundleVersions(cfg *DeclarativeConfig) (map[string]semver.Version, error) {
+ entries := make(map[string]semver.Version)
+ for index := range cfg.Bundles {
+ if _, ok := entries[cfg.Bundles[index].Name]; !ok {
+ ver, err := parseVersionProperty(&cfg.Bundles[index])
+ if err != nil {
+ return entries, err
+ }
+ entries[cfg.Bundles[index].Name] = *ver
+ }
+ }
+
+ return entries, nil
+}
+
+func (writer *MermaidWriter) getMinEdgePackage(cfg *DeclarativeConfig) string {
+ if writer.MinEdgeName == "" {
+ return ""
+ }
+
+ for _, c := range cfg.Channels {
+ for _, ce := range c.Entries {
+ if writer.MinEdgeName == ce.Name {
+ return c.Package
+ }
+ }
+ }
+
+ return ""
+}
+
+func WriteJSON(cfg DeclarativeConfig, w io.Writer) error {
+ enc := json.NewEncoder(w)
+ enc.SetIndent("", " ")
+ enc.SetEscapeHTML(false)
+ return writeToEncoder(cfg, enc)
+}
+
+func WriteYAML(cfg DeclarativeConfig, w io.Writer) error {
+ enc := newYAMLEncoder(w)
+ enc.SetEscapeHTML(false)
+ return writeToEncoder(cfg, enc)
+}
+
+type yamlEncoder struct {
+ w io.Writer
+ escapeHTML bool
+}
+
+func newYAMLEncoder(w io.Writer) *yamlEncoder {
+ return &yamlEncoder{w, true}
+}
+
+func (e *yamlEncoder) SetEscapeHTML(on bool) {
+ e.escapeHTML = on
+}
+
+func (e *yamlEncoder) Encode(v interface{}) error {
+ var buf bytes.Buffer
+ enc := json.NewEncoder(&buf)
+ enc.SetEscapeHTML(e.escapeHTML)
+ if err := enc.Encode(v); err != nil {
+ return err
+ }
+ yamlData, err := yaml.JSONToYAML(buf.Bytes())
+ if err != nil {
+ return err
+ }
+ yamlData = append([]byte("---\n"), yamlData...)
+ _, err = e.w.Write(yamlData)
+ return err
+}
+
+type encoder interface {
+ Encode(interface{}) error
+}
+
+func writeToEncoder(cfg DeclarativeConfig, enc encoder) error {
+ pkgNames := sets.NewString()
+
+ packagesByName := map[string][]Package{}
+ for _, p := range cfg.Packages {
+ pkgName := p.Name
+ pkgNames.Insert(pkgName)
+ packagesByName[pkgName] = append(packagesByName[pkgName], p)
+ }
+ channelsByPackage := map[string][]Channel{}
+ for _, c := range cfg.Channels {
+ pkgName := c.Package
+ pkgNames.Insert(pkgName)
+ channelsByPackage[pkgName] = append(channelsByPackage[pkgName], c)
+ }
+ bundlesByPackage := map[string][]Bundle{}
+ for _, b := range cfg.Bundles {
+ pkgName := b.Package
+ pkgNames.Insert(pkgName)
+ bundlesByPackage[pkgName] = append(bundlesByPackage[pkgName], b)
+ }
+ othersByPackage := map[string][]Meta{}
+ for _, o := range cfg.Others {
+ pkgName := o.Package
+ pkgNames.Insert(pkgName)
+ othersByPackage[pkgName] = append(othersByPackage[pkgName], o)
+ }
+
+ for _, pName := range pkgNames.List() {
+ if len(pName) == 0 {
+ continue
+ }
+ pkgs := packagesByName[pName]
+ for _, p := range pkgs {
+ if err := enc.Encode(p); err != nil {
+ return err
+ }
+ }
+
+ channels := channelsByPackage[pName]
+ sort.Slice(channels, func(i, j int) bool {
+ return channels[i].Name < channels[j].Name
+ })
+ for _, c := range channels {
+ if err := enc.Encode(c); err != nil {
+ return err
+ }
+ }
+
+ bundles := bundlesByPackage[pName]
+ sort.Slice(bundles, func(i, j int) bool {
+ return bundles[i].Name < bundles[j].Name
+ })
+ for _, b := range bundles {
+ if err := enc.Encode(b); err != nil {
+ return err
+ }
+ }
+
+ others := othersByPackage[pName]
+ sort.SliceStable(others, func(i, j int) bool {
+ return others[i].Schema < others[j].Schema
+ })
+ for _, o := range others {
+ if err := enc.Encode(o); err != nil {
+ return err
+ }
+ }
+ }
+
+ for _, o := range othersByPackage[""] {
+ if err := enc.Encode(o); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/pkg/lib/declcfg/write_test.go b/pkg/lib/declcfg/write_test.go
new file mode 100644
index 000000000..46f6d68f8
--- /dev/null
+++ b/pkg/lib/declcfg/write_test.go
@@ -0,0 +1,565 @@
+package declcfg
+
+import (
+ "bytes"
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestWriteJSON(t *testing.T) {
+ type spec struct {
+ name string
+ cfg DeclarativeConfig
+ expected string
+ }
+ specs := []spec{
+ {
+ name: "Success",
+ cfg: buildValidDeclarativeConfig(true),
+ expected: `{
+ "schema": "olm.package",
+ "name": "anakin",
+ "defaultChannel": "dark",
+ "icon": {
+ "base64data": "PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PGNpcmNsZSBjeD0iMjUiIGN5PSIyNSIgcj0iMjUiLz48L3N2Zz4=",
+ "mediatype": "image/svg+xml"
+ },
+ "description": "anakin operator"
+}
+{
+ "schema": "olm.channel",
+ "name": "dark",
+ "package": "anakin",
+ "entries": [
+ {
+ "name": "anakin.v0.0.1"
+ },
+ {
+ "name": "anakin.v0.1.0",
+ "replaces": "anakin.v0.0.1"
+ },
+ {
+ "name": "anakin.v0.1.1",
+ "replaces": "anakin.v0.0.1",
+ "skips": [
+ "anakin.v0.1.0"
+ ]
+ }
+ ]
+}
+{
+ "schema": "olm.channel",
+ "name": "light",
+ "package": "anakin",
+ "entries": [
+ {
+ "name": "anakin.v0.0.1"
+ },
+ {
+ "name": "anakin.v0.1.0",
+ "replaces": "anakin.v0.0.1"
+ }
+ ]
+}
+{
+ "schema": "olm.bundle",
+ "name": "anakin.v0.0.1",
+ "package": "anakin",
+ "image": "anakin-bundle:v0.0.1",
+ "properties": [
+ {
+ "type": "olm.bundle.object",
+ "value": {
+ "data": "eyJraW5kIjogIkN1c3RvbVJlc291cmNlRGVmaW5pdGlvbiIsICJhcGlWZXJzaW9uIjogImFwaWV4dGVuc2lvbnMuazhzLmlvL3YxIn0="
+ }
+ },
+ {
+ "type": "olm.bundle.object",
+ "value": {
+ "ref": "objects/anakin.v0.0.1.csv.yaml"
+ }
+ },
+ {
+ "type": "olm.package",
+ "value": {
+ "packageName": "anakin",
+ "version": "0.0.1"
+ }
+ }
+ ],
+ "relatedImages": [
+ {
+ "name": "bundle",
+ "image": "anakin-bundle:v0.0.1"
+ }
+ ]
+}
+{
+ "schema": "olm.bundle",
+ "name": "anakin.v0.1.0",
+ "package": "anakin",
+ "image": "anakin-bundle:v0.1.0",
+ "properties": [
+ {
+ "type": "olm.bundle.object",
+ "value": {
+ "data": "eyJraW5kIjogIkN1c3RvbVJlc291cmNlRGVmaW5pdGlvbiIsICJhcGlWZXJzaW9uIjogImFwaWV4dGVuc2lvbnMuazhzLmlvL3YxIn0="
+ }
+ },
+ {
+ "type": "olm.bundle.object",
+ "value": {
+ "ref": "objects/anakin.v0.1.0.csv.yaml"
+ }
+ },
+ {
+ "type": "olm.package",
+ "value": {
+ "packageName": "anakin",
+ "version": "0.1.0"
+ }
+ }
+ ],
+ "relatedImages": [
+ {
+ "name": "bundle",
+ "image": "anakin-bundle:v0.1.0"
+ }
+ ]
+}
+{
+ "schema": "olm.bundle",
+ "name": "anakin.v0.1.1",
+ "package": "anakin",
+ "image": "anakin-bundle:v0.1.1",
+ "properties": [
+ {
+ "type": "olm.bundle.object",
+ "value": {
+ "data": "eyJraW5kIjogIkN1c3RvbVJlc291cmNlRGVmaW5pdGlvbiIsICJhcGlWZXJzaW9uIjogImFwaWV4dGVuc2lvbnMuazhzLmlvL3YxIn0="
+ }
+ },
+ {
+ "type": "olm.bundle.object",
+ "value": {
+ "ref": "objects/anakin.v0.1.1.csv.yaml"
+ }
+ },
+ {
+ "type": "olm.package",
+ "value": {
+ "packageName": "anakin",
+ "version": "0.1.1"
+ }
+ }
+ ],
+ "relatedImages": [
+ {
+ "name": "bundle",
+ "image": "anakin-bundle:v0.1.1"
+ }
+ ]
+}
+{
+ "myField": "foobar",
+ "package": "anakin",
+ "schema": "custom.3"
+}
+{
+ "schema": "olm.package",
+ "name": "boba-fett",
+ "defaultChannel": "mando",
+ "icon": {
+ "base64data": "PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iNTAiLz48L3N2Zz4=",
+ "mediatype": "image/svg+xml"
+ },
+ "description": "boba-fett operator"
+}
+{
+ "schema": "olm.channel",
+ "name": "mando",
+ "package": "boba-fett",
+ "entries": [
+ {
+ "name": "boba-fett.v1.0.0"
+ },
+ {
+ "name": "boba-fett.v2.0.0",
+ "replaces": "boba-fett.v1.0.0"
+ }
+ ]
+}
+{
+ "schema": "olm.bundle",
+ "name": "boba-fett.v1.0.0",
+ "package": "boba-fett",
+ "image": "boba-fett-bundle:v1.0.0",
+ "properties": [
+ {
+ "type": "olm.bundle.object",
+ "value": {
+ "data": "eyJraW5kIjogIkN1c3RvbVJlc291cmNlRGVmaW5pdGlvbiIsICJhcGlWZXJzaW9uIjogImFwaWV4dGVuc2lvbnMuazhzLmlvL3YxIn0="
+ }
+ },
+ {
+ "type": "olm.bundle.object",
+ "value": {
+ "ref": "objects/boba-fett.v1.0.0.csv.yaml"
+ }
+ },
+ {
+ "type": "olm.package",
+ "value": {
+ "packageName": "boba-fett",
+ "version": "1.0.0"
+ }
+ }
+ ],
+ "relatedImages": [
+ {
+ "name": "bundle",
+ "image": "boba-fett-bundle:v1.0.0"
+ }
+ ]
+}
+{
+ "schema": "olm.bundle",
+ "name": "boba-fett.v2.0.0",
+ "package": "boba-fett",
+ "image": "boba-fett-bundle:v2.0.0",
+ "properties": [
+ {
+ "type": "olm.bundle.object",
+ "value": {
+ "data": "eyJraW5kIjogIkN1c3RvbVJlc291cmNlRGVmaW5pdGlvbiIsICJhcGlWZXJzaW9uIjogImFwaWV4dGVuc2lvbnMuazhzLmlvL3YxIn0="
+ }
+ },
+ {
+ "type": "olm.bundle.object",
+ "value": {
+ "ref": "objects/boba-fett.v2.0.0.csv.yaml"
+ }
+ },
+ {
+ "type": "olm.package",
+ "value": {
+ "packageName": "boba-fett",
+ "version": "2.0.0"
+ }
+ }
+ ],
+ "relatedImages": [
+ {
+ "name": "bundle",
+ "image": "boba-fett-bundle:v2.0.0"
+ }
+ ]
+}
+{
+ "myField": "foobar",
+ "package": "boba-fett",
+ "schema": "custom.3"
+}
+{
+ "schema": "custom.1"
+}
+{
+ "schema": "custom.2"
+}
+`,
+ },
+ }
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ var buf bytes.Buffer
+ err := WriteJSON(s.cfg, &buf)
+ require.NoError(t, err)
+ require.Equal(t, s.expected, buf.String())
+ })
+ }
+}
+
+func TestWriteYAML(t *testing.T) {
+ type spec struct {
+ name string
+ cfg DeclarativeConfig
+ expected string
+ }
+ specs := []spec{
+ {
+ name: "Success",
+ cfg: buildValidDeclarativeConfig(true),
+ expected: `---
+defaultChannel: dark
+description: anakin operator
+icon:
+ base64data: PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PGNpcmNsZSBjeD0iMjUiIGN5PSIyNSIgcj0iMjUiLz48L3N2Zz4=
+ mediatype: image/svg+xml
+name: anakin
+schema: olm.package
+---
+entries:
+- name: anakin.v0.0.1
+- name: anakin.v0.1.0
+ replaces: anakin.v0.0.1
+- name: anakin.v0.1.1
+ replaces: anakin.v0.0.1
+ skips:
+ - anakin.v0.1.0
+name: dark
+package: anakin
+schema: olm.channel
+---
+entries:
+- name: anakin.v0.0.1
+- name: anakin.v0.1.0
+ replaces: anakin.v0.0.1
+name: light
+package: anakin
+schema: olm.channel
+---
+image: anakin-bundle:v0.0.1
+name: anakin.v0.0.1
+package: anakin
+properties:
+- type: olm.bundle.object
+ value:
+ data: eyJraW5kIjogIkN1c3RvbVJlc291cmNlRGVmaW5pdGlvbiIsICJhcGlWZXJzaW9uIjogImFwaWV4dGVuc2lvbnMuazhzLmlvL3YxIn0=
+- type: olm.bundle.object
+ value:
+ ref: objects/anakin.v0.0.1.csv.yaml
+- type: olm.package
+ value:
+ packageName: anakin
+ version: 0.0.1
+relatedImages:
+- image: anakin-bundle:v0.0.1
+ name: bundle
+schema: olm.bundle
+---
+image: anakin-bundle:v0.1.0
+name: anakin.v0.1.0
+package: anakin
+properties:
+- type: olm.bundle.object
+ value:
+ data: eyJraW5kIjogIkN1c3RvbVJlc291cmNlRGVmaW5pdGlvbiIsICJhcGlWZXJzaW9uIjogImFwaWV4dGVuc2lvbnMuazhzLmlvL3YxIn0=
+- type: olm.bundle.object
+ value:
+ ref: objects/anakin.v0.1.0.csv.yaml
+- type: olm.package
+ value:
+ packageName: anakin
+ version: 0.1.0
+relatedImages:
+- image: anakin-bundle:v0.1.0
+ name: bundle
+schema: olm.bundle
+---
+image: anakin-bundle:v0.1.1
+name: anakin.v0.1.1
+package: anakin
+properties:
+- type: olm.bundle.object
+ value:
+ data: eyJraW5kIjogIkN1c3RvbVJlc291cmNlRGVmaW5pdGlvbiIsICJhcGlWZXJzaW9uIjogImFwaWV4dGVuc2lvbnMuazhzLmlvL3YxIn0=
+- type: olm.bundle.object
+ value:
+ ref: objects/anakin.v0.1.1.csv.yaml
+- type: olm.package
+ value:
+ packageName: anakin
+ version: 0.1.1
+relatedImages:
+- image: anakin-bundle:v0.1.1
+ name: bundle
+schema: olm.bundle
+---
+myField: foobar
+package: anakin
+schema: custom.3
+---
+defaultChannel: mando
+description: boba-fett operator
+icon:
+ base64data: PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iNTAiLz48L3N2Zz4=
+ mediatype: image/svg+xml
+name: boba-fett
+schema: olm.package
+---
+entries:
+- name: boba-fett.v1.0.0
+- name: boba-fett.v2.0.0
+ replaces: boba-fett.v1.0.0
+name: mando
+package: boba-fett
+schema: olm.channel
+---
+image: boba-fett-bundle:v1.0.0
+name: boba-fett.v1.0.0
+package: boba-fett
+properties:
+- type: olm.bundle.object
+ value:
+ data: eyJraW5kIjogIkN1c3RvbVJlc291cmNlRGVmaW5pdGlvbiIsICJhcGlWZXJzaW9uIjogImFwaWV4dGVuc2lvbnMuazhzLmlvL3YxIn0=
+- type: olm.bundle.object
+ value:
+ ref: objects/boba-fett.v1.0.0.csv.yaml
+- type: olm.package
+ value:
+ packageName: boba-fett
+ version: 1.0.0
+relatedImages:
+- image: boba-fett-bundle:v1.0.0
+ name: bundle
+schema: olm.bundle
+---
+image: boba-fett-bundle:v2.0.0
+name: boba-fett.v2.0.0
+package: boba-fett
+properties:
+- type: olm.bundle.object
+ value:
+ data: eyJraW5kIjogIkN1c3RvbVJlc291cmNlRGVmaW5pdGlvbiIsICJhcGlWZXJzaW9uIjogImFwaWV4dGVuc2lvbnMuazhzLmlvL3YxIn0=
+- type: olm.bundle.object
+ value:
+ ref: objects/boba-fett.v2.0.0.csv.yaml
+- type: olm.package
+ value:
+ packageName: boba-fett
+ version: 2.0.0
+relatedImages:
+- image: boba-fett-bundle:v2.0.0
+ name: bundle
+schema: olm.bundle
+---
+myField: foobar
+package: boba-fett
+schema: custom.3
+---
+schema: custom.1
+---
+schema: custom.2
+`,
+ },
+ }
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ var buf bytes.Buffer
+ err := WriteYAML(s.cfg, &buf)
+ require.NoError(t, err)
+ require.Equal(t, s.expected, buf.String())
+ })
+ }
+}
+
+func removeJSONWhitespace(cfg *DeclarativeConfig) {
+ for ib := range cfg.Bundles {
+ for ip := range cfg.Bundles[ib].Properties {
+ var buf bytes.Buffer
+ json.Compact(&buf, cfg.Bundles[ib].Properties[ip].Value)
+ cfg.Bundles[ib].Properties[ip].Value = buf.Bytes()
+ }
+ }
+ for io := range cfg.Others {
+ var buf bytes.Buffer
+ json.Compact(&buf, cfg.Others[io].Blob)
+ cfg.Others[io].Blob = buf.Bytes()
+ }
+}
+
+func TestWriteMermaidChannels(t *testing.T) {
+ type spec struct {
+ name string
+ cfg DeclarativeConfig
+ startEdge string
+ packageFilter string
+ expected string
+ }
+ specs := []spec{
+ {
+ name: "SuccessNoFilters",
+ cfg: buildValidDeclarativeConfig(true),
+ startEdge: "",
+ packageFilter: "",
+ expected: `graph LR
+ %% package "anakin"
+ subgraph "anakin"
+ %% channel "dark"
+ subgraph anakin-dark["dark"]
+ anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]
+ anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
+ anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]-- replaces --> anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]
+ anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
+ anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]-- replaces --> anakin-dark-anakin.v0.0.1["anakin.v0.0.1"]
+ anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]-- skips --> anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
+ end
+ %% channel "light"
+ subgraph anakin-light["light"]
+ anakin-light-anakin.v0.0.1["anakin.v0.0.1"]
+ anakin-light-anakin.v0.1.0["anakin.v0.1.0"]
+ anakin-light-anakin.v0.1.0["anakin.v0.1.0"]-- replaces --> anakin-light-anakin.v0.0.1["anakin.v0.0.1"]
+ end
+ end
+ %% package "boba-fett"
+ subgraph "boba-fett"
+ %% channel "mando"
+ subgraph boba-fett-mando["mando"]
+ boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
+ boba-fett-mando-boba-fett.v2.0.0["boba-fett.v2.0.0"]
+ boba-fett-mando-boba-fett.v2.0.0["boba-fett.v2.0.0"]-- replaces --> boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
+ end
+ end
+`,
+ },
+ {
+ name: "SuccessMinEdgeFilter",
+ cfg: buildValidDeclarativeConfig(true),
+ startEdge: "anakin.v0.1.0",
+ packageFilter: "",
+ expected: `graph LR
+ %% package "anakin"
+ subgraph "anakin"
+ %% channel "dark"
+ subgraph anakin-dark["dark"]
+ anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
+ anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]
+ anakin-dark-anakin.v0.1.1["anakin.v0.1.1"]-- skips --> anakin-dark-anakin.v0.1.0["anakin.v0.1.0"]
+ end
+ %% channel "light"
+ subgraph anakin-light["light"]
+ anakin-light-anakin.v0.1.0["anakin.v0.1.0"]
+ end
+ end
+`,
+ },
+ {
+ name: "SuccessPackageNameFilter",
+ cfg: buildValidDeclarativeConfig(true),
+ startEdge: "",
+ packageFilter: "boba-fett",
+ expected: `graph LR
+ %% package "boba-fett"
+ subgraph "boba-fett"
+ %% channel "mando"
+ subgraph boba-fett-mando["mando"]
+ boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
+ boba-fett-mando-boba-fett.v2.0.0["boba-fett.v2.0.0"]
+ boba-fett-mando-boba-fett.v2.0.0["boba-fett.v2.0.0"]-- replaces --> boba-fett-mando-boba-fett.v1.0.0["boba-fett.v1.0.0"]
+ end
+ end
+`,
+ },
+ }
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ var buf bytes.Buffer
+ writer := NewMermaidWriter(WithMinEdgeName(s.startEdge), WithSpecifiedPackageName(s.packageFilter))
+ err := writer.WriteChannels(s.cfg, &buf)
+ require.NoError(t, err)
+ require.Equal(t, s.expected, buf.String())
+ })
+ }
+}
diff --git a/pkg/lib/model/error.go b/pkg/lib/model/error.go
new file mode 100644
index 000000000..0ad0f7adb
--- /dev/null
+++ b/pkg/lib/model/error.go
@@ -0,0 +1,66 @@
+package model
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+)
+
+type validationError struct {
+ message string
+ subErrors []error
+}
+
+func newValidationError(message string) *validationError {
+ return &validationError{message: message}
+}
+
+func (v *validationError) orNil() error {
+ if len(v.subErrors) == 0 {
+ return nil
+ }
+ return v
+}
+
+func (v *validationError) Error() string {
+ if v == nil {
+ return ""
+ }
+ return strings.TrimSpace(v.errorPrefix(nil, true, nil))
+}
+
+func (v *validationError) errorPrefix(prefix []rune, last bool, seen []error) string {
+ for _, s := range seen {
+ if v == s {
+ return ""
+ }
+ }
+ seen = append(seen, v)
+ sep := ":\n"
+ if len(v.subErrors) == 0 {
+ sep = "\n"
+ }
+ errMsg := bytes.NewBufferString(fmt.Sprintf("%s%s%s", string(prefix), v.message, sep))
+ for i, serr := range v.subErrors {
+ subPrefix := prefix
+ if len(subPrefix) >= 4 {
+ if last {
+ subPrefix = append(subPrefix[0:len(subPrefix)-4], []rune(" ")...)
+ } else {
+ subPrefix = append(subPrefix[0:len(subPrefix)-4], []rune("│ ")...)
+ }
+ }
+ subLast := i == len(v.subErrors)-1
+ if subLast {
+ subPrefix = append(subPrefix, []rune("└── ")...)
+ } else {
+ subPrefix = append(subPrefix, []rune("├── ")...)
+ }
+ if verr, ok := serr.(*validationError); ok {
+ errMsg.WriteString(verr.errorPrefix(subPrefix, subLast, seen))
+ } else {
+ errMsg.WriteString(fmt.Sprintf("%s%s\n", string(subPrefix), serr))
+ }
+ }
+ return errMsg.String()
+}
diff --git a/pkg/lib/model/error_test.go b/pkg/lib/model/error_test.go
new file mode 100644
index 000000000..3f53aefd0
--- /dev/null
+++ b/pkg/lib/model/error_test.go
@@ -0,0 +1,129 @@
+package model
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestValidationError_Error(t *testing.T) {
+ type spec struct {
+ name string
+ err *validationError
+ expect string
+ }
+
+ recursiveErr := &validationError{
+ message: "l1",
+ }
+ recursiveErr.subErrors = []error{
+ fmt.Errorf("err1"),
+ &validationError{
+ message: "l2",
+ subErrors: []error{
+ fmt.Errorf("err3"),
+ recursiveErr,
+ fmt.Errorf("err4"),
+ },
+ },
+ fmt.Errorf("err2"),
+ }
+
+ specs := []spec{
+ {
+ name: "Nil",
+ err: nil,
+ expect: "",
+ },
+ {
+ name: "Empty",
+ err: &validationError{},
+ expect: "",
+ },
+ {
+ name: "RecursiveError",
+ err: recursiveErr,
+ expect: `l1:
+├── err1
+├── l2:
+│ ├── err3
+│ └── err4
+└── err2`,
+ },
+ {
+ name: "MessageOnly",
+ err: &validationError{message: "hello"},
+ expect: "hello",
+ },
+ {
+ name: "WithSubErrors",
+ err: &validationError{
+ message: "hello",
+ subErrors: []error{
+ fmt.Errorf("world"),
+ fmt.Errorf("foobar"),
+ }},
+ expect: `hello:
+├── world
+└── foobar`,
+ },
+ {
+ name: "WithEmptyLeafSubErrors",
+ err: &validationError{
+ message: "hello",
+ subErrors: []error{
+ &validationError{
+ message: "foo",
+ subErrors: []error{},
+ },
+ &validationError{
+ message: "bar",
+ subErrors: []error{
+ fmt.Errorf("bar1"),
+ fmt.Errorf("bar2"),
+ },
+ },
+ }},
+ expect: `hello:
+├── foo
+└── bar:
+ ├── bar1
+ └── bar2`,
+ },
+ {
+ name: "WithSubSubErrors",
+ err: &validationError{
+ message: "hello",
+ subErrors: []error{
+ &validationError{
+ message: "foo",
+ subErrors: []error{
+ fmt.Errorf("foo1"),
+ fmt.Errorf("foo2"),
+ },
+ },
+ &validationError{
+ message: "bar",
+ subErrors: []error{
+ fmt.Errorf("bar1"),
+ fmt.Errorf("bar2"),
+ },
+ },
+ }},
+ expect: `hello:
+├── foo:
+│ ├── foo1
+│ └── foo2
+└── bar:
+ ├── bar1
+ └── bar2`,
+ },
+ }
+
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ require.Equal(t, s.expect, s.err.Error())
+ })
+ }
+}
diff --git a/pkg/lib/model/model.go b/pkg/lib/model/model.go
new file mode 100644
index 000000000..48861e49c
--- /dev/null
+++ b/pkg/lib/model/model.go
@@ -0,0 +1,376 @@
+package model
+
+import (
+ "errors"
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/blang/semver/v4"
+ "github.com/h2non/filetype"
+ "github.com/h2non/filetype/matchers"
+ "github.com/h2non/filetype/types"
+ svg "github.com/h2non/go-is-svg"
+ "k8s.io/apimachinery/pkg/util/sets"
+
+ "github.com/operator-framework/operator-registry/alpha/property"
+)
+
+func init() {
+ t := types.NewType("svg", "image/svg+xml")
+ filetype.AddMatcher(t, svg.Is)
+ matchers.Image[types.NewType("svg", "image/svg+xml")] = svg.Is
+}
+
+type Model map[string]*Package
+
+func (m Model) Validate() error {
+ result := newValidationError("invalid index")
+
+ for name, pkg := range m {
+ if name != pkg.Name {
+ result.subErrors = append(result.subErrors, fmt.Errorf("package key %q does not match package name %q", name, pkg.Name))
+ }
+ if err := pkg.Validate(); err != nil {
+ result.subErrors = append(result.subErrors, err)
+ }
+ }
+ return result.orNil()
+}
+
+type Package struct {
+ Name string
+ Description string
+ Icon *Icon
+ DefaultChannel *Channel
+ Channels map[string]*Channel
+}
+
+func (m *Package) Validate() error {
+ result := newValidationError(fmt.Sprintf("invalid package %q", m.Name))
+
+ if m.Name == "" {
+ result.subErrors = append(result.subErrors, errors.New("package name must not be empty"))
+ }
+
+ if err := m.Icon.Validate(); err != nil {
+ result.subErrors = append(result.subErrors, err)
+ }
+
+ if m.DefaultChannel == nil {
+ result.subErrors = append(result.subErrors, fmt.Errorf("default channel must be set"))
+ }
+
+ if len(m.Channels) == 0 {
+ result.subErrors = append(result.subErrors, fmt.Errorf("package must contain at least one channel"))
+ }
+
+ foundDefault := false
+ for name, ch := range m.Channels {
+ if name != ch.Name {
+ result.subErrors = append(result.subErrors, fmt.Errorf("channel key %q does not match channel name %q", name, ch.Name))
+ }
+ if err := ch.Validate(); err != nil {
+ result.subErrors = append(result.subErrors, err)
+ }
+ if ch == m.DefaultChannel {
+ foundDefault = true
+ }
+ if ch.Package != m {
+ result.subErrors = append(result.subErrors, fmt.Errorf("channel %q not correctly linked to parent package", ch.Name))
+ }
+ }
+
+ if m.DefaultChannel != nil && !foundDefault {
+ result.subErrors = append(result.subErrors, fmt.Errorf("default channel %q not found in channels list", m.DefaultChannel.Name))
+ }
+ return result.orNil()
+}
+
+type Icon struct {
+ Data []byte
+ MediaType string
+}
+
+func (i *Icon) Validate() error {
+ if i == nil {
+ return nil
+ }
+ // TODO(joelanford): Should we check that data and mediatype are set,
+ // and detect the media type of the data and compare it to the
+ // mediatype listed in the icon field? Currently, some production
+ // index databases are failing these tests, so leaving this
+ // commented out for now.
+ result := newValidationError("invalid icon")
+ //if len(i.Data) == 0 {
+ // result.subErrors = append(result.subErrors, errors.New("icon data must be set if icon is defined"))
+ //}
+ //if len(i.MediaType) == 0 {
+ // result.subErrors = append(result.subErrors, errors.New("icon mediatype must be set if icon is defined"))
+ //}
+ //if len(i.Data) > 0 {
+ // if err := i.validateData(); err != nil {
+ // result.subErrors = append(result.subErrors, err)
+ // }
+ //}
+ return result.orNil()
+}
+
+func (i *Icon) validateData() error {
+ if !filetype.IsImage(i.Data) {
+ return errors.New("icon data is not an image")
+ }
+ t, err := filetype.Match(i.Data)
+ if err != nil {
+ return err
+ }
+ if t.MIME.Value != i.MediaType {
+ return fmt.Errorf("icon media type %q does not match detected media type %q", i.MediaType, t.MIME.Value)
+ }
+ return nil
+}
+
+type Channel struct {
+ Package *Package
+ Name string
+ Bundles map[string]*Bundle
+ // NOTICE: The field Properties of the type Channel is for internal use only.
+ // DO NOT use it for any public-facing functionalities.
+ // This API is in alpha stage and it is subject to change.
+ Properties []property.Property
+}
+
+// TODO(joelanford): This function determines the channel head by finding the bundle that has 0
+//
+// incoming edges, based on replaces and skips. It also expects to find exactly one such bundle.
+// Is this the correct algorithm?
+func (c Channel) Head() (*Bundle, error) {
+ incoming := map[string]int{}
+ for _, b := range c.Bundles {
+ if b.Replaces != "" {
+ incoming[b.Replaces]++
+ }
+ for _, skip := range b.Skips {
+ incoming[skip]++
+ }
+ }
+ var heads []*Bundle
+ for _, b := range c.Bundles {
+ if _, ok := incoming[b.Name]; !ok {
+ heads = append(heads, b)
+ }
+ }
+ if len(heads) == 0 {
+ return nil, fmt.Errorf("no channel head found in graph")
+ }
+ if len(heads) > 1 {
+ var headNames []string
+ for _, head := range heads {
+ headNames = append(headNames, head.Name)
+ }
+ sort.Strings(headNames)
+ return nil, fmt.Errorf("multiple channel heads found in graph: %s", strings.Join(headNames, ", "))
+ }
+ return heads[0], nil
+}
+
+func (c *Channel) Validate() error {
+ result := newValidationError(fmt.Sprintf("invalid channel %q", c.Name))
+
+ if c.Name == "" {
+ result.subErrors = append(result.subErrors, errors.New("channel name must not be empty"))
+ }
+
+ if c.Package == nil {
+ result.subErrors = append(result.subErrors, errors.New("package must be set"))
+ }
+
+ if len(c.Bundles) == 0 {
+ result.subErrors = append(result.subErrors, fmt.Errorf("channel must contain at least one bundle"))
+ }
+
+ if len(c.Bundles) > 0 {
+ if err := c.validateReplacesChain(); err != nil {
+ result.subErrors = append(result.subErrors, err)
+ }
+ }
+
+ for name, b := range c.Bundles {
+ if name != b.Name {
+ result.subErrors = append(result.subErrors, fmt.Errorf("bundle key %q does not match bundle name %q", name, b.Name))
+ }
+ if err := b.Validate(); err != nil {
+ result.subErrors = append(result.subErrors, err)
+ }
+ if b.Channel != c {
+ result.subErrors = append(result.subErrors, fmt.Errorf("bundle %q not correctly linked to parent channel", b.Name))
+ }
+ }
+ return result.orNil()
+}
+
+// validateReplacesChain checks the replaces chain of a channel.
+// Specifically the following rules must be followed:
+// 1. There must be exactly 1 channel head.
+// 2. Beginning at the head, the replaces chain must reach all non-skipped entries.
+// Non-skipped entries are defined as entries that are not skipped by any other entry in the channel.
+// 3. There must be no cycles in the replaces chain.
+// 4. The tail entry in the replaces chain is permitted to replace a non-existent entry.
+func (c *Channel) validateReplacesChain() error {
+ head, err := c.Head()
+ if err != nil {
+ return err
+ }
+
+ allBundles := sets.NewString()
+ skippedBundles := sets.NewString()
+ for _, b := range c.Bundles {
+ allBundles = allBundles.Insert(b.Name)
+ skippedBundles = skippedBundles.Insert(b.Skips...)
+ }
+
+ chainFrom := map[string][]string{}
+ replacesChainFromHead := sets.NewString(head.Name)
+ cur := head
+ for cur != nil {
+ if _, ok := chainFrom[cur.Name]; !ok {
+ chainFrom[cur.Name] = []string{cur.Name}
+ }
+ for k := range chainFrom {
+ chainFrom[k] = append(chainFrom[k], cur.Replaces)
+ }
+ if replacesChainFromHead.Has(cur.Replaces) {
+ return fmt.Errorf("detected cycle in replaces chain of upgrade graph: %s", strings.Join(chainFrom[cur.Replaces], " -> "))
+ }
+ replacesChainFromHead = replacesChainFromHead.Insert(cur.Replaces)
+ cur = c.Bundles[cur.Replaces]
+ }
+
+ strandedBundles := allBundles.Difference(replacesChainFromHead).Difference(skippedBundles).List()
+ if len(strandedBundles) > 0 {
+ return fmt.Errorf("channel contains one or more stranded bundles: %s", strings.Join(strandedBundles, ", "))
+ }
+
+ return nil
+}
+
+type Bundle struct {
+ Package *Package
+ Channel *Channel
+ Name string
+ Image string
+ Replaces string
+ Skips []string
+ SkipRange string
+ Properties []property.Property
+ RelatedImages []RelatedImage
+
+ // These fields are present so that we can continue serving
+ // the GRPC API the way packageserver expects us to in a
+ // backwards-compatible way.
+ Objects []string
+ CsvJSON string
+
+ // These fields are used to compare bundles in a diff.
+ PropertiesP *property.Properties
+ Version semver.Version
+}
+
+func (b *Bundle) Validate() error {
+ result := newValidationError(fmt.Sprintf("invalid bundle %q", b.Name))
+
+ if b.Name == "" {
+ result.subErrors = append(result.subErrors, errors.New("name must be set"))
+ }
+ if b.Channel == nil {
+ result.subErrors = append(result.subErrors, errors.New("channel must be set"))
+ }
+ if b.Package == nil {
+ result.subErrors = append(result.subErrors, errors.New("package must be set"))
+ }
+ if b.Channel != nil && b.Package != nil && b.Package != b.Channel.Package {
+ result.subErrors = append(result.subErrors, errors.New("package does not match channel's package"))
+ }
+ props, err := property.Parse(b.Properties)
+ if err != nil {
+ result.subErrors = append(result.subErrors, err)
+ }
+ for i, skip := range b.Skips {
+ if skip == "" {
+ result.subErrors = append(result.subErrors, fmt.Errorf("skip[%d] is empty", i))
+ }
+ }
+ // TODO(joelanford): Validate related images? It looks like some
+ // CSVs in production databases use incorrect fields ([name,value]
+ // instead of [name,image]), which results in empty image values.
+ // Example is in redhat-operators: 3scale-operator.v0.5.5
+ //for i, relatedImage := range b.RelatedImages {
+ // if err := relatedImage.Validate(); err != nil {
+ // result.subErrors = append(result.subErrors, WithIndex(i, err))
+ // }
+ //}
+
+ if props != nil && len(props.Packages) != 1 {
+ result.subErrors = append(result.subErrors, fmt.Errorf("must be exactly one property with type %q", property.TypePackage))
+ }
+
+ if b.Image == "" && len(b.Objects) == 0 {
+ result.subErrors = append(result.subErrors, errors.New("bundle image must be set"))
+ }
+
+ return result.orNil()
+}
+
+type RelatedImage struct {
+ Name string
+ Image string
+}
+
+func (i RelatedImage) Validate() error {
+ result := newValidationError("invalid related image")
+ if i.Image == "" {
+ result.subErrors = append(result.subErrors, fmt.Errorf("image must be set"))
+ }
+ return result.orNil()
+}
+
+func (m Model) Normalize() {
+ for _, pkg := range m {
+ for _, ch := range pkg.Channels {
+ for _, b := range ch.Bundles {
+ for i := range b.Properties {
+ // Ensure property value is encoded in a standard way.
+ if normalized, err := property.Build(&b.Properties[i]); err == nil {
+ b.Properties[i] = *normalized
+ }
+ }
+ }
+ }
+ }
+}
+
+func (m Model) AddBundle(b Bundle) {
+ if _, present := m[b.Package.Name]; !present {
+ m[b.Package.Name] = b.Package
+ }
+ p := m[b.Package.Name]
+ b.Package = p
+
+ if ch, ok := p.Channels[b.Channel.Name]; ok {
+ b.Channel = ch
+ ch.Bundles[b.Name] = &b
+ } else {
+ newCh := &Channel{
+ Name: b.Channel.Name,
+ Package: p,
+ Bundles: make(map[string]*Bundle),
+ }
+ b.Channel = newCh
+ newCh.Bundles[b.Name] = &b
+ p.Channels[newCh.Name] = newCh
+ }
+
+ if p.DefaultChannel == nil {
+ p.DefaultChannel = b.Channel
+ }
+}
diff --git a/pkg/lib/model/model_test.go b/pkg/lib/model/model_test.go
new file mode 100644
index 000000000..c286dcdb4
--- /dev/null
+++ b/pkg/lib/model/model_test.go
@@ -0,0 +1,739 @@
+package model
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/operator-framework/operator-registry/alpha/property"
+)
+
+type validator interface {
+ Validate() error
+}
+
+const svgData = `PHN2ZyB2aWV3Qm94PTAgMCAxMDAgMTAwPjxjaXJjbGUgY3g9MjUgY3k9MjUgcj0yNS8+PC9zdmc+`
+const pngData = `iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=`
+const jpegData = `/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigD//2Q==`
+
+func mustBase64Decode(in string) []byte {
+ out, err := base64.StdEncoding.DecodeString(in)
+ if err != nil {
+ panic(err)
+ }
+ return out
+}
+
+func TestNormalize(t *testing.T) {
+ b := &Bundle{}
+ pkgs := Model{
+ "anakin": {
+ Channels: map[string]*Channel{
+ "alpha": {
+ Bundles: map[string]*Bundle{
+ "anakin.v0.0.1": b,
+ },
+ },
+ },
+ },
+ }
+ t.Run("Success/IgnoreInvalid", func(t *testing.T) {
+ invalidJSON := json.RawMessage(`}`)
+ b.Properties = []property.Property{{Value: invalidJSON}}
+ pkgs.Normalize()
+ assert.Equal(t, invalidJSON, b.Properties[0].Value)
+ })
+
+ t.Run("Success/Unchanged", func(t *testing.T) {
+ unchanged := json.RawMessage(`{}`)
+ b.Properties = []property.Property{{Value: unchanged}}
+ pkgs.Normalize()
+ assert.Equal(t, unchanged, b.Properties[0].Value)
+ })
+
+ t.Run("Success/RemoveSpaces", func(t *testing.T) {
+ withWhitespace := json.RawMessage(` {
+ "foo": "bar"
+
+ } `)
+ expected := json.RawMessage(`{"foo":"bar"}`)
+ b.Properties = []property.Property{{Value: withWhitespace}}
+ pkgs.Normalize()
+ assert.Equal(t, expected, b.Properties[0].Value)
+ })
+}
+
+func TestChannelHead(t *testing.T) {
+ type spec struct {
+ name string
+ ch Channel
+ head *Bundle
+ assertion require.ErrorAssertionFunc
+ }
+
+ head := &Bundle{
+ Name: "anakin.v0.0.3",
+ Replaces: "anakin.v0.0.1",
+ Skips: []string{"anakin.v0.0.2"},
+ }
+
+ specs := []spec{
+ {
+ name: "Success/Valid",
+ ch: Channel{Bundles: map[string]*Bundle{
+ "anakin.v0.0.1": {Name: "anakin.v0.0.1"},
+ "anakin.v0.0.2": {Name: "anakin.v0.0.2"},
+ "anakin.v0.0.3": head,
+ }},
+ head: head,
+ assertion: require.NoError,
+ },
+ {
+ name: "Error/NoChannelHead",
+ ch: Channel{Bundles: map[string]*Bundle{
+ "anakin.v0.0.1": {Name: "anakin.v0.0.1", Replaces: "anakin.v0.0.3"},
+ "anakin.v0.0.3": head,
+ }},
+ assertion: hasError(`no channel head found in graph`),
+ },
+ {
+ name: "Error/MultipleChannelHeads",
+ ch: Channel{Bundles: map[string]*Bundle{
+ "anakin.v0.0.1": {Name: "anakin.v0.0.1"},
+ "anakin.v0.0.3": head,
+ "anakin.v0.0.4": {Name: "anakin.v0.0.4", Replaces: "anakin.v0.0.1"},
+ }},
+ assertion: hasError(`multiple channel heads found in graph: anakin.v0.0.3, anakin.v0.0.4`),
+ },
+ }
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ h, err := s.ch.Head()
+ assert.Equal(t, s.head, h)
+ s.assertion(t, err)
+ })
+ }
+}
+
+func TestValidReplacesChain(t *testing.T) {
+ type spec struct {
+ name string
+ ch Channel
+ assertion require.ErrorAssertionFunc
+ }
+ specs := []spec{
+ {
+ name: "Success/Valid",
+ ch: Channel{Bundles: map[string]*Bundle{
+ "anakin.v0.0.1": {Name: "anakin.v0.0.1"},
+ "anakin.v0.0.2": {Name: "anakin.v0.0.2", Skips: []string{"anakin.v0.0.1"}},
+ "anakin.v0.0.3": {Name: "anakin.v0.0.3", Skips: []string{"anakin.v0.0.2"}},
+ "anakin.v0.0.4": {Name: "anakin.v0.0.4", Replaces: "anakin.v0.0.3"},
+ }},
+ assertion: require.NoError,
+ },
+ {
+ name: "Error/CycleNoHops",
+ ch: Channel{Bundles: map[string]*Bundle{
+ "anakin.v0.0.4": {Name: "anakin.v0.0.4", Replaces: "anakin.v0.0.4"},
+ "anakin.v0.0.5": {Name: "anakin.v0.0.5", Replaces: "anakin.v0.0.4"},
+ }},
+ assertion: hasError(`detected cycle in replaces chain of upgrade graph: anakin.v0.0.4 -> anakin.v0.0.4`),
+ },
+ {
+ name: "Error/CycleMultipleHops",
+ ch: Channel{Bundles: map[string]*Bundle{
+ "anakin.v0.0.1": {Name: "anakin.v0.0.1", Replaces: "anakin.v0.0.3"},
+ "anakin.v0.0.2": {Name: "anakin.v0.0.2", Replaces: "anakin.v0.0.1"},
+ "anakin.v0.0.3": {Name: "anakin.v0.0.3", Replaces: "anakin.v0.0.2"},
+ "anakin.v0.0.4": {Name: "anakin.v0.0.4", Replaces: "anakin.v0.0.3"},
+ }},
+ assertion: hasError(`detected cycle in replaces chain of upgrade graph: anakin.v0.0.3 -> anakin.v0.0.2 -> anakin.v0.0.1 -> anakin.v0.0.3`),
+ },
+ {
+ name: "Error/Stranded",
+ ch: Channel{Bundles: map[string]*Bundle{
+ "anakin.v0.0.1": {Name: "anakin.v0.0.1"},
+ "anakin.v0.0.2": {Name: "anakin.v0.0.2", Replaces: "anakin.v0.0.1"},
+ "anakin.v0.0.3": {Name: "anakin.v0.0.3", Skips: []string{"anakin.v0.0.2"}},
+ }},
+ assertion: hasError(`channel contains one or more stranded bundles: anakin.v0.0.1`),
+ },
+ }
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ err := s.ch.validateReplacesChain()
+ s.assertion(t, err)
+ })
+ }
+}
+
+func hasError(expectedError string) require.ErrorAssertionFunc {
+ return func(t require.TestingT, actualError error, args ...interface{}) {
+ if stdt, ok := t.(*testing.T); ok {
+ stdt.Helper()
+ }
+ errsToCheck := []error{actualError}
+ for len(errsToCheck) > 0 {
+ var err error
+ err, errsToCheck = errsToCheck[0], errsToCheck[1:]
+ if err == nil {
+ continue
+ }
+ if verr, ok := err.(*validationError); ok {
+ if verr.message == expectedError {
+ return
+ }
+ errsToCheck = append(errsToCheck, verr.subErrors...)
+ } else if expectedError == err.Error() {
+ return
+ }
+ }
+ t.Errorf("expected error to be or contain suberror `%s`, got `%s`", expectedError, actualError)
+ t.FailNow()
+ }
+}
+
+func TestValidators(t *testing.T) {
+ type spec struct {
+ name string
+ v validator
+ assertion require.ErrorAssertionFunc
+ }
+
+ pkg, ch := makePackageChannelBundle()
+ pkgIncorrectDefaultChannel, _ := makePackageChannelBundle()
+ pkgIncorrectDefaultChannel.DefaultChannel = &Channel{Name: "not-found"}
+
+ var nilIcon *Icon = nil
+
+ specs := []spec{
+ {
+ name: "Model/Success/Valid",
+ v: Model{
+ pkg.Name: pkg,
+ },
+ assertion: require.NoError,
+ },
+ {
+ name: "Model/Error/PackageKeyNameMismatch",
+ v: Model{
+ "foo": pkg,
+ },
+ assertion: hasError(`package key "foo" does not match package name "anakin"`),
+ },
+ {
+ name: "Model/Error/InvalidPackage",
+ v: Model{
+ pkgIncorrectDefaultChannel.Name: pkgIncorrectDefaultChannel,
+ },
+ assertion: hasError(`invalid package "anakin"`),
+ },
+ {
+ name: "Package/Success/Valid",
+ v: pkg,
+ assertion: require.NoError,
+ },
+ {
+ name: "Package/Error/NoName",
+ v: &Package{},
+ assertion: hasError("package name must not be empty"),
+ },
+ //{
+ // name: "Package/Error/InvalidIcon",
+ // v: &Package{
+ // Name: "anakin",
+ // Icon: &Icon{Data: mustBase64Decode(svgData)},
+ // },
+ // assertion: hasError("icon mediatype must be set if icon is defined"),
+ //},
+ {
+ name: "Package/Error/NoChannels",
+ v: &Package{
+ Name: "anakin",
+ Icon: &Icon{Data: mustBase64Decode(svgData), MediaType: "image/svg+xml"},
+ },
+ assertion: hasError("package must contain at least one channel"),
+ },
+ {
+ name: "Package/Error/NoDefaultChannel",
+ v: &Package{
+ Name: "anakin",
+ Icon: &Icon{Data: mustBase64Decode(svgData), MediaType: "image/svg+xml"},
+ Channels: map[string]*Channel{"light": ch},
+ },
+ assertion: hasError("default channel must be set"),
+ },
+ {
+ name: "Package/Error/ChannelKeyNameMismatch",
+ v: &Package{
+ Name: "anakin",
+ Icon: &Icon{Data: mustBase64Decode(svgData), MediaType: "image/svg+xml"},
+ DefaultChannel: ch,
+ Channels: map[string]*Channel{"dark": ch},
+ },
+ assertion: hasError(`channel key "dark" does not match channel name "light"`),
+ },
+ {
+ name: "Package/Error/InvalidChannel",
+ v: &Package{
+ Name: "anakin",
+ Icon: &Icon{Data: mustBase64Decode(svgData), MediaType: "image/svg+xml"},
+ DefaultChannel: ch,
+ Channels: map[string]*Channel{"light": {Name: "light"}},
+ },
+ assertion: hasError(`invalid channel "light"`),
+ },
+ {
+ name: "Package/Error/InvalidChannelPackageLink",
+ v: &Package{
+ Name: "anakin",
+ Icon: &Icon{Data: mustBase64Decode(svgData), MediaType: "image/svg+xml"},
+ DefaultChannel: ch,
+ Channels: map[string]*Channel{"light": ch},
+ },
+ assertion: hasError(`channel "light" not correctly linked to parent package`),
+ },
+ {
+ name: "Package/Error/DefaultChannelNotInChannelMap",
+ v: pkgIncorrectDefaultChannel,
+ assertion: hasError(`default channel "not-found" not found in channels list`),
+ },
+ {
+ name: "Icon/Success/ValidSVG",
+ v: &Icon{
+ Data: mustBase64Decode(svgData),
+ MediaType: "image/svg+xml",
+ },
+ assertion: require.NoError,
+ },
+ {
+ name: "Icon/Success/ValidPNG",
+ v: &Icon{
+ Data: mustBase64Decode(pngData),
+ MediaType: "image/png",
+ },
+ assertion: require.NoError,
+ },
+ {
+ name: "Icon/Success/ValidJPEG",
+ v: &Icon{
+ Data: mustBase64Decode(jpegData),
+ MediaType: "image/jpeg",
+ },
+ assertion: require.NoError,
+ },
+ {
+ name: "Icon/Success/Nil",
+ v: nilIcon,
+ assertion: require.NoError,
+ },
+ //{
+ // name: "Icon/Error/NoData",
+ // v: &Icon{
+ // Data: nil,
+ // MediaType: "image/svg+xml",
+ // },
+ // assertion: hasError(`icon data must be set if icon is defined`),
+ //},
+ //{
+ // name: "Icon/Error/NoMediaType",
+ // v: &Icon{
+ // Data: mustBase64Decode(svgData),
+ // MediaType: "",
+ // },
+ // assertion: hasError(`icon mediatype must be set if icon is defined`),
+ //},
+ //{
+ // name: "Icon/Error/DataIsNotImage",
+ // v: &Icon{
+ // Data: []byte("{}"),
+ // MediaType: "application/json",
+ // },
+ // assertion: hasError(`icon data is not an image`),
+ //},
+ //{
+ // name: "Icon/Error/DataDoesNotMatchMediaType",
+ // v: &Icon{
+ // Data: mustBase64Decode(svgData),
+ // MediaType: "image/jpeg",
+ // },
+ // assertion: hasError(`icon media type "image/jpeg" does not match detected media type "image/svg+xml"`),
+ //},
+ {
+ name: "Channel/Success/Valid",
+ v: ch,
+ assertion: require.NoError,
+ },
+ {
+ name: "Channel/Error/NoName",
+ v: &Channel{},
+ assertion: hasError(`channel name must not be empty`),
+ },
+ {
+ name: "Channel/Error/NoPackage",
+ v: &Channel{
+ Name: "light",
+ },
+ assertion: hasError(`package must be set`),
+ },
+ {
+ name: "Channel/Error/NoBundles",
+ v: &Channel{
+ Package: pkg,
+ Name: "light",
+ },
+ assertion: hasError(`channel must contain at least one bundle`),
+ },
+ {
+ name: "Channel/Error/InvalidHead",
+ v: &Channel{
+ Package: pkg,
+ Name: "light",
+ Bundles: map[string]*Bundle{
+ "anakin.v0.0.0": {Name: "anakin.v0.0.0"},
+ "anakin.v0.0.1": {Name: "anakin.v0.0.1"},
+ },
+ },
+ assertion: hasError(`multiple channel heads found in graph: anakin.v0.0.0, anakin.v0.0.1`),
+ },
+ {
+ name: "Channel/Error/BundleKeyNameMismatch",
+ v: &Channel{
+ Package: pkg,
+ Name: "light",
+ Bundles: map[string]*Bundle{
+ "foo": {Name: "bar"},
+ },
+ },
+ assertion: hasError(`bundle key "foo" does not match bundle name "bar"`),
+ },
+ {
+ name: "Channel/Error/InvalidBundle",
+ v: &Channel{
+ Package: pkg,
+ Name: "light",
+ Bundles: map[string]*Bundle{
+ "anakin.v0.0.0": {Name: "anakin.v0.0.0"},
+ },
+ },
+ assertion: hasError(`invalid bundle "anakin.v0.0.0"`),
+ },
+ {
+ name: "Channel/Error/InvalidBundleChannelLink",
+ v: &Channel{
+ Package: pkg,
+ Name: "light",
+ Bundles: map[string]*Bundle{
+ "anakin.v0.0.0": {
+ Package: pkg,
+ Channel: ch,
+ Name: "anakin.v0.0.0",
+ Image: "anakin-operator:v0.0.0",
+ },
+ },
+ },
+ assertion: hasError(`bundle "anakin.v0.0.0" not correctly linked to parent channel`),
+ },
+ {
+ name: "Bundle/Success/Valid",
+ v: &Bundle{
+ Package: pkg,
+ Channel: ch,
+ Name: "anakin.v0.1.0",
+ Image: "registry.io/image",
+ Replaces: "anakin.v0.0.1",
+ Skips: []string{"anakin.v0.0.2"},
+ Properties: []property.Property{
+ property.MustBuildPackage("anakin", "0.1.0"),
+ property.MustBuildGVK("skywalker.me", "v1alpha1", "PodRacer"),
+ },
+ },
+ assertion: require.NoError,
+ },
+ {
+ name: "Bundle/Success/ReplacesNotInChannel",
+ v: &Bundle{
+ Package: pkg,
+ Channel: ch,
+ Name: "anakin.v0.1.0",
+ Image: "registry.io/image",
+ Replaces: "anakin.v0.0.0",
+ Properties: []property.Property{
+ property.MustBuildPackage("anakin", "0.1.0"),
+ },
+ },
+ assertion: require.NoError,
+ },
+ {
+ name: "Bundle/Success/NoBundleImage/HaveBundleData",
+ v: &Bundle{
+ Package: pkg,
+ Channel: ch,
+ Name: "anakin.v0.1.0",
+ Image: "",
+ Properties: []property.Property{
+ property.MustBuildPackage("anakin", "0.1.0"),
+ property.MustBuildGVK("skywalker.me", "v1alpha1", "PodRacer"),
+ property.MustBuildBundleObjectRef("path/to/data"),
+ },
+ Objects: []string{"testdata"},
+ CsvJSON: "CSVjson",
+ },
+ assertion: require.NoError,
+ },
+ {
+ name: "Bundle/Error/NoBundleImage",
+ v: &Bundle{
+ Package: pkg,
+ Channel: ch,
+ Name: "anakin.v0.1.0",
+ Image: "",
+ Properties: []property.Property{
+ property.MustBuildPackage("anakin", "0.1.0"),
+ property.MustBuildGVK("skywalker.me", "v1alpha1", "PodRacer"),
+ },
+ },
+ assertion: hasError(`bundle image must be set`),
+ },
+ {
+ name: "Bundle/Error/NoName",
+ v: &Bundle{},
+ assertion: hasError(`name must be set`),
+ },
+ {
+ name: "Bundle/Error/NoChannel",
+ v: &Bundle{
+ Name: "anakin.v0.1.0",
+ },
+ assertion: hasError(`channel must be set`),
+ },
+ {
+ name: "Bundle/Error/NoPackage",
+ v: &Bundle{
+ Channel: ch,
+ Name: "anakin.v0.1.0",
+ },
+ assertion: hasError(`package must be set`),
+ },
+ {
+ name: "Bundle/Error/WrongPackage",
+ v: &Bundle{
+ Package: &Package{},
+ Channel: ch,
+ Name: "anakin.v0.1.0",
+ },
+ assertion: hasError(`package does not match channel's package`),
+ },
+ {
+ name: "Bundle/Error/InvalidProperty",
+ v: &Bundle{
+ Package: pkg,
+ Channel: ch,
+ Name: "anakin.v0.1.0",
+ Replaces: "anakin.v0.0.1",
+ Properties: []property.Property{{Type: "broken", Value: json.RawMessage("")}},
+ },
+ assertion: hasError(`parse property[0] of type "broken": unexpected end of JSON input`),
+ },
+ {
+ name: "Bundle/Error/EmptySkipsValue",
+ v: &Bundle{
+ Package: pkg,
+ Channel: ch,
+ Name: "anakin.v0.1.0",
+ Replaces: "anakin.v0.0.1",
+ Properties: []property.Property{{Type: "custom", Value: json.RawMessage("{}")}},
+ Skips: []string{""},
+ },
+ assertion: hasError(`skip[0] is empty`),
+ },
+ {
+ name: "Bundle/Error/MissingPackage",
+ v: &Bundle{
+ Package: pkg,
+ Channel: ch,
+ Name: "anakin.v0.1.0",
+ Image: "",
+ Replaces: "anakin.v0.0.1",
+ Skips: []string{"anakin.v0.0.2"},
+ Properties: []property.Property{},
+ },
+ assertion: hasError(`must be exactly one property with type "olm.package"`),
+ },
+ {
+ name: "Bundle/Error/MultiplePackages",
+ v: &Bundle{
+ Package: pkg,
+ Channel: ch,
+ Name: "anakin.v0.1.0",
+ Image: "",
+ Replaces: "anakin.v0.0.1",
+ Skips: []string{"anakin.v0.0.2"},
+ Properties: []property.Property{
+ property.MustBuildPackage("anakin", "0.1.0"),
+ property.MustBuildPackage("anakin", "0.2.0"),
+ },
+ },
+ assertion: hasError(`must be exactly one property with type "olm.package"`),
+ },
+ {
+ name: "RelatedImage/Success/Valid",
+ v: RelatedImage{
+ Name: "foo",
+ Image: "bar",
+ },
+ assertion: require.NoError,
+ },
+ {
+ name: "RelatedImage/Error/NoImage",
+ v: RelatedImage{
+ Name: "foo",
+ Image: "",
+ },
+ assertion: hasError(`image must be set`),
+ },
+ }
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ s.assertion(t, s.v.Validate())
+ })
+ }
+}
+
+func makePackageChannelBundle() (*Package, *Channel) {
+ bundle1 := &Bundle{
+ Name: "anakin.v0.0.1",
+ Image: "anakin-operator:v0.0.1",
+ Properties: []property.Property{
+ property.MustBuildPackage("anakin", "0.0.1"),
+ property.MustBuildGVK("skywalker.me", "v1alpha1", "PodRacer"),
+ },
+ }
+ bundle2 := &Bundle{
+ Name: "anakin.v0.0.2",
+ Image: "anakin-operator:v0.0.2",
+ Replaces: "anakin.v0.0.1",
+ Properties: []property.Property{
+ property.MustBuildPackage("anakin", "0.0.2"),
+ property.MustBuildGVK("skywalker.me", "v1alpha1", "PodRacer"),
+ },
+ }
+ ch := &Channel{
+ Name: "light",
+ Bundles: map[string]*Bundle{
+ "anakin.v0.0.1": bundle1,
+ "anakin.v0.0.2": bundle2,
+ },
+ }
+ pkg := &Package{
+ Name: "anakin",
+ DefaultChannel: ch,
+ Channels: map[string]*Channel{
+ ch.Name: ch,
+ },
+ }
+
+ bundle1.Channel, bundle2.Channel = ch, ch
+ bundle1.Package, bundle2.Package, ch.Package = pkg, pkg, pkg
+
+ return pkg, ch
+}
+
+func TestAddBundle(t *testing.T) {
+ type spec struct {
+ name string
+ model Model
+ bundle Bundle
+ numPkgIncrease bool
+ numBundlesIncrease bool
+ pkgBundleAddedTo string
+ }
+ pkg, _ := makePackageChannelBundle()
+ m := Model{}
+ m[pkg.Name] = pkg
+
+ bundle1 := Bundle{
+ Name: "darth.vader.v0.0.1",
+ Replaces: "anakin.v0.0.1",
+ Skips: []string{"anakin.v0.0.2"},
+ Package: &Package{Name: pkg.Name},
+ }
+ ch1 := &Channel{
+ Name: "darkness",
+ Bundles: map[string]*Bundle{
+ "vader.v0.0.1": &bundle1,
+ },
+ }
+ bundle1.Channel = ch1
+
+ bundle2 := Bundle{
+ Name: "kylo.ren.v0.0.1",
+ Replaces: "darth.vader.v0.0.1",
+ Skips: []string{"anakin.v0.0.2"},
+ Package: &Package{
+ Name: "Empire",
+ Description: "The Empire Will Rise Again",
+ Icon: &Icon{
+ MediaType: "gif",
+ Data: []byte("palpatineLaughing"),
+ },
+ Channels: make(map[string]*Channel),
+ },
+ }
+ ch2 := &Channel{
+ Name: "darkeness",
+ Bundles: map[string]*Bundle{
+ "kylo.ren.v0.0.1": &bundle2,
+ },
+ }
+ bundle2.Channel = ch2
+ bundle2.Package.Channels[ch2.Name] = ch2
+
+ specs := []spec{
+ {
+ name: "AddingToExistingPackage",
+ bundle: bundle1,
+ model: m,
+ numPkgIncrease: false,
+ numBundlesIncrease: true,
+ pkgBundleAddedTo: bundle1.Package.Name,
+ },
+ {
+ name: "AddingNewPackage",
+ bundle: bundle2,
+ model: m,
+ numPkgIncrease: true,
+ numBundlesIncrease: false,
+ pkgBundleAddedTo: "",
+ },
+ }
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ existingPkgCount := len(s.model)
+ existingBundleCount := 0
+ if s.pkgBundleAddedTo != "" {
+ existingBundleCount = countBundles(m, s.pkgBundleAddedTo)
+ }
+ s.model.AddBundle(s.bundle)
+ if s.numPkgIncrease {
+ assert.Equal(t, len(s.model), existingPkgCount+1)
+ }
+ if s.numBundlesIncrease {
+ assert.Equal(t, countBundles(m, s.pkgBundleAddedTo), existingBundleCount+1)
+ }
+ })
+ }
+}
+
+func countBundles(m Model, pkg string) int {
+ count := 0
+ mpkg := m[pkg]
+ for _, ch := range mpkg.Channels {
+ count += len(ch.Bundles)
+ }
+ return count
+}
diff --git a/pkg/lib/property/errors.go b/pkg/lib/property/errors.go
new file mode 100644
index 000000000..6c3689c5b
--- /dev/null
+++ b/pkg/lib/property/errors.go
@@ -0,0 +1,25 @@
+package property
+
+import (
+ "fmt"
+)
+
+type ParseError struct {
+ Idx int
+ Typ string
+ Err error
+}
+
+func (e ParseError) Error() string {
+ return fmt.Sprintf("parse property[%d] of type %q: %v", e.Idx, e.Typ, e.Err)
+}
+
+type MatchMissingError struct {
+ foundType string
+ foundValue interface{}
+ expectedType string
+}
+
+func (e MatchMissingError) Error() string {
+ return fmt.Sprintf("property %q for %+v requires matching %q property", e.foundType, e.foundValue, e.expectedType)
+}
diff --git a/pkg/lib/property/property.go b/pkg/lib/property/property.go
new file mode 100644
index 000000000..9ccd0f14b
--- /dev/null
+++ b/pkg/lib/property/property.go
@@ -0,0 +1,296 @@
+package property
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/fs"
+ "io/ioutil"
+ "path/filepath"
+ "reflect"
+)
+
+type Property struct {
+ Type string `json:"type"`
+ Value json.RawMessage `json:"value"`
+}
+
+func (p Property) Validate() error {
+ if len(p.Type) == 0 {
+ return errors.New("type must be set")
+ }
+ if len(p.Value) == 0 {
+ return errors.New("value must be set")
+ }
+ var raw json.RawMessage
+ if err := json.Unmarshal(p.Value, &raw); err != nil {
+ return fmt.Errorf("value is not valid json: %v", err)
+ }
+ return nil
+}
+
+func (p Property) String() string {
+ return fmt.Sprintf("type: %q, value: %q", p.Type, p.Value)
+}
+
+type Package struct {
+ PackageName string `json:"packageName"`
+ Version string `json:"version"`
+}
+
+// NOTICE: The Channel properties are for internal use only.
+//
+// DO NOT use it for any public-facing functionalities.
+// This API is in alpha stage and it is subject to change.
+type Channel struct {
+ ChannelName string `json:"channelName"`
+ //Priority string `json:"priority"`
+ Priority int `json:"priority"`
+}
+
+type PackageRequired struct {
+ PackageName string `json:"packageName"`
+ VersionRange string `json:"versionRange"`
+}
+
+type GVK struct {
+ Group string `json:"group"`
+ Kind string `json:"kind"`
+ Version string `json:"version"`
+}
+
+type GVKRequired struct {
+ Group string `json:"group"`
+ Kind string `json:"kind"`
+ Version string `json:"version"`
+}
+
+type BundleObject struct {
+ File `json:",inline"`
+}
+
+type File struct {
+ ref string
+ data []byte
+}
+
+type fileJSON struct {
+ Ref string `json:"ref,omitempty"`
+ Data []byte `json:"data,omitempty"`
+}
+
+func (f *File) UnmarshalJSON(data []byte) error {
+ var t fileJSON
+ if err := json.Unmarshal(data, &t); err != nil {
+ return err
+ }
+ if len(t.Ref) > 0 && len(t.Data) > 0 {
+ return errors.New("fields 'ref' and 'data' are mutually exclusive")
+ }
+ f.ref = t.Ref
+ f.data = t.Data
+ return nil
+}
+
+func (f File) MarshalJSON() ([]byte, error) {
+ return json.Marshal(fileJSON{
+ Ref: f.ref,
+ Data: f.data,
+ })
+}
+
+func (f File) IsRef() bool {
+ return len(f.ref) > 0
+}
+
+func (f File) GetRef() string {
+ return f.ref
+}
+
+func (f File) GetData(root fs.FS, cwd string) ([]byte, error) {
+ if !f.IsRef() {
+ return f.data, nil
+ }
+ if filepath.IsAbs(f.ref) {
+ return nil, fmt.Errorf("reference must be a relative path")
+ }
+ file, err := root.Open(filepath.Join(cwd, f.ref))
+ if err != nil {
+ return nil, err
+ }
+ return ioutil.ReadAll(file)
+}
+
+type Properties struct {
+ Packages []Package `hash:"set"`
+ PackagesRequired []PackageRequired `hash:"set"`
+ GVKs []GVK `hash:"set"`
+ GVKsRequired []GVKRequired `hash:"set"`
+ BundleObjects []BundleObject `hash:"set"`
+ Channels []Channel `hash:"set"`
+
+ Others []Property `hash:"set"`
+}
+
+const (
+ TypePackage = "olm.package"
+ TypePackageRequired = "olm.package.required"
+ TypeGVK = "olm.gvk"
+ TypeGVKRequired = "olm.gvk.required"
+ TypeBundleObject = "olm.bundle.object"
+ TypeChannel = "olm.channel"
+)
+
+func Parse(in []Property) (*Properties, error) {
+ var out Properties
+ for i, prop := range in {
+ switch prop.Type {
+ case TypePackage:
+ var p Package
+ if err := json.Unmarshal(prop.Value, &p); err != nil {
+ return nil, ParseError{Idx: i, Typ: prop.Type, Err: err}
+ }
+ out.Packages = append(out.Packages, p)
+ case TypePackageRequired:
+ var p PackageRequired
+ if err := json.Unmarshal(prop.Value, &p); err != nil {
+ return nil, ParseError{Idx: i, Typ: prop.Type, Err: err}
+ }
+ out.PackagesRequired = append(out.PackagesRequired, p)
+ case TypeGVK:
+ var p GVK
+ if err := json.Unmarshal(prop.Value, &p); err != nil {
+ return nil, ParseError{Idx: i, Typ: prop.Type, Err: err}
+ }
+ out.GVKs = append(out.GVKs, p)
+ case TypeGVKRequired:
+ var p GVKRequired
+ if err := json.Unmarshal(prop.Value, &p); err != nil {
+ return nil, ParseError{Idx: i, Typ: prop.Type, Err: err}
+ }
+ out.GVKsRequired = append(out.GVKsRequired, p)
+ case TypeBundleObject:
+ var p BundleObject
+ if err := json.Unmarshal(prop.Value, &p); err != nil {
+ return nil, ParseError{Idx: i, Typ: prop.Type, Err: err}
+ }
+ out.BundleObjects = append(out.BundleObjects, p)
+ // NOTICE: The Channel properties are for internal use only.
+ // DO NOT use it for any public-facing functionalities.
+ // This API is in alpha stage and it is subject to change.
+ case TypeChannel:
+ var p Channel
+ if err := json.Unmarshal(prop.Value, &p); err != nil {
+ return nil, ParseError{Idx: i, Typ: prop.Type, Err: err}
+ }
+ out.Channels = append(out.Channels, p)
+ default:
+ var p json.RawMessage
+ if err := json.Unmarshal(prop.Value, &p); err != nil {
+ return nil, ParseError{Idx: i, Typ: prop.Type, Err: err}
+ }
+ out.Others = append(out.Others, prop)
+ }
+ }
+ return &out, nil
+}
+
+func Deduplicate(in []Property) []Property {
+ type key struct {
+ typ string
+ value string
+ }
+
+ props := map[key]Property{}
+ var out []Property
+ for _, p := range in {
+ k := key{p.Type, string(p.Value)}
+ if _, ok := props[k]; ok {
+ continue
+ }
+ props[k] = p
+ out = append(out, p)
+ }
+ return out
+}
+
+func Build(p interface{}) (*Property, error) {
+ var (
+ typ string
+ val interface{}
+ )
+ if prop, ok := p.(*Property); ok {
+ typ = prop.Type
+ val = prop.Value
+ } else {
+ t := reflect.TypeOf(p)
+ if t.Kind() != reflect.Ptr {
+ return nil, errors.New("input must be a pointer to a type")
+ }
+ typ, ok = scheme[t]
+ if !ok {
+ return nil, fmt.Errorf("%s not a known property type registered with the scheme", t)
+ }
+ val = p
+ }
+ d, err := jsonMarshal(val)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Property{
+ Type: typ,
+ Value: d,
+ }, nil
+}
+
+func MustBuild(p interface{}) Property {
+ prop, err := Build(p)
+ if err != nil {
+ panic(err)
+ }
+ return *prop
+}
+
+func jsonMarshal(p interface{}) ([]byte, error) {
+ buf := &bytes.Buffer{}
+ dec := json.NewEncoder(buf)
+ dec.SetEscapeHTML(false)
+ err := dec.Encode(p)
+ if err != nil {
+ return nil, err
+ }
+ out := &bytes.Buffer{}
+ if err := json.Compact(out, buf.Bytes()); err != nil {
+ return nil, err
+ }
+ return out.Bytes(), nil
+}
+
+func MustBuildPackage(name, version string) Property {
+ return MustBuild(&Package{PackageName: name, Version: version})
+}
+func MustBuildPackageRequired(name, versionRange string) Property {
+ return MustBuild(&PackageRequired{name, versionRange})
+}
+func MustBuildGVK(group, version, kind string) Property {
+ return MustBuild(&GVK{group, kind, version})
+}
+func MustBuildGVKRequired(group, version, kind string) Property {
+ return MustBuild(&GVKRequired{group, kind, version})
+}
+func MustBuildBundleObjectRef(ref string) Property {
+ return MustBuild(&BundleObject{File: File{ref: ref}})
+}
+func MustBuildBundleObjectData(data []byte) Property {
+ return MustBuild(&BundleObject{File: File{data: data}})
+}
+
+// NOTICE: The Channel properties are for internal use only.
+//
+// DO NOT use it for any public-facing functionalities.
+// This API is in alpha stage and it is subject to change.
+func MustBuildChannelPriority(name string, priority int) Property {
+ return MustBuild(&Channel{ChannelName: name, Priority: priority})
+}
diff --git a/pkg/lib/property/property_test.go b/pkg/lib/property/property_test.go
new file mode 100644
index 000000000..8a05c450b
--- /dev/null
+++ b/pkg/lib/property/property_test.go
@@ -0,0 +1,434 @@
+package property
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestValidate(t *testing.T) {
+ type spec struct {
+ name string
+ v Property
+ assertion require.ErrorAssertionFunc
+ }
+
+ specs := []spec{
+ {
+ name: "Success/Valid",
+ v: Property{
+ Type: "custom.type",
+ Value: json.RawMessage("{}"),
+ },
+ assertion: require.NoError,
+ },
+ {
+ name: "Error/NoType",
+ v: Property{
+ Value: json.RawMessage(""),
+ },
+ assertion: require.Error,
+ },
+ {
+ name: "Error/NoValue",
+ v: Property{
+ Type: "custom.type",
+ Value: nil,
+ },
+ assertion: require.Error,
+ },
+ {
+ name: "Error/EmptyValue",
+ v: Property{
+ Type: "custom.type",
+ Value: json.RawMessage{},
+ },
+ assertion: require.Error,
+ },
+ {
+ name: "Error/ValueNotJSON",
+ v: Property{
+ Type: "custom.type",
+ Value: json.RawMessage("{"),
+ },
+ assertion: require.Error,
+ },
+ }
+
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ err := s.v.Validate()
+ s.assertion(t, err)
+ })
+ }
+}
+
+func TestFile_MarshalJSON(t *testing.T) {
+ type spec struct {
+ name string
+ file File
+ json string
+ assertion require.ErrorAssertionFunc
+ }
+ specs := []spec{
+ {
+ name: "Success/Ref",
+ file: File{ref: "foo"},
+ json: `{"ref":"foo"}`,
+ assertion: require.NoError,
+ },
+ {
+ name: "Success/Data",
+ file: File{data: []byte("foo")},
+ json: fmt.Sprintf(`{"data":%q}`, base64.StdEncoding.EncodeToString([]byte("foo"))),
+ assertion: require.NoError,
+ },
+ }
+
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ d, err := json.Marshal(s.file)
+ s.assertion(t, err)
+ assert.Equal(t, s.json, string(d))
+ })
+ }
+}
+
+func TestFile_UnmarshalJSON(t *testing.T) {
+ type spec struct {
+ name string
+ file File
+ json string
+ assertion require.ErrorAssertionFunc
+ }
+ specs := []spec{
+ {
+ name: "Success/Ref",
+ file: File{ref: "foo"},
+ json: `{"ref":"foo"}`,
+ assertion: require.NoError,
+ },
+ {
+ name: "Success/Data",
+ file: File{data: []byte("foo")},
+ json: fmt.Sprintf(`{"data":%q}`, base64.StdEncoding.EncodeToString([]byte("foo"))),
+ assertion: require.NoError,
+ },
+ {
+ name: "Error/RefAndData",
+ json: fmt.Sprintf(`{"ref":"foo","data":%q}`, base64.StdEncoding.EncodeToString([]byte("bar"))),
+ assertion: require.Error,
+ },
+ {
+ name: "Error/InvalidJSON",
+ json: `["ref","data"]`,
+ assertion: require.Error,
+ },
+ }
+
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ var actual File
+ err := json.Unmarshal([]byte(s.json), &actual)
+ s.assertion(t, err)
+ assert.Equal(t, s.file, actual)
+ })
+ }
+}
+
+func TestFile_IsRef(t *testing.T) {
+ assert.True(t, File{ref: "foo"}.IsRef())
+ assert.False(t, File{data: []byte("bar")}.IsRef())
+}
+
+func TestFile_GetRef(t *testing.T) {
+ assert.Equal(t, "foo", File{ref: "foo"}.GetRef())
+ assert.Equal(t, "", File{data: []byte("bar")}.GetRef())
+}
+
+func TestFile_GetData(t *testing.T) {
+ type spec struct {
+ name string
+ createFile func(root string) error
+ file File
+ assertion assert.ErrorAssertionFunc
+ expectData []byte
+ }
+
+ createFile := func(root string) error {
+ dir := filepath.Join(root, "tmp")
+ if err := os.MkdirAll(dir, 0777); err != nil {
+ return err
+ }
+ return ioutil.WriteFile(filepath.Join(dir, "foo.txt"), []byte("bar"), 0666)
+ }
+
+ specs := []spec{
+ {
+ name: "Success/NilData",
+ file: File{},
+ assertion: assert.NoError,
+ expectData: nil,
+ },
+ {
+ name: "Success/WithData",
+ file: File{data: []byte("bar")},
+ assertion: assert.NoError,
+ expectData: []byte("bar"),
+ },
+ {
+ name: "Success/WithRef",
+ createFile: createFile,
+ file: File{ref: "tmp/foo.txt"},
+ assertion: assert.NoError,
+ expectData: []byte("bar"),
+ },
+ {
+ name: "Error/WithRef/FileDoesNotExist",
+ file: File{ref: "non-existent.txt"},
+ assertion: assert.Error,
+ },
+ {
+ name: "Error/WithRef/RefIsAbsolutePath",
+ file: File{ref: "/etc/hosts"},
+ assertion: assert.Error,
+ },
+ {
+ name: "Error/WithRef/RefIsOutsideRoot",
+ file: File{ref: "../etc/hosts"},
+ assertion: assert.Error,
+ },
+ }
+
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ dir := t.TempDir()
+
+ if s.createFile != nil {
+ require.NoError(t, s.createFile(dir))
+ }
+
+ data, err := s.file.GetData(os.DirFS(dir), ".")
+ s.assertion(t, err)
+ assert.Equal(t, s.expectData, data)
+ })
+ }
+}
+
+func TestParse(t *testing.T) {
+ type spec struct {
+ name string
+ input []Property
+ expectProps *Properties
+ assertion assert.ErrorAssertionFunc
+ }
+ specs := []spec{
+ {
+ name: "Error/InvalidPackage",
+ input: []Property{
+ {Type: TypePackage, Value: json.RawMessage(`{`)},
+ },
+ assertion: assert.Error,
+ },
+ {
+ name: "Error/InvalidPackageRequired",
+ input: []Property{
+ {Type: TypePackageRequired, Value: json.RawMessage(`{`)},
+ },
+ assertion: assert.Error,
+ },
+ {
+ name: "Error/InvalidGVK",
+ input: []Property{
+ {Type: TypeGVK, Value: json.RawMessage(`{`)},
+ },
+ assertion: assert.Error,
+ },
+ {
+ name: "Error/InvalidGVKRequired",
+ input: []Property{
+ {Type: TypeGVKRequired, Value: json.RawMessage(`{`)},
+ },
+ assertion: assert.Error,
+ },
+ {
+ name: "Error/InvalidBundleObject",
+ input: []Property{
+ {Type: TypeBundleObject, Value: json.RawMessage(`{`)},
+ },
+ assertion: assert.Error,
+ },
+ {
+ name: "Error/InvalidOther",
+ input: []Property{
+ {Type: "otherType1", Value: json.RawMessage(`{`)},
+ },
+ assertion: assert.Error,
+ },
+ {
+ name: "Success/Valid",
+ input: []Property{
+ MustBuildPackage("package1", "0.1.0"),
+ MustBuildPackage("package2", "0.2.0"),
+ MustBuildPackageRequired("package3", ">=1.0.0 <2.0.0-0"),
+ MustBuildPackageRequired("package4", ">=2.0.0 <3.0.0-0"),
+ MustBuildGVK("group", "v1", "Kind1"),
+ MustBuildGVK("group", "v1", "Kind2"),
+ MustBuildGVKRequired("other", "v2", "Kind3"),
+ MustBuildGVKRequired("other", "v2", "Kind4"),
+ MustBuildBundleObjectRef("testref1"),
+ MustBuildBundleObjectData([]byte("testdata2")),
+ {Type: "otherType1", Value: json.RawMessage(`{"v":"otherValue1"}`)},
+ {Type: "otherType2", Value: json.RawMessage(`["otherValue2"]`)},
+ },
+ expectProps: &Properties{
+ Packages: []Package{
+ {"package1", "0.1.0"},
+ {"package2", "0.2.0"},
+ },
+ PackagesRequired: []PackageRequired{
+ {"package3", ">=1.0.0 <2.0.0-0"},
+ {"package4", ">=2.0.0 <3.0.0-0"},
+ },
+ GVKs: []GVK{
+ {"group", "Kind1", "v1"},
+ {"group", "Kind2", "v1"},
+ },
+ GVKsRequired: []GVKRequired{
+ {"other", "Kind3", "v2"},
+ {"other", "Kind4", "v2"},
+ },
+ BundleObjects: []BundleObject{
+ {File: File{ref: "testref1"}},
+ {File: File{data: []byte("testdata2")}},
+ },
+ Others: []Property{
+ {Type: "otherType1", Value: json.RawMessage(`{"v":"otherValue1"}`)},
+ {Type: "otherType2", Value: json.RawMessage(`["otherValue2"]`)},
+ },
+ },
+ assertion: assert.NoError,
+ },
+ }
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ actual, err := Parse(s.input)
+ s.assertion(t, err)
+ assert.Equal(t, s.expectProps, actual)
+ })
+ }
+}
+
+func TestDeduplicate(t *testing.T) {
+ type spec struct {
+ name string
+ input []Property
+ expectProps []Property
+ }
+ specs := []spec{
+ {
+ name: "Identical",
+ input: []Property{
+ MustBuildPackage("package1", "0.1.0"),
+ MustBuildGVK("group", "v1", "Kind"),
+ MustBuildGVK("group", "v1", "Kind"),
+ MustBuildGVK("group", "v1", "Kind"),
+ },
+ expectProps: []Property{
+ MustBuildPackage("package1", "0.1.0"),
+ MustBuildGVK("group", "v1", "Kind"),
+ },
+ },
+ }
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ actual := Deduplicate(s.input)
+ assert.Equal(t, s.expectProps, actual)
+ })
+ }
+}
+
+func TestBuild(t *testing.T) {
+ type spec struct {
+ name string
+ input interface{}
+ assertion require.ErrorAssertionFunc
+ expectedProperty *Property
+ }
+ specs := []spec{
+ {
+ name: "Success/Package",
+ input: &Package{"name", "0.1.0"},
+ assertion: require.NoError,
+ expectedProperty: propPtr(MustBuildPackage("name", "0.1.0")),
+ },
+ {
+ name: "Success/PackageRequired",
+ input: &PackageRequired{"name", ">=0.1.0"},
+ assertion: require.NoError,
+ expectedProperty: propPtr(MustBuildPackageRequired("name", ">=0.1.0")),
+ },
+ {
+ name: "Success/GVK",
+ input: &GVK{"group", "Kind", "v1"},
+ assertion: require.NoError,
+ expectedProperty: propPtr(MustBuildGVK("group", "v1", "Kind")),
+ },
+ {
+ name: "Success/GVKRequired",
+ input: &GVKRequired{"group", "Kind", "v1"},
+ assertion: require.NoError,
+ expectedProperty: propPtr(MustBuildGVKRequired("group", "v1", "Kind")),
+ },
+ {
+ name: "Success/BundleObject",
+ input: &BundleObject{File: File{ref: "test"}},
+ assertion: require.NoError,
+ expectedProperty: propPtr(MustBuildBundleObjectRef("test")),
+ },
+ {
+ name: "Success/Property",
+ input: &Property{Type: "foo", Value: json.RawMessage(`"bar"`)},
+ assertion: require.NoError,
+ expectedProperty: &Property{Type: "foo", Value: json.RawMessage(`"bar"`)},
+ },
+ {
+ name: "Error/InvalidProperty",
+ input: &Property{Type: "foo", Value: json.RawMessage(`{`)},
+ assertion: require.Error,
+ },
+ {
+ name: "Error/NotAPointer",
+ input: Package{},
+ assertion: require.Error,
+ },
+ {
+ name: "Error/NotRegisteredInScheme",
+ input: &struct{}{},
+ assertion: require.Error,
+ },
+ }
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ actual, err := Build(s.input)
+ s.assertion(t, err)
+ assert.Equal(t, s.expectedProperty, actual)
+ })
+ }
+}
+
+func TestMustBuild(t *testing.T) {
+ assert.NotPanics(t, func() { MustBuild(&Package{}) })
+ assert.Panics(t, func() { MustBuild(Package{}) })
+}
+
+func propPtr(in Property) *Property {
+ return &in
+}
diff --git a/pkg/lib/property/scheme.go b/pkg/lib/property/scheme.go
new file mode 100644
index 000000000..28d8a5d14
--- /dev/null
+++ b/pkg/lib/property/scheme.go
@@ -0,0 +1,33 @@
+package property
+
+import (
+ "fmt"
+ "reflect"
+)
+
+func init() {
+ scheme = map[reflect.Type]string{
+ reflect.TypeOf(&Package{}): TypePackage,
+ reflect.TypeOf(&PackageRequired{}): TypePackageRequired,
+ reflect.TypeOf(&GVK{}): TypeGVK,
+ reflect.TypeOf(&GVKRequired{}): TypeGVKRequired,
+ reflect.TypeOf(&BundleObject{}): TypeBundleObject,
+ // NOTICE: The Channel properties are for internal use only.
+ // DO NOT use it for any public-facing functionalities.
+ // This API is in alpha stage and it is subject to change.
+ reflect.TypeOf(&Channel{}): TypeChannel,
+ }
+}
+
+var scheme map[reflect.Type]string
+
+func AddToScheme(typ string, p interface{}) {
+ t := reflect.TypeOf(p)
+ if t.Kind() != reflect.Ptr {
+ panic("input must be a pointer to a type")
+ }
+ if _, ok := scheme[t]; ok {
+ panic(fmt.Sprintf("scheme already contains registration for type %q", t))
+ }
+ scheme[t] = typ
+}
diff --git a/pkg/lib/property/scheme_test.go b/pkg/lib/property/scheme_test.go
new file mode 100644
index 000000000..81810c3d9
--- /dev/null
+++ b/pkg/lib/property/scheme_test.go
@@ -0,0 +1,46 @@
+package property
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAddToScheme(t *testing.T) {
+ type custom struct {
+ Name string `json:"name"`
+ }
+
+ type spec struct {
+ name string
+ typ string
+ val interface{}
+ assertion func(assert.TestingT, assert.PanicTestFunc, ...interface{}) bool
+ }
+ specs := []spec{
+ {
+ name: "Success/CustomTypeValue",
+ typ: "custom1",
+ val: &custom{},
+ assertion: assert.NotPanics,
+ },
+ {
+ name: "Panic/MustBeAPointer",
+ typ: TypePackage,
+ val: custom{},
+ assertion: assert.Panics,
+ },
+ {
+ name: "Panic/AlreadyRegistered",
+ typ: TypePackage,
+ val: &custom{},
+ assertion: assert.Panics,
+ },
+ }
+ for _, s := range specs {
+ t.Run(s.name, func(t *testing.T) {
+ f := func() { AddToScheme(s.typ, s.val) }
+ s.assertion(t, f)
+ })
+ }
+}