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) + }) + } +}