diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 47c78f398..34b42d354 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -52,7 +52,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 + uses: github/codeql-action/init@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 with: languages: ${{ matrix.language }} tools: latest @@ -64,7 +64,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 + uses: github/codeql-action/autobuild@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -78,4 +78,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 + uses: github/codeql-action/analyze@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index dc09154fc..f79356472 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 + uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 with: sarif_file: results.sarif diff --git a/CATALOG.md b/CATALOG.md index c28fda8d8..a728484da 100644 --- a/CATALOG.md +++ b/CATALOG.md @@ -7,7 +7,7 @@ Depending on the workload type, not all tests are required to pass to satisfy be ## Test cases summary -### Total test cases: 116 +### Total test cases: 117 ### Total suites: 10 @@ -19,7 +19,7 @@ Depending on the workload type, not all tests are required to pass to satisfy be |manageability|2| |networking|12| |observability|5| -|operator|10| +|operator|11| |performance|6| |platform-alteration|13| |preflight|17| @@ -36,11 +36,11 @@ Depending on the workload type, not all tests are required to pass to satisfy be |---|---| |8|1| -### Non-Telco specific tests only: 68 +### Non-Telco specific tests only: 69 |Mandatory|Optional| |---|---| -|43|25| +|44|25| ### Telco specific tests only: 27 @@ -1346,6 +1346,22 @@ Tags|common,operator |Non-Telco|Mandatory| |Telco|Mandatory| +#### operator-valid-installation-tenant-namespace + +Property|Description +---|--- +Unique ID|operator-valid-installation-tenant-namespace +Description|Tests whether operator installation is valid in tenant namespace. +Suggested Remediation|Ensure that operator with install mode SingleNamespace only is installed in the tenant namespace. +Best Practice Reference|https://redhat-best-practices-for-k8s.github.io/guide/#redhat-best-practices-for-k8s-cnf-operator-requirements +Exception Process|No exceptions +Tags|common,operator +|**Scenario**|**Optional/Mandatory**| +|Extended|Mandatory| +|Far-Edge|Mandatory| +|Non-Telco|Mandatory| +|Telco|Mandatory| + ### performance #### performance-exclusive-cpu-pool diff --git a/config/certsuite_config.yml b/config/certsuite_config.yml index 420ed4120..f70db46df 100644 --- a/config/certsuite_config.yml +++ b/config/certsuite_config.yml @@ -1,11 +1,13 @@ targetNameSpaces: - name: tnf + - name: certsuite-operator podsUnderTestLabels: - "redhat-best-practices-for-k8s.com/generic: target" operatorsUnderTestLabels: - "redhat-best-practices-for-k8s.com/operator:target" - "redhat-best-practices-for-k8s.com/operator1:new" - "cnf/test:cr-scale-operator" + - "operators.coreos.com/rh-best-practices-for-k8s-certsuite-operator.certsuite-operator:" targetCrdFilters: - nameSuffix: "group1.test.com" scalable: false diff --git a/expected_results.yaml b/expected_results.yaml index 9fa8b0f23..31f275dfd 100644 --- a/expected_results.yaml +++ b/expected_results.yaml @@ -59,6 +59,7 @@ testCases: - operator-single-crd-owner - operator-pods-no-hugepages - operator-multiple-same-operators + - operator-valid-installation-tenant-namespace - performance-exclusive-cpu-pool - performance-max-resources-exec-probes - performance-shared-cpu-pool-non-rt-scheduling-policy # hazelcast pod meets requirements diff --git a/go.mod b/go.mod index afab7c3d5..209ecba53 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,12 @@ require ( github.com/redhat-best-practices-for-k8s/certsuite-claim v1.0.50 github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cobra v1.8.1 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/xeipuuv/gojsonschema v1.2.0 // indirect gopkg.in/yaml.v3 v3.0.1 ) -require k8s.io/client-go v0.31.2 +require k8s.io/client-go v0.31.3 require ( github.com/mittwald/go-helm-client v0.12.14 @@ -22,8 +22,8 @@ require ( github.com/operator-framework/operator-lifecycle-manager v0.30.0 github.com/pkg/errors v0.9.1 // indirect helm.sh/helm/v3 v3.16.3 - k8s.io/api v0.31.2 - k8s.io/apimachinery v0.31.2 + k8s.io/api v0.31.3 + k8s.io/apimachinery v0.31.3 k8s.io/klog/v2 v2.130.1 // indirect ) @@ -194,9 +194,9 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiserver v0.31.2 // indirect - k8s.io/cli-runtime v0.31.2 // indirect - k8s.io/component-base v0.31.2 // indirect + k8s.io/apiserver v0.31.3 // indirect + k8s.io/cli-runtime v0.31.3 // indirect + k8s.io/component-base v0.31.3 // indirect k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 // indirect k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 // indirect modernc.org/libc v1.37.6 // indirect @@ -215,7 +215,7 @@ require ( require ( github.com/hashicorp/go-version v1.7.0 - k8s.io/apiextensions-apiserver v0.31.2 + k8s.io/apiextensions-apiserver v0.31.3 ) require ( @@ -226,13 +226,13 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.7.5 github.com/manifoldco/promptui v0.9.0 - github.com/redhat-best-practices-for-k8s/oct v0.0.27 - github.com/redhat-best-practices-for-k8s/privileged-daemonset v1.0.39 + github.com/redhat-best-practices-for-k8s/oct v0.0.28 + github.com/redhat-best-practices-for-k8s/privileged-daemonset v1.0.40 github.com/redhat-openshift-ecosystem/openshift-preflight v0.0.0-20241021175030-e64988a27024 github.com/robert-nix/ansihtml v1.0.1 golang.org/x/term v0.26.0 - google.golang.org/api v0.207.0 - k8s.io/kubectl v0.31.2 + google.golang.org/api v0.209.0 + k8s.io/kubectl v0.31.3 ) replace github.com/redhat-openshift-ecosystem/openshift-preflight => github.com/redhat-openshift-ecosystem/openshift-preflight v0.0.0-20241021175030-e64988a27024 diff --git a/go.sum b/go.sum index c948e8f7d..e7d2e0653 100644 --- a/go.sum +++ b/go.sum @@ -444,10 +444,10 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/redhat-best-practices-for-k8s/certsuite-claim v1.0.50 h1:I5h0QManS/gIqu+ontECO7wl7YRNynITdu2ox55aHmQ= github.com/redhat-best-practices-for-k8s/certsuite-claim v1.0.50/go.mod h1:MEPl9mYANLNgdd7KKpW6rRXmUK1pXtbWmG2eVaKmBKM= -github.com/redhat-best-practices-for-k8s/oct v0.0.27 h1:7iJ1475uhyxjhStR4riCQisG/LzUhKktMKdwaaGzBQo= -github.com/redhat-best-practices-for-k8s/oct v0.0.27/go.mod h1:ilJ7CTkImZOhT0WW5jbqxYighVRBPpLxcxcuc5Fd/ZU= -github.com/redhat-best-practices-for-k8s/privileged-daemonset v1.0.39 h1:fdifQZFjx5NIiD/8pt94ELeTPUXy7s/KYwJC8wzgqns= -github.com/redhat-best-practices-for-k8s/privileged-daemonset v1.0.39/go.mod h1:Xvn/SQLaro2vpiVut0e1epYr/SrUsw5VBueIZcb2dcM= +github.com/redhat-best-practices-for-k8s/oct v0.0.28 h1:jsVQ8zNY4HP0a+N5LpW445Y3phklZ4PGtlU1pZQ0Wic= +github.com/redhat-best-practices-for-k8s/oct v0.0.28/go.mod h1:TnyopIPwdsaI7gikOi7WmT9/lgzd1Lp6DZaN6nMJhBA= +github.com/redhat-best-practices-for-k8s/privileged-daemonset v1.0.40 h1:imtI6P0XZSeaGbMd81Dg6QpsJrtUPobAlHhvwEyGouU= +github.com/redhat-best-practices-for-k8s/privileged-daemonset v1.0.40/go.mod h1:ZVOdgHN8bR8bRFQytKgyDa+26AUa96wcnjsnJ6RYnzQ= github.com/redhat-openshift-ecosystem/openshift-preflight v0.0.0-20241021175030-e64988a27024 h1:qPsNS6SIDigSwcUMUrEdDovIAdbVCNOGvhir8p6wVNc= github.com/redhat-openshift-ecosystem/openshift-preflight v0.0.0-20241021175030-e64988a27024/go.mod h1:WgrOUZnQYsTQetJwbMHkcNecYaOcWMmTVDmDOTTIMcQ= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= @@ -511,8 +511,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= @@ -665,8 +666,8 @@ golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSm golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/api v0.207.0 h1:Fvt6IGCYjf7YLcQ+GCegeAI2QSQCfIWhRkmrMPj3JRM= -google.golang.org/api v0.207.0/go.mod h1:I53S168Yr/PNDNMi5yPnDc0/LGRZO6o7PoEbl/HY3CM= +google.golang.org/api v0.209.0 h1:Ja2OXNlyRlWCWu8o+GgI4yUn/wz9h/5ZfFbKz+dQX+w= +google.golang.org/api v0.209.0/go.mod h1:I53S168Yr/PNDNMi5yPnDc0/LGRZO6o7PoEbl/HY3CM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -721,26 +722,26 @@ helm.sh/helm/v3 v3.16.3 h1:kb8bSxMeRJ+knsK/ovvlaVPfdis0X3/ZhYCSFRP+YmY= helm.sh/helm/v3 v3.16.3/go.mod h1:zeVWGDR4JJgiRbT3AnNsjYaX8OTJlIE9zC+Q7F7iUSU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= -k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= -k8s.io/apiextensions-apiserver v0.31.2 h1:W8EwUb8+WXBLu56ser5IudT2cOho0gAKeTOnywBLxd0= -k8s.io/apiextensions-apiserver v0.31.2/go.mod h1:i+Geh+nGCJEGiCGR3MlBDkS7koHIIKWVfWeRFiOsUcM= -k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= -k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/apiserver v0.31.2 h1:VUzOEUGRCDi6kX1OyQ801m4A7AUPglpsmGvdsekmcI4= -k8s.io/apiserver v0.31.2/go.mod h1:o3nKZR7lPlJqkU5I3Ove+Zx3JuoFjQobGX1Gctw6XuE= -k8s.io/cli-runtime v0.31.2 h1:7FQt4C4Xnqx8V1GJqymInK0FFsoC+fAZtbLqgXYVOLQ= -k8s.io/cli-runtime v0.31.2/go.mod h1:XROyicf+G7rQ6FQJMbeDV9jqxzkWXTYD6Uxd15noe0Q= -k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= -k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= -k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA= -k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ= +k8s.io/api v0.31.3 h1:umzm5o8lFbdN/hIXbrK9oRpOproJO62CV1zqxXrLgk8= +k8s.io/api v0.31.3/go.mod h1:UJrkIp9pnMOI9K2nlL6vwpxRzzEX5sWgn8kGQe92kCE= +k8s.io/apiextensions-apiserver v0.31.3 h1:+GFGj2qFiU7rGCsA5o+p/rul1OQIq6oYpQw4+u+nciE= +k8s.io/apiextensions-apiserver v0.31.3/go.mod h1:2DSpFhUZZJmn/cr/RweH1cEVVbzFw9YBu4T+U3mf1e4= +k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4= +k8s.io/apimachinery v0.31.3/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apiserver v0.31.3 h1:+1oHTtCB+OheqFEz375D0IlzHZ5VeQKX1KGXnx+TTuY= +k8s.io/apiserver v0.31.3/go.mod h1:PrxVbebxrxQPFhJk4powDISIROkNMKHibTg9lTRQ0Qg= +k8s.io/cli-runtime v0.31.3 h1:fEQD9Xokir78y7pVK/fCJN090/iYNrLHpFbGU4ul9TI= +k8s.io/cli-runtime v0.31.3/go.mod h1:Q2jkyTpl+f6AtodQvgDI8io3jrfr+Z0LyQBPJJ2Btq8= +k8s.io/client-go v0.31.3 h1:CAlZuM+PH2cm+86LOBemaJI/lQ5linJ6UFxKX/SoG+4= +k8s.io/client-go v0.31.3/go.mod h1:2CgjPUTpv3fE5dNygAr2NcM8nhHzXvxB8KL5gYc3kJs= +k8s.io/component-base v0.31.3 h1:DMCXXVx546Rfvhj+3cOm2EUxhS+EyztH423j+8sOwhQ= +k8s.io/component-base v0.31.3/go.mod h1:xME6BHfUOafRgT0rGVBGl7TuSg8Z9/deT7qq6w7qjIU= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 h1:MErs8YA0abvOqJ8gIupA1Tz6PKXYUw34XsGlA7uSL1k= k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094/go.mod h1:7ioBJr1A6igWjsR2fxq2EZ0mlMwYLejazSIc2bzMp2U= -k8s.io/kubectl v0.31.2 h1:gTxbvRkMBwvTSAlobiTVqsH6S8Aa1aGyBcu5xYLsn8M= -k8s.io/kubectl v0.31.2/go.mod h1:EyASYVU6PY+032RrTh5ahtSOMgoDRIux9V1JLKtG5xM= +k8s.io/kubectl v0.31.3 h1:3r111pCjPsvnR98oLLxDMwAeM6OPGmPty6gSKaLTQes= +k8s.io/kubectl v0.31.3/go.mod h1:lhMECDCbJN8He12qcKqs2QfmVo9Pue30geovBVpH5fs= k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI= k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= diff --git a/pkg/autodiscover/autodiscover.go b/pkg/autodiscover/autodiscover.go index 6d2a4a2ae..95d887fdc 100644 --- a/pkg/autodiscover/autodiscover.go +++ b/pkg/autodiscover/autodiscover.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "path" "regexp" "strings" "time" @@ -62,6 +63,7 @@ type DiscoveredTestData struct { AllPods []corev1.Pod ProbePods []corev1.Pod CSVToPodListMap map[types.NamespacedName][]*corev1.Pod + OperandPods []*corev1.Pod ResourceQuotaItems []corev1.ResourceQuota PodDisruptionBudgets []policyv1.PodDisruptionBudget NetworkPolicies []networkingv1.NetworkPolicy @@ -133,7 +135,7 @@ func createLabels(labelStrings []string) (labelObjects []labelObject) { // DoAutoDiscover finds objects under test // -//nolint:funlen +//nolint:funlen,gocyclo func DoAutoDiscover(config *configuration.TestConfiguration) DiscoveredTestData { oc := clientsholder.GetClientsHolder() @@ -198,6 +200,17 @@ func DoAutoDiscover(config *configuration.TestConfiguration) DiscoveredTestData log.Fatal("Failed to get the operator pods, err: %v", err) } + // Best effort mode autodiscovery for operand (running-only) pods. + pods, _ := findPodsByLabels(oc.K8sClient.CoreV1(), nil, data.Namespaces) + if err != nil { + log.Fatal("Failed to get running pods, err: %v", err) + } + + data.OperandPods, err = getOperandPodsFromTestCsvs(data.Csvs, pods) + if err != nil { + log.Fatal("Failed to get operand pods, err: %v", err) + } + openshiftVersion, err := getOpenshiftVersion(oc.OcpClient) if err != nil { log.Fatal("Failed to get the OpenShift version, err: %v", err) @@ -355,3 +368,62 @@ func getPodsOwnedByCsv(csvName, operatorNamespace string, client *clientsholder. } return managedPods, nil } + +// getOperandPodsFromTestCsvs returns a subset of pods whose owner CRs are managed by any of the testCsvs. +func getOperandPodsFromTestCsvs(testCsvs []*olmv1Alpha.ClusterServiceVersion, pods []corev1.Pod) ([]*corev1.Pod, error) { + // Helper var to store all the managed crds from the operators under test + // They map key is "Kind.group/version" or "Kind.APIversion", which should be the same. + // e.g.: "Subscription.operators.coreos.com/v1alpha1" + crds := map[string]*olmv1Alpha.ClusterServiceVersion{} + + // First, iterate on each testCsv to fill the helper crds map. + for _, csv := range testCsvs { + ownedCrds := csv.Spec.CustomResourceDefinitions.Owned + if len(ownedCrds) == 0 { + continue + } + + for i := range ownedCrds { + crd := &ownedCrds[i] + + _, group, found := strings.Cut(crd.Name, ".") + if !found { + return nil, fmt.Errorf("failed to parse resources and group from crd name %q", crd.Name) + } + + log.Info("CSV %q owns crd %v", csv.Name, crd.Kind+"/"+group+"/"+crd.Version) + + crdPath := path.Join(crd.Kind, group, crd.Version) + crds[crdPath] = csv + } + } + + // Now, iterate on every pod in the list to check whether they're owned by any of the CRs that + // the csvs are managing. + operandPods := []*corev1.Pod{} + for i := range pods { + pod := &pods[i] + owners, err := podhelper.GetPodTopOwner(pod.Namespace, pod.OwnerReferences) + if err != nil { + return nil, fmt.Errorf("failed to get top owners of pod %v/%v: %v", pod.Namespace, pod.Name, err) + } + + for _, owner := range owners { + versionedCrdPath := path.Join(owner.Kind, owner.APIVersion) + + var csv *olmv1Alpha.ClusterServiceVersion + if csv = crds[versionedCrdPath]; csv == nil { + // The owner is not a CR or it's not a CR owned by any operator under test + continue + } + + log.Info("Pod %v/%v has owner CR %s of CRD %q (CSV %v)", pod.Namespace, pod.Name, + owner.Name, versionedCrdPath, csv.Name) + + operandPods = append(operandPods, pod) + break + } + } + + return operandPods, nil +} diff --git a/pkg/autodiscover/autodiscover_operators.go b/pkg/autodiscover/autodiscover_operators.go index 07f330fe7..e2ededd60 100644 --- a/pkg/autodiscover/autodiscover_operators.go +++ b/pkg/autodiscover/autodiscover_operators.go @@ -116,6 +116,7 @@ func getAllNamespaces(oc corev1client.CoreV1Interface) (allNs []string, err erro } return allNs, nil } + func getAllOperators(olmClient clientOlm.Interface) ([]*olmv1Alpha.ClusterServiceVersion, error) { csvs := []*olmv1Alpha.ClusterServiceVersion{} diff --git a/pkg/podhelper/podhelper.go b/pkg/podhelper/podhelper.go index dd47dc3dd..85ff4f18e 100644 --- a/pkg/podhelper/podhelper.go +++ b/pkg/podhelper/podhelper.go @@ -3,9 +3,9 @@ package podhelper import ( "context" "fmt" - "strings" "github.com/redhat-best-practices-for-k8s/certsuite/internal/clientsholder" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" @@ -13,9 +13,10 @@ import ( // Structure to describe a top owner of a pod type TopOwner struct { - Kind string - Name string - Namespace string + APIVersion string + Kind string + Name string + Namespace string } // Get the list of top owners of pods @@ -23,7 +24,7 @@ func GetPodTopOwner(podNamespace string, podOwnerReferences []metav1.OwnerRefere topOwners = make(map[string]TopOwner) err = followOwnerReferences(clientsholder.GetClientsHolder().GroupResources, clientsholder.GetClientsHolder().DynamicClient, topOwners, podNamespace, podOwnerReferences) if err != nil { - return topOwners, fmt.Errorf("could not get top owners, err=%s", err) + return topOwners, fmt.Errorf("could not get top owners, err: %v", err) } return topOwners, nil } @@ -31,43 +32,67 @@ func GetPodTopOwner(podNamespace string, podOwnerReferences []metav1.OwnerRefere // Recursively follow the ownership tree to find the top owners func followOwnerReferences(resourceList []*metav1.APIResourceList, dynamicClient dynamic.Interface, topOwners map[string]TopOwner, namespace string, ownerRefs []metav1.OwnerReference) (err error) { for _, ownerRef := range ownerRefs { - // Get group resource version - gvr := getResourceSchema(resourceList, ownerRef.APIVersion, ownerRef.Kind) - // Get the owner resources - resource, err := dynamicClient.Resource(gvr).Namespace(namespace).Get(context.Background(), ownerRef.Name, metav1.GetOptions{}) + apiResource, err := searchAPIResource(ownerRef.Kind, ownerRef.APIVersion, resourceList) if err != nil { - return fmt.Errorf("could not get object indicated by owner references") + return fmt.Errorf("error searching APIResource for owner reference %v: %v", ownerRef, err) + } + + gv, err := schema.ParseGroupVersion(ownerRef.APIVersion) + if err != nil { + return fmt.Errorf("failed to parse apiVersion %q: %v", ownerRef.APIVersion, err) + } + + gvr := schema.GroupVersionResource{ + Group: gv.Group, + Version: gv.Version, + Resource: apiResource.Name, + } + + // If the owner reference is a non-namespaced resource (like Node), we need to change the namespace to empty string. + if !apiResource.Namespaced { + namespace = "" + } + + // Get the owner resource, but don't care if it's not found: it might happen for ocp jobs that are constantly + // spawned and removed after completion. + resource, err := dynamicClient.Resource(gvr).Namespace(namespace).Get(context.Background(), ownerRef.Name, metav1.GetOptions{}) + if err != nil && !k8serrors.IsNotFound(err) { + return fmt.Errorf("could not get object indicated by owner references %+v (gvr=%+v): %v", ownerRef, gvr, err) } + // Get owner references of the unstructured object ownerReferences := resource.GetOwnerReferences() // if no owner references, we have reached the top record it if len(ownerReferences) == 0 { - topOwners[ownerRef.Name] = TopOwner{Kind: ownerRef.Kind, Name: ownerRef.Name, Namespace: namespace} + topOwners[ownerRef.Name] = TopOwner{APIVersion: ownerRef.APIVersion, Kind: ownerRef.Kind, Name: ownerRef.Name, Namespace: namespace} + continue } - // if not continue following other branches + err = followOwnerReferences(resourceList, dynamicClient, topOwners, namespace, ownerReferences) if err != nil { - return fmt.Errorf("error following owners") + return err } } + return nil } -// Get the Group Version Resource based on APIVersion and kind -func getResourceSchema(resourceList []*metav1.APIResourceList, apiVersion, kind string) (gvr schema.GroupVersionResource) { - const groupVersionComponentsNumber = 2 - for _, gr := range resourceList { - for i := 0; i < len(gr.APIResources); i++ { - if gr.APIResources[i].Kind == kind && gr.GroupVersion == apiVersion { - groupSplit := strings.Split(gr.GroupVersion, "/") - if len(groupSplit) == groupVersionComponentsNumber { - gvr.Group = groupSplit[0] - gvr.Version = groupSplit[1] - gvr.Resource = gr.APIResources[i].Name - } - return gvr +// searchAPIResource is a helper func that returns the metav1.APIResource pointer of the resource by kind and apiVersion. +// from a metav1.APIResourceList. +func searchAPIResource(kind, apiVersion string, apis []*metav1.APIResourceList) (*metav1.APIResource, error) { + for _, api := range apis { + if api.GroupVersion != apiVersion { + continue + } + + for i := range api.APIResources { + apiResource := &api.APIResources[i] + + if kind == apiResource.Kind { + return apiResource, nil } } } - return gvr + + return nil, fmt.Errorf("apiResource not found for kind=%v and APIVersion=%v", kind, apiVersion) } diff --git a/pkg/podhelper/podhelper_test.go b/pkg/podhelper/podhelper_test.go index b9ee6ef76..0d4ccaa0a 100644 --- a/pkg/podhelper/podhelper_test.go +++ b/pkg/podhelper/podhelper_test.go @@ -9,8 +9,10 @@ import ( "k8s.io/apimachinery/pkg/runtime" appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" k8sDynamicFake "k8s.io/client-go/dynamic/fake" + k8sscheme "k8s.io/client-go/kubernetes/scheme" k8stesting "k8s.io/client-go/testing" ) @@ -46,11 +48,29 @@ func Test_followOwnerReferences(t *testing.T) { }, } + node1 := &corev1.Node{ + TypeMeta: metav1.TypeMeta{Kind: "Node", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + } + + pod1 := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "ns1", + OwnerReferences: []metav1.OwnerReference{{APIVersion: "v1", Kind: "Node", Name: "node1"}}, + }, + } + resourceList := []*metav1.APIResourceList{ - {GroupVersion: "operators.coreos.com/v1alpha1", APIResources: []metav1.APIResource{{Name: "clusterserviceversions", Kind: "ClusterServiceVersion"}}}, - {GroupVersion: "apps/v1", APIResources: []metav1.APIResource{{Name: "deployments", Kind: "Deployment"}}}, - {GroupVersion: "apps/v1", APIResources: []metav1.APIResource{{Name: "replicasets", Kind: "ReplicaSet"}}}, - {GroupVersion: "apps/v1", APIResources: []metav1.APIResource{{Name: "pods", Kind: "Pod"}}}, + {GroupVersion: "operators.coreos.com/v1alpha1", APIResources: []metav1.APIResource{{Name: "clusterserviceversions", Kind: "ClusterServiceVersion", Namespaced: true}}}, + {GroupVersion: "apps/v1", APIResources: []metav1.APIResource{{Name: "deployments", Kind: "Deployment", Namespaced: true}}}, + {GroupVersion: "apps/v1", APIResources: []metav1.APIResource{{Name: "replicasets", Kind: "ReplicaSet", Namespaced: true}}}, + {GroupVersion: "apps/v1", APIResources: []metav1.APIResource{{Name: "pods", Kind: "Pod", Namespaced: true}}}, + {GroupVersion: "v1", APIResources: []metav1.APIResource{{Name: "nodes", Kind: "Node", Namespaced: false}}}, + {GroupVersion: "v1", APIResources: []metav1.APIResource{{Name: "pods", Kind: "Pod", Namespaced: true}}}, } tests := []struct { @@ -60,15 +80,31 @@ func Test_followOwnerReferences(t *testing.T) { }{ { name: "test1", - args: args{topOwners: map[string]TopOwner{"csv1": {Namespace: "ns1", Kind: "ClusterServiceVersion", Name: "csv1"}}, + args: args{topOwners: map[string]TopOwner{"csv1": {APIVersion: "operators.coreos.com/v1alpha1", Namespace: "ns1", Kind: "ClusterServiceVersion", Name: "csv1"}}, namespace: "ns1", ownerRefs: []metav1.OwnerReference{{APIVersion: "apps/v1", Kind: "ReplicaSet", Name: "rep1"}}, }, }, + { + name: "test2 - non-namespaced owner: pod owned a node", + args: args{topOwners: map[string]TopOwner{"node1": {APIVersion: "v1", Namespace: "", Kind: "Node", Name: "node1"}}, + namespace: "ns1", + ownerRefs: []metav1.OwnerReference{{APIVersion: "v1", Kind: "Pod", Name: "pod1"}}, + }, + }, + } + + scheme := runtime.NewScheme() + // Add native resources to the scheme, otherwise, resources of APIVersion "v1" (not "core/v1") won't be found as unstructured resource in the type to GKV map here: + // https://github.com/kubernetes/apimachinery/blob/96b97de8d6ba49bc192968551f2120ef3881f42d/pkg/runtime/scheme.go#L263 + err := k8sscheme.AddToScheme(scheme) + if err != nil { + t.Errorf("failed to ad k8s resources to scheme: %v", err) } - // Spoof the get and update functions - client := k8sDynamicFake.NewSimpleDynamicClient(runtime.NewScheme(), rep1, dep1, csv1) + client := k8sDynamicFake.NewSimpleDynamicClient(scheme, rep1, dep1, csv1, node1, pod1) + + // Spoof the get functions client.Fake.AddReactor("get", "ClusterServiceVersion", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { return true, csv1, nil }) @@ -78,6 +114,13 @@ func Test_followOwnerReferences(t *testing.T) { client.Fake.AddReactor("get", "ReplicaSet", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { return true, rep1, nil }) + client.Fake.AddReactor("get", "Node", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { + return true, node1, nil + }) + client.Fake.AddReactor("get", "Pod", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { + return true, pod1, nil + }) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotResults := map[string]TopOwner{} diff --git a/pkg/provider/operators.go b/pkg/provider/operators.go index 3ac2072e5..b105d0ea2 100644 --- a/pkg/provider/operators.go +++ b/pkg/provider/operators.go @@ -55,7 +55,8 @@ type Operator struct { Version string `yaml:"version" json:"version"` Channel string `yaml:"channel" json:"channel"` PackageFromCsvName string `yaml:"packagefromcsvname" json:"packagefromcsvname"` - PreflightResults PreflightResultsDB + PreflightResults PreflightResultsDB `yaml:"operandPods" json:"operandPods"` + OperandPods map[string]*Pod } type CsvInstallPlan struct { @@ -365,28 +366,50 @@ func GetAllOperatorGroups() ([]*olmv1.OperatorGroup, error) { return operatorGroups, nil } -func addOperatorPodsToTestPods(operatorPods []*Pod, env *TestEnvironment) { +func searchPodInSlice(name, namespace string, pods []*Pod) *Pod { // Helper map to filter pods that have been already added - testPodsMap := map[types.NamespacedName]*Pod{} - for _, testPod := range env.Pods { - testPodsMap[types.NamespacedName{Namespace: testPod.Namespace, Name: testPod.Name}] = testPod + podsMap := map[types.NamespacedName]*Pod{} + for _, testPod := range pods { + podsMap[types.NamespacedName{Namespace: testPod.Namespace, Name: testPod.Name}] = testPod } - // Now check that the operator pod doesn't exist yet. If it exists, make sure it's flagged as operator pod. - for _, operatorPod := range operatorPods { - operatorPodKey := types.NamespacedName{Namespace: operatorPod.Namespace, Name: operatorPod.Name} - if pod, found := testPodsMap[operatorPodKey]; found { - log.Info("Operator pod %v already discovered.", operatorPodKey) + // Search by namespace+name key + podKey := types.NamespacedName{Namespace: namespace, Name: name} + if pod, found := podsMap[podKey]; found { + return pod + } + return nil +} + +func addOperatorPodsToTestPods(operatorPods []*Pod, env *TestEnvironment) { + for _, operatorPod := range operatorPods { + // Check whether the pod was already discovered + testPod := searchPodInSlice(operatorPod.Name, operatorPod.Namespace, env.Pods) + if testPod != nil { + log.Info("Operator pod %v/%v already discovered.", testPod.Namespace, testPod.Name) // Make sure it's flagged as operator pod. - pod.IsOperator = true + testPod.IsOperator = true } else { - log.Info("Operator pod %v added to test pod list", operatorPodKey) + log.Info("Operator pod %v/%v added to test pod list", operatorPod.Namespace, operatorPod.Name) // Append pod to the test pod list. env.Pods = append(env.Pods, operatorPod) + } + } +} - // Update the helper map. - testPodsMap[operatorPodKey] = operatorPod +func addOperandPodsToTestPods(operandPods []*Pod, env *TestEnvironment) { + for _, operandPod := range operandPods { + // Check whether the pod was already discovered + testPod := searchPodInSlice(operandPod.Name, operandPod.Namespace, env.Pods) + if testPod != nil { + log.Info("Operand pod %v/%v already discovered.", testPod.Namespace, testPod.Name) + // Make sure it's flagged as operand pod. + testPod.IsOperand = true + } else { + log.Info("Operand pod %v/%v added to test pod list", operandPod.Namespace, operandPod.Name) + // Append pod to the test pod list. + env.Pods = append(env.Pods, operandPod) } } } diff --git a/pkg/provider/pods.go b/pkg/provider/pods.go index 42cd1bfc9..87316c70d 100644 --- a/pkg/provider/pods.go +++ b/pkg/provider/pods.go @@ -49,6 +49,7 @@ type Pod struct { SkipNetTests bool SkipMultusNetTests bool IsOperator bool + IsOperand bool } func NewPod(aPod *corev1.Pod) (out Pod) { diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 999930b10..39b07475f 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -211,7 +211,7 @@ func deployDaemonSet(namespace string) error { return nil } -func buildTestEnvironment() { //nolint:funlen +func buildTestEnvironment() { //nolint:funlen,gocyclo start := time.Now() env = TestEnvironment{} @@ -299,6 +299,16 @@ func buildTestEnvironment() { //nolint:funlen // Add operator pods to list of normal pods to test. addOperatorPodsToTestPods(csvPods, &env) + // Best effort mode autodiscovery for operand pods. + operandPods := []*Pod{} + for _, pod := range data.OperandPods { + aNewPod := NewPod(pod) + aNewPod.AllServiceAccountsMap = &env.AllServiceAccountsMap + aNewPod.IsOperand = true + operandPods = append(operandPods, &aNewPod) + } + + addOperandPodsToTestPods(operandPods, &env) // Add operator pods' containers to the list. for _, pod := range env.Pods { // Note: 'getPodContainers' is returning a filtered list of Container objects. @@ -362,6 +372,7 @@ func buildTestEnvironment() { //nolint:funlen log.Warn("Pod %q has been deployed using a DeploymentConfig, please use Deployment or StatefulSet instead.", pod.String()) } } + log.Info("Completed the test environment build process in %.2f seconds", time.Since(start).Seconds()) } diff --git a/pkg/stringhelper/stringhelper.go b/pkg/stringhelper/stringhelper.go index 6d4d01cf5..e16752b2b 100644 --- a/pkg/stringhelper/stringhelper.go +++ b/pkg/stringhelper/stringhelper.go @@ -22,9 +22,9 @@ import ( ) // StringInSlice checks a slice for a given string. -func StringInSlice[T ~string](s []T, str T, contains bool) bool { +func StringInSlice[T ~string](s []T, str T, containsCheck bool) bool { for _, v := range s { - if !contains { + if !containsCheck { if strings.TrimSpace(string(v)) == string(str) { return true } diff --git a/pkg/stringhelper/stringhelper_test.go b/pkg/stringhelper/stringhelper_test.go index 5cb8735c8..c1fc88cae 100644 --- a/pkg/stringhelper/stringhelper_test.go +++ b/pkg/stringhelper/stringhelper_test.go @@ -71,6 +71,22 @@ func TestStringInSlice(t *testing.T) { containsFeature: false, // Note: Turn 'off' the contains check expected: false, }, + { + testSlice: []string{ + "oneapple", + }, + testString: "apple", + containsFeature: false, // Note: Turn 'off' the contains check + expected: false, + }, + { + testSlice: []string{ + "apples", + }, + testString: "twoapples", + containsFeature: false, // Note: Turn 'off' the contains check + expected: false, + }, } for _, tc := range testCases { @@ -125,6 +141,14 @@ func TestStringInSlice_other(t *testing.T) { containsFeature: false, // Note: Turn 'off' the contains check expected: false, }, + { + testSlice: []otherString{ + "intreeintreeintree", + }, + testString: "intree", + containsFeature: false, // Note: Turn 'off' the contains check + expected: false, + }, } for _, tc := range testCases { diff --git a/tests/identifiers/doclinks.go b/tests/identifiers/doclinks.go index 9fa0d3d17..44cac74f9 100644 --- a/tests/identifiers/doclinks.go +++ b/tests/identifiers/doclinks.go @@ -107,6 +107,7 @@ const ( TestOperatorNoPrivilegesDocLink = DocOperatorRequirement TestOperatorIsCertifiedIdentifierDocLink = DocOperatorRequirement TestOperatorIsInstalledViaOLMIdentifierDocLink = DocOperatorRequirement + TestOperatorInstallationInTenantNamespaceDocLink = DocOperatorRequirement TestOperatorHasSemanticVersioningIdentifierDocLink = DocOperatorRequirement TestOperatorCrdSchemaIdentifierDocLink = DocOperatorRequirement TestOperatorCrdVersioningIdentifierDocLink = DocOperatorRequirement diff --git a/tests/identifiers/identifiers.go b/tests/identifiers/identifiers.go index ec505d4e2..a30b14199 100644 --- a/tests/identifiers/identifiers.go +++ b/tests/identifiers/identifiers.go @@ -134,6 +134,7 @@ var ( TestOperatorSingleCrdOwnerIdentifier claim.Identifier TestOperatorPodsNoHugepages claim.Identifier TestMultipleSameOperatorsIdentifier claim.Identifier + TestOperatorInstallationInTenantNamespace claim.Identifier TestPodNodeSelectorAndAffinityBestPractices claim.Identifier TestPodHighAvailabilityBestPractices claim.Identifier TestPodClusterRoleBindingsBestPracticesIdentifier claim.Identifier @@ -972,6 +973,22 @@ that Node's kernel may not have the same hacks.'`, }, TagCommon) + TestOperatorInstallationInTenantNamespace = AddCatalogEntry( + "valid-installation-tenant-namespace", + common.OperatorTestKey, + `Tests whether operator installation is valid in tenant namespace.`, + OperatorInstallationInTenantNamespaceRemediation, + NoExceptions, + TestOperatorInstallationInTenantNamespaceDocLink, + false, + map[string]string{ + FarEdge: Mandatory, + Telco: Mandatory, + NonTelco: Mandatory, + Extended: Mandatory, + }, + TagCommon) + TestOperatorHasSemanticVersioningIdentifier = AddCatalogEntry( "semantic-versioning", common.OperatorTestKey, diff --git a/tests/identifiers/remediation.go b/tests/identifiers/remediation.go index 510594360..fb8ae588a 100644 --- a/tests/identifiers/remediation.go +++ b/tests/identifiers/remediation.go @@ -103,6 +103,8 @@ const ( MultipleSameOperatorsRemediation = `Ensure that only one Operator of the same type is installed in the cluster.` + OperatorInstallationInTenantNamespaceRemediation = `Ensure that operator with install mode SingleNamespace only is installed in the tenant namespace.` + PodNodeSelectorAndAffinityBestPracticesRemediation = `In most cases, Pod's should not specify their host Nodes through nodeSelector or nodeAffinity. However, there are cases in which workloads require specialized hardware specific to a particular class of Node.` PodHighAvailabilityBestPracticesRemediation = `In high availability cases, Pod podAntiAffinity rule should be specified for pod scheduling and pod replica value is set to more than 1 .` diff --git a/tests/operator/helper.go b/tests/operator/helper.go index 793d81511..0067e65d8 100644 --- a/tests/operator/helper.go +++ b/tests/operator/helper.go @@ -20,7 +20,13 @@ Package operator provides CNFCERT tests used to validate operator CNF facets. package operator -import "strings" +import ( + "strings" + + operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" + "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/redhat-best-practices-for-k8s/certsuite/pkg/stringhelper" +) // CsvResult holds the results of the splitCsv function. type CsvResult struct { @@ -45,3 +51,28 @@ func SplitCsv(csv string) CsvResult { } return result } + +func isInstallModeSingleNamespace(installModes []v1alpha1.InstallMode) bool { + for i := 0; i < len(installModes); i++ { + if installModes[i].Type == v1alpha1.InstallModeTypeSingleNamespace { + return true + } + } + return false +} + +func findOperatorGroup(name, namespace string, groups []*operatorsv1.OperatorGroup) *operatorsv1.OperatorGroup { + for _, group := range groups { + if group.Name == name && group.Namespace == namespace { + return group + } + } + return nil +} + +func checkOperatorInstallationCompliance(opGroupTargetNamespaces []string, operatorNamespace string, targetNamespaces []string, isSingleNamespaceInstallMode bool) bool { + if isSingleNamespaceInstallMode { + return len(opGroupTargetNamespaces) == 1 && len(targetNamespaces) == 1 && opGroupTargetNamespaces[0] == targetNamespaces[0] + } + return stringhelper.StringInSlice(opGroupTargetNamespaces, operatorNamespace, false) // false in the function arg indicates equals check +} diff --git a/tests/operator/suite.go b/tests/operator/suite.go index ed44397ce..6854dfcbd 100644 --- a/tests/operator/suite.go +++ b/tests/operator/suite.go @@ -17,6 +17,7 @@ package operator import ( + "fmt" "strings" "github.com/redhat-best-practices-for-k8s/certsuite/tests/common" @@ -115,9 +116,74 @@ func LoadChecks() { testMultipleSameOperators(c, &env) return nil })) + + checksGroup.Add(checksdb.NewCheck(identifiers.GetTestIDAndLabels(identifiers.TestOperatorInstallationInTenantNamespace)). + WithSkipCheckFn(testhelper.GetNoOperatorsSkipFn(&env)). + WithCheckFn(func(c *checksdb.Check) error { + testOperatorInstallationInTenantNamespace(c, &env) + return nil + })) +} + +/* +Checks : + + 1. Operators whose InstallTypeMode is not SingleNamespace must not be installed in the namespaces + specified by targetNamespace in the OperatorGroup of the operators + + 2. Operators that are SingleNamespace must have CRs in only tenant namespace +*/ +func testOperatorInstallationInTenantNamespace(check *checksdb.Check, + env *provider.TestEnvironment) { + check.LogInfo("Starting testInstalledSingleNamespaceOperatorInTenanttNamespace") + var compliantObjects []*testhelper.ReportObject + var nonCompliantObjects []*testhelper.ReportObject + + for _, operator := range env.Operators { + check.LogInfo("Checking operator %s in namespace %s ", operator.Name, operator.Namespace) + + csv := operator.Csv + + operatorNamespace := csv.Annotations["olm.operatorNamespace"] + targetNamespacesStr := csv.Annotations["olm.targetNamespaces"] + operatorTargetNamespaces := strings.Split(targetNamespacesStr, ",") + operatorGroup := findOperatorGroup(csv.Annotations["olm.operatorGroup"], operator.Namespace, env.OperatorGroups) + + check.LogInfo("operatorNamespace %s, targetNamespaces %v, operatorGroup %s", operatorNamespace, + operatorTargetNamespaces, operatorGroup.Name) + + isSingleNamespaceInstallMode := isInstallModeSingleNamespace(csv.Spec.InstallModes) + isCompliant := checkOperatorInstallationCompliance( + operatorGroup.Spec.TargetNamespaces, + operatorNamespace, + operatorTargetNamespaces, + isSingleNamespaceInstallMode, + ) + + message := "Operator with %s is %sinstalled in tenant namespace " + mode := "SingleNamespace InstallMode" + if !isSingleNamespaceInstallMode { + mode = "no SingleNamespace InstallMode" + } + status := "not " + if isCompliant { + status = "" + msg := fmt.Sprintf(message, mode, status) + check.LogInfo(msg) + compliantObjects = append(compliantObjects, testhelper.NewOperatorReportObject(operator.Namespace, operator.Name, + msg, true).AddField(testhelper.OperatorName, operator.Name)) + } else { + msg := fmt.Sprintf(message, mode, status) + check.LogInfo(msg) + nonCompliantObjects = append(nonCompliantObjects, testhelper.NewOperatorReportObject(operator.Namespace, operator.Name, + msg, false).AddField(testhelper.OperatorName, operator.Name)) + } + } + + check.SetResult(compliantObjects, nonCompliantObjects) } -// This function check if the Operator CRD version follows K8s versioning +// This function checks if the Operator CRD version follows K8s versioning func testOperatorCrdVersioning(check *checksdb.Check, env *provider.TestEnvironment) { check.LogInfo("Starting testOperatorCrdVersioning") var compliantObjects []*testhelper.ReportObject diff --git a/tests/platform/operatingsystem/files/rhcos_version_map b/tests/platform/operatingsystem/files/rhcos_version_map index 5a310aad7..0512ae62d 100644 --- a/tests/platform/operatingsystem/files/rhcos_version_map +++ b/tests/platform/operatingsystem/files/rhcos_version_map @@ -388,6 +388,7 @@ 4.15.36 / 415.92.202410020020-0 4.15.37 / 415.92.202410232038-0 4.15.38 / 415.92.202411050056-0 +4.15.39 / 415.92.202411201723-0 4.15.5 / 415.92.202403191241-0 4.15.6 / 415.92.202403270524-0 4.15.7 / 415.92.202403270524-0 @@ -424,6 +425,7 @@ 4.16.20 / 416.94.202410292028-0 4.16.21 / 416.94.202411061221-0 4.16.23 / 416.94.202411111409-0 +4.16.24 / 416.94.202411201433-0 4.16.3 / 416.94.202407081958-0 4.16.4 / 416.94.202407171205-0 4.16.5 / 416.94.202407231922-0 @@ -447,6 +449,7 @@ 4.17.3 / 417.94.202410211619-0 4.17.4 / 417.94.202411050056-0 4.17.5 / 417.94.202411070820-0 +4.17.6 / 417.94.202411201839-0 4.4.0 / 44.81.202004260825-0 4.4.0-rc.0 / 44.81.202003110830-0 4.4.0-rc.1 / 44.81.202003130330-0