diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index 4f8dd4bd1..ca27557f2 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -12,19 +12,21 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.15 + - name: Set up Go 1.17 uses: actions/setup-go@v2 with: - go-version: ^1.15 + go-version: 1.17.9 - name: Check out code into the Go module directory uses: actions/checkout@v2 - name: Rebuild mocks - run: go get github.com/golang/mock/mockgen && make mocks + run: go install github.com/golang/mock/mockgen@v1.6.0 && make mocks - name: Run Tests run: make test + env: + SHELL: /bin/bash close_job: if: github.event.pull_request.merged == false diff --git a/.github/workflows/pre-main.yaml b/.github/workflows/pre-main.yaml index 6b43c9348..df1be2db8 100644 --- a/.github/workflows/pre-main.yaml +++ b/.github/workflows/pre-main.yaml @@ -12,24 +12,30 @@ env: IMAGE_NAME: testnetworkfunction/test-network-function IMAGE_TAG: unstable TNF_CONTAINER_CLIENT: docker - TNF_MINIKUBE_ONLY: true - TNF_NON_INTRUSIVE_ONLY: true + TNF_NON_INTRUSIVE_ONLY: false TNF_DISABLE_CONFIG_AUTODISCOVER: false TNF_CONFIG_DIR: /tmp/tnf/config TNF_OUTPUT_DIR: /tmp/tnf/output TNF_SRC_URL: 'https://github.com/${{ github.repository }}' TESTING_CMD_PARAMS: '-n host -i ${REGISTRY_LOCAL}/${IMAGE_NAME}:${IMAGE_TAG} -t ${TNF_CONFIG_DIR} -o ${TNF_OUTPUT_DIR}' + CONTAINER_DIAGNOSTIC_LOG_LEVEL: trace + TNF_PARTNER_DIR: '/usr/tnf-partner' + TNF_PARTNER_SRC_DIR: '${TNF_PARTNER_DIR}/src' + TERM: xterm-color jobs: lint: - name: Run Linter - runs-on: ubuntu-20.04 + name: Run Linter and Vet + runs-on: ubuntu-22.04 steps: - - name: Set up Go 1.15 + - name: Set up Go 1.17 uses: actions/setup-go@v2 with: - go-version: ^1.15 + go-version: 1.17.9 + + - name: Disable default go problem matcher + run: echo "::remove-matcher owner=go::" - name: Check out code into the Go module directory uses: actions/checkout@v2 @@ -37,28 +43,50 @@ jobs: ref: ${{ github.sha }} - name: Rebuild mocks - run: go get github.com/golang/mock/mockgen && make mocks - - - name: Install golint - run: go get golang.org/x/lint/golint + run: go install github.com/golang/mock/mockgen@v1.6.0 && make mocks - # TODO: golangci-lint team recommends using a GitHub Action to perform golangci-lint responsibilities. However, + # TODO: golangci-lint team recommends using a GitHub Action to perform golangci-lint responsibilities. However # there does not appear to be a way to honor our existing .golangci.yml. For now, mimic developer behavior. - name: Install golangci-lint - run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.39.0 + run: make install-lint - name: make lint run: make lint + - name: make vet + run: make vet + + yamllint: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v2 + - name: yaml-lint + uses: ibiqlik/action-yamllint@v3 + with: + config_data: | + extends: default + rules: + line-length: + level: warning + trailing-spaces: + level: warning + brackets: + level: warning + empty-lines: + level: warning + unit-tests: name: Run Unit Tests - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - - name: Set up Go 1.15 + - name: Set up Go 1.17 uses: actions/setup-go@v2 with: - go-version: ^1.15 + go-version: 1.17.9 + + - name: Disable default go problem matcher + run: echo "::remove-matcher owner=go::" - name: Check out code into the Go module directory uses: actions/checkout@v2 @@ -66,34 +94,55 @@ jobs: ref: ${{ github.sha }} - name: Rebuild mocks - run: go get github.com/golang/mock/mockgen && make mocks + run: go install github.com/golang/mock/mockgen@v1.6.0 && make mocks - name: Run Tests run: make test + env: + SHELL: /bin/bash + + - name: Quality Gate - Test coverage shall be above threshold + env: + TESTCOVERAGE_THRESHOLD: 50 + run: | + echo "Quality Gate: checking test coverage is above threshold ..." + echo "Threshold : $TESTCOVERAGE_THRESHOLD %" + totalCoverage=`UNIT_TEST='true' go tool cover -func=cover.out | grep total | grep -Eo '[0-9]+\.[0-9]+'` + echo "Current test coverage : $totalCoverage %" + if (( $(echo "$totalCoverage $TESTCOVERAGE_THRESHOLD" | awk '{print ($1 > $2)}') )); then + echo "OK" + else + echo "Current test coverage is below threshold. Please add more unit tests or adjust threshold to a lower value." + echo "Failed" + exit 1 + fi smoke-tests: name: Run Smoke Tests - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 env: SHELL: /bin/bash KUBECONFIG: '/home/runner/.kube/config' steps: - - name: Set up Go 1.15 + - name: Set up Go 1.17 uses: actions/setup-go@v2 with: - go-version: ^1.15 + go-version: 1.17.9 + - name: Disable default go problem matcher + run: echo "::remove-matcher owner=go::" + - name: Check out code into the Go module directory uses: actions/checkout@v2 with: ref: ${{ github.sha }} - name: Execute `make mocks` - run: go get github.com/golang/mock/mockgen && make mocks + run: go install github.com/golang/mock/mockgen@v1.6.0 && make mocks - name: Install ginkgo - run: go get -u github.com/onsi/ginkgo/ginkgo + run: go install github.com/onsi/ginkgo/v2/ginkgo@v2.1.3 - name: Execute `make build` run: make build @@ -107,7 +156,9 @@ jobs: path: cnf-certification-test-partner - name: Start the minikube cluster for `local-test-infra` - uses: ./cnf-certification-test-partner/.github/actions/start-minikube + uses: ./cnf-certification-test-partner/.github/actions/start-k8s-cluster + with: + working_directory: cnf-certification-test-partner - name: Create `local-test-infra` OpenShift resources uses: ./cnf-certification-test-partner/.github/actions/create-local-test-infra-resources @@ -116,11 +167,19 @@ jobs: # Perform smoke tests. - - name: 'Test: Run diagnostic test suite' - run: ./run-cnf-suites.sh diagnostic - - name: 'Test: Run test suites' - run: ./run-cnf-suites.sh access-control lifecycle platform observablility networking + run: ./run-cnf-suites.sh --focus access-control lifecycle platform observability networking affiliated-certification operator + + - name: Upload smoke test results as an artifact + uses: actions/upload-artifact@v2 + if: always() + with: + name: smoke-tests + path: | + test-network-function/*.xml + test-network-function/claim.json + test-network-function/claimjson.js + test-network-function/results.html # Perform smoke tests using a TNF container. @@ -140,14 +199,24 @@ jobs: cp test-network-function/*.yml $TNF_CONFIG_DIR shell: bash - - name: 'Test: Run diagnostic test suite in a TNF container' - run: ./run-tnf-container.sh ${{ env.TESTING_CMD_PARAMS }} diagnostic - + - name: 'Test: Run without any TS, just get diagnostic information' + run: LOG_LEVEL=${CONTAINER_DIAGNOSTIC_LOG_LEVEL} ./run-tnf-container.sh ${{ env.TESTING_CMD_PARAMS }} + - name: 'Test: Run generic test suite in a TNF container' - run: ./run-tnf-container.sh ${{ env.TESTING_CMD_PARAMS }} generic access-control + run: ./run-tnf-container.sh ${{ env.TESTING_CMD_PARAMS }} -f access-control lifecycle platform observability networking affiliated-certification operator - # Push the new unstable TNF image to Quay.io. + - name: Upload container test results as an artifact + uses: actions/upload-artifact@v2 + if: always() + with: + name: smoke-tests-container + path: | + ${{ env.TNF_OUTPUT_DIR }}/*.xml + ${{ env.TNF_OUTPUT_DIR }}/claim.json + ${{ env.TNF_OUTPUT_DIR }}/claimjson.js + ${{ env.TNF_OUTPUT_DIR }}/results.html + # Push the new unstable TNF image to Quay.io. - name: (if on main and upstream) Authenticate against Quay.io if: ${{ github.ref == 'refs/heads/main' && github.repository_owner == 'test-network-function' }} uses: docker/login-action@v1 diff --git a/.github/workflows/tnf-image.yaml b/.github/workflows/tnf-image.yaml index 6e7bfe7e8..05cda36cd 100644 --- a/.github/workflows/tnf-image.yaml +++ b/.github/workflows/tnf-image.yaml @@ -20,54 +20,75 @@ env: REGISTRY_LOCAL: localhost IMAGE_NAME: testnetworkfunction/test-network-function IMAGE_TAG: latest - CURRENT_VERSION_GENERIC_BRANCH: 2.0.x TNF_CONTAINER_CLIENT: docker + TNF_NON_INTRUSIVE_ONLY: false TNF_MINIKUBE_ONLY: true - TNF_NON_INTRUSIVE_ONLY: true TNF_DISABLE_CONFIG_AUTODISCOVER: false TNF_CONFIG_DIR: /tmp/tnf/config TNF_OUTPUT_DIR: /tmp/tnf/output TNF_SRC_URL: 'https://github.com/${{ github.repository }}' + PARTNER_REPO: test-network-function/cnf-certification-test-partner + PARTNER_SRC_URL: 'https://github.com/${PARTNER_REPO}' TESTING_CMD_PARAMS: '-n host -i ${REGISTRY_LOCAL}/${IMAGE_NAME}:${IMAGE_TAG} -t ${TNF_CONFIG_DIR} -o ${TNF_OUTPUT_DIR}' + LATEST_BRANCH_VERSION: 3.3.x jobs: - get-latest-tnf-version-number: - name: 'Get the version number of the latest release' - if: ${{ github.repository_owner == 'test-network-function' }} - runs-on: ubuntu-20.04 - outputs: - TNF_VERSION: ${{ steps.set_tnf_version.outputs.version_number }} - + test-and-push-tnf-image: + name: 'Test and push the `test-network-function` image' + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + branch: [3.1.x, 3.2.x, 3.3.x] + env: + SHELL: /bin/bash + KUBECONFIG: '/home/runner/.kube/config' + CURRENT_VERSION_GENERIC_BRANCH: ${{ matrix.branch }} + TNF_VERSION: "" + PARTNER_VERSION: "" steps: + - name: Checkout generic working branch of the current version uses: actions/checkout@v2 with: ref: ${{ env.CURRENT_VERSION_GENERIC_BRANCH }} + fetch-depth: '0' + + - name: Get the latest TNF version from GIT + run: | + GIT_RELEASE=$(git tag --points-at HEAD | head -n 1) + GIT_PREVIOUS_RELEASE=$(git tag --no-contains HEAD --sort=v:refname | tail -n 1) + GIT_LATEST_RELEASE=$GIT_RELEASE + if [ -z "$GIT_RELEASE" ]; then + GIT_LATEST_RELEASE=$GIT_PREVIOUS_RELEASE + fi + + echo "::set-output name=version_number::$GIT_LATEST_RELEASE" + id: set_tnf_version + + - name: Print the latest TNF version from GIT + run: | + echo Version tag: ${{ steps.set_tnf_version.outputs.version_number }} - name: Get contents of the version.json file run: echo "::set-output name=json::$(cat version.json | tr -d '[:space:]')" id: get_version_json_file - - name: Save the version number to $TNF_VERSION + - name: Get the partner version number from file run: | - echo Version tag: $VERSION_FROM_FILE - echo "::set-output name=version_number::$VERSION_FROM_FILE" - id: set_tnf_version + echo Partner version tag: $VERSION_FROM_FILE_PARTNER + echo "::set-output name=partner_version_number::$VERSION_FROM_FILE_PARTNER" + id: set_partner_version env: - VERSION_FROM_FILE: ${{ fromJSON(steps.get_version_json_file.outputs.json)['tag'] }} - - test-and-push-tnf-image: - name: 'Test and push the `test-network-function` image' - needs: [get-latest-tnf-version-number] - runs-on: ubuntu-20.04 - env: - SHELL: /bin/bash - KUBECONFIG: '/home/runner/.kube/config' - TNF_VERSION: ${{ needs['get-latest-tnf-version-number'].outputs.TNF_VERSION }} - - steps: + VERSION_FROM_FILE_PARTNER: ${{ fromJSON(steps.get_version_json_file.outputs.json).partner_tag }} + + - name: Update env variables + run: | + echo "TNF_VERSION=${{ steps.set_tnf_version.outputs.version_number }}" >> $GITHUB_ENV + echo "PARTNER_VERSION=${{ steps.set_partner_version.outputs.partner_version_number }}" >> $GITHUB_ENV + - name: Ensure $TNF_VERSION and $IMAGE_TAG are set - run: '[[ -n "$TNF_VERSION" ]] && [[ -n "$IMAGE_TAG" ]]' + run: '[[ -n "$TNF_VERSION" ]] && [[ -n "$IMAGE_TAG" ]] && [[ -n "$PARTNER_VERSION" ]]' - name: Check whether the version tag exists on remote run: git ls-remote --exit-code $TNF_SRC_URL refs/tags/$TNF_VERSION @@ -76,6 +97,13 @@ jobs: if: ${{ failure() }} run: echo "Tag '$TNF_VERSION' does not exist on remote $TNF_SRC_URL" + - name: Check whether the version tag exists on remote + run: git ls-remote --exit-code ${{ env.PARTNER_SRC_URL }} refs/tags/$PARTNER_VERSION + + - name: (if partner_tag is missing) Display debug message + if: ${{ failure() }} + run: echo "Tag '$PARTNER_VERSION' does not exist on remote $PARTNER_SRC_URL" + - name: Checkout the version tag uses: actions/checkout@v2 with: @@ -97,8 +125,32 @@ jobs: with: repository: test-network-function/cnf-certification-test-partner path: cnf-certification-test-partner + ref: ${{ env.PARTNER_VERSION }} + + # Only one of the following 2 steps will work depending on the repo version + + # For versions > 3.3.0 + + - name: Check for start-k8s-cluster existence + id: check_start_k8s_cluster + uses: andstor/file-existence-action@v1 + with: + files: "./cnf-certification-test-partner/.github/actions/start-k8s-cluster" + + - name: Start the k8s cluster for `local-test-infra` + if: steps.check_start_k8s_cluster.outputs.files_exists == 'true' + uses: ./cnf-certification-test-partner/.github/actions/start-k8s-cluster + + # For version <= 3.3.0 + + - name: Check for start-minikube existence + id: check_start_minikube + uses: andstor/file-existence-action@v1 + with: + files: "./cnf-certification-test-partner/.github/actions/start-minikube" - name: Start the minikube cluster for `local-test-infra` + if: steps.check_start_minikube.outputs.files_exists == 'true' uses: ./cnf-certification-test-partner/.github/actions/start-minikube - name: Create `local-test-infra` OpenShift resources @@ -114,11 +166,8 @@ jobs: cp test-network-function/*.yml $TNF_CONFIG_DIR shell: bash - - name: 'Test: Run diagnostic test suite in a TNF container' - run: ./run-tnf-container.sh ${{ env.TESTING_CMD_PARAMS }} diagnostic - - - name: 'Test: Run generic test suite in a TNF container' - run: ./run-tnf-container.sh ${{ env.TESTING_CMD_PARAMS }} generic + - name: 'Test: Run without any TS, just get diagnostic information' + run: ./run-tnf-container.sh ${{ env.TESTING_CMD_PARAMS }} -f diagnostic # Push the new TNF image to Quay.io. @@ -132,4 +181,9 @@ jobs: password: ${{ secrets.QUAY_ROBOT_TOKEN }} - name: Push the newly built image to Quay.io - run: docker push --all-tags ${REGISTRY}/${IMAGE_NAME} + run: | + docker push ${REGISTRY}/${IMAGE_NAME}:${TNF_VERSION} + if [ "$CURRENT_VERSION_GENERIC_BRANCH" == "$LATEST_BRANCH_VERSION" ]; then + docker push ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} + fi + diff --git a/.gitignore b/.gitignore index e8f036a10..2ebcc58ff 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ setup_junit.xml validation_junit.xml .vscode jsontest-cli +gradetool +test-out.json +cover.out # Explicitly don't track mocks. mocks should be built on request. pkg/tnf/mocks/mock_tester.go @@ -20,3 +23,9 @@ pkg/tnf/reel/mocks/mock_reel.go # NOTE: mock_expect.go is tracked, as it is generated from source externally. pkg/tnf/interactive/mocks/mock_spawner.go temp/ +/tnf +/all-releases.txt +/latest-release-tag.txt +/release-tag.txt +test-network-function/claimjson.js +test-network-function/results.html diff --git a/.golangci.yml b/.golangci.yml index 44b2bea50..77219f692 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -28,8 +28,6 @@ linters-settings: min-complexity: 15 goimports: local-prefixes: github.com/golangci/golangci-lint - golint: - min-confidence: 0 gomnd: settings: mnd: @@ -72,7 +70,6 @@ linters: - gocyclo - gofmt - goimports - - golint - gomnd - goprintffuncname - gosec @@ -84,12 +81,14 @@ linters: - nakedret - noctx - nolintlint + - revive - rowserrcheck - staticcheck - stylecheck - typecheck - unconvert - unparam + - unused - varcheck - whitespace # don't enable: @@ -106,7 +105,6 @@ linters: # - scopelint # - structcheck # - testpackage - # - unused # - wsl issues: # Excluding configuration per-path, per-linter, per-text and per-source @@ -130,6 +128,6 @@ issues: # golangci.com configuration # https://github.com/golangci/golangci/wiki/Configuration service: - golangci-lint-version: 1.23.x # use the fixed version to not introduce new linters unexpectedly + golangci-lint-version: 1.45.x # use the fixed version to not introduce new linters unexpectedly prepare: - echo "here I can run custom commands, but no preparation needed for this repo" diff --git a/CATALOG.md b/CATALOG.md index 0134a1d1f..53aaf4a13 100644 --- a/CATALOG.md +++ b/CATALOG.md @@ -1,211 +1,487 @@ -# test-network-function test case catalog +# test-network-function Catalog +The catalog for test-network-function contains a variety of `Test Cases`, as well as `Test Case Building Blocks`. + * Test Cases: Traditional JUnit testcases, which are specified internally using `Ginkgo.It`. Test cases often utilize several Test Case Building Blocks. + * Test Case Building Blocks: Self-contained building blocks, which perform a small task in the context of `oc`, `ssh`, `shell`, or some other `Expecter`. + +So, a Test Case could be composed by one or many Test Case Building Blocks. -test-network-function contains a variety of `Test Cases`, as well as `Test Case Building Blocks`. -* Test Cases: Traditional JUnit testcases, which are specified internally using `Ginkgo.It`. Test cases often utilize several Test Case Building Blocks. -* Test Case Building Blocks: Self-contained building blocks, which perform a small task in the context of `oc`, `ssh`, `shell`, or some other `Expecter`. ## Test Case Catalog -Test Cases are the specifications used to perform a meaningful test. Test cases may run once, or several times against several targets. CNF Certification includes a number of normative and informative tests to ensure CNFs follow best practices. Here is the list of available Test Cases: -### http://test-network-function.com/testcases/access-control/cluster-role-bindings +Test Cases are the specifications used to perform a meaningful test. Test cases may run once, or several times against several targets. CNF Certification includes a number of normative and informative tests to ensure CNFs follow best practices. Here is the list of available + +### access-control + +#### cluster-role-bindings Property|Description ---|--- +Test Case Name|cluster-role-bindings +Test Case Label|access-control-cluster-role-bindings +Unique ID|http://test-network-function.com/testcases/access-control/cluster-role-bindings Version|v1.0.0 Description|http://test-network-function.com/testcases/access-control/cluster-role-bindings tests that a Pod does not specify ClusterRoleBindings. Result Type|normative Suggested Remediation|In most cases, Pod's should not have ClusterRoleBindings. The suggested remediation is to remove the need for ClusterRoleBindings, if possible. -### http://test-network-function.com/testcases/access-control/host-resource +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2.10 and 6.3.6 +#### host-resource Property|Description ---|--- +Test Case Name|host-resource +Test Case Label|access-control-host-resource +Unique ID|http://test-network-function.com/testcases/access-control/host-resource Version|v1.0.0 -Description|http://test-network-function.com/testcases/access-control/host-resource tests several aspects of CNF best practices, including: 1. The Pod does not have access to Host Node Networking. 2. The Pod does not have access to Host Node Ports. 3. The Pod cannot access Host Node IPC space. 4. The Pod cannot access Host Node PID space. 5. The Pod is not granted NET_ADMIN SCC. 6. The Pod is not granted SYS_ADMIN SCC. 7. The Pod does not run as root. 8. The Pod does not allow privileged escalation. +Description|http://test-network-function.com/testcases/access-control/host-resource tests several aspects of CNF best practices, including: 1. The Pod does not have access to Host Node Networking. 2. The Pod does not have access to Host Node Ports. 3. The Pod cannot access Host Node IPC space. 4. The Pod cannot access Host Node PID space. 5. The Pod is not granted NET_ADMIN SCC. 6. The Pod is not granted SYS_ADMIN SCC. 7. The Pod does not run as root. 8. The Pod does not allow privileged escalation. 9. The Pod is not granted NET_RAW SCC. 10. The Pod is not granted IPC_LOCK SCC. Result Type|normative Suggested Remediation|Ensure that each Pod in the CNF abides by the suggested best practices listed in the test description. In some rare cases, not all best practices can be followed. For example, some CNFs may be required to run as root. Such exceptions should be handled on a case-by-case basis, and should provide a proper justification as to why the best practice(s) cannot be followed. -### http://test-network-function.com/testcases/access-control/namespace +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### namespace + +Property|Description +---|--- +Test Case Name|namespace +Test Case Label|access-control-namespace +Unique ID|http://test-network-function.com/testcases/access-control/namespace +Version|v1.0.0 +Description|http://test-network-function.com/testcases/access-control/namespace tests that all CNF's resources (PUTs and CRs) belong to valid namespaces. A valid namespace meets the following conditions: (1) It was declared in the yaml config file under the targetNameSpaces tag. (2) It doesn't have any of the following prefixes: default, openshift-, istio- and aspenmesh- +Result Type|normative +Suggested Remediation|Ensure that your CNF utilizes namespaces declared in the yaml config file. Additionally, the namespaces should not start with "default, openshift-, istio- or aspenmesh-", except in rare cases. +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2, 16.3.8 & 16.3.9 +#### pod-automount-service-account-token Property|Description ---|--- +Test Case Name|pod-automount-service-account-token +Test Case Label|access-control-pod-automount-service-account-token +Unique ID|http://test-network-function.com/testcases/access-control/pod-automount-service-account-token Version|v1.0.0 -Description|http://test-network-function.com/testcases/access-control/namespace tests that CNFs utilize a CNF-specific namespace, and that the namespace does not start with "openshift-". OpenShift may host a variety of CNF and software applications, and multi-tenancy of such applications is supported through namespaces. As such, each CNF should be a good neighbor, and utilize an appropriate, unique namespace. +Description|http://test-network-function.com/testcases/access-control/pod-automount-service-account-token check that all pods under test have automountServiceAccountToken set to false Result Type|normative -Suggested Remediation|Ensure that your CNF utilizes a CNF-specific namespace. Additionally, the CNF-specific namespace should not start with "openshift-", except in rare cases. -### http://test-network-function.com/testcases/access-control/pod-role-bindings +Suggested Remediation|check that pod has automountServiceAccountToken set to false or pod is attached to service account which has automountServiceAccountToken set to false +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 13.7 +#### pod-role-bindings Property|Description ---|--- +Test Case Name|pod-role-bindings +Test Case Label|access-control-pod-role-bindings +Unique ID|http://test-network-function.com/testcases/access-control/pod-role-bindings Version|v1.0.0 Description|http://test-network-function.com/testcases/access-control/pod-role-bindings ensures that a CNF does not utilize RoleBinding(s) in a non-CNF Namespace. Result Type|normative Suggested Remediation|Ensure the CNF is not configured to use RoleBinding(s) in a non-CNF Namespace. -### http://test-network-function.com/testcases/access-control/pod-service-account +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.3.3 and 6.3.5 +#### pod-service-account Property|Description ---|--- +Test Case Name|pod-service-account +Test Case Label|access-control-pod-service-account +Unique ID|http://test-network-function.com/testcases/access-control/pod-service-account Version|v1.0.0 Description|http://test-network-function.com/testcases/access-control/pod-service-account tests that each CNF Pod utilizes a valid Service Account. Result Type|normative Suggested Remediation|Ensure that the each CNF Pod is configured to use a valid Service Account -### http://test-network-function.com/testcases/affiliated-certification/container-is-certified +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2.3 and 6.2.7 + +### affiliated-certification + +#### container-is-certified Property|Description ---|--- +Test Case Name|container-is-certified +Test Case Label|affiliated-certification-container-is-certified +Unique ID|http://test-network-function.com/testcases/affiliated-certification/container-is-certified Version|v1.0.0 -Description|http://test-network-function.com/testcases/affiliated-certification/container-is-certified tests whether container images have passed the Red Hat Container Certification Program (CCP). +Description|http://test-network-function.com/testcases/affiliated-certification/container-is-certified tests whether container images listed in the configuration file or used by test target Pods have passed the Red Hat Container Certification Program (CCP) with a [health index](https://redhat-connect.gitbook.io/catalog-help/container-images/container-health) C or above. Result Type|normative Suggested Remediation|Ensure that your container has passed the Red Hat Container Certification Program (CCP). -### http://test-network-function.com/testcases/affiliated-certification/operator-is-certified +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.3.7 +#### helmchart-is-certified Property|Description ---|--- +Test Case Name|helmchart-is-certified +Test Case Label|affiliated-certification-helmchart-is-certified +Unique ID|http://test-network-function.com/testcases/affiliated-certification/helmchart-is-certified Version|v1.0.0 -Description|http://test-network-function.com/testcases/affiliated-certification/operator-is-certified tests whether CNF Operators have passed the Red Hat Operator Certification Program (OCP). +Description|http://test-network-function.com/testcases/affiliated-certification/helmchart-is-certified tests whether helm charts listed in the cluster passed the Red Hat Helm Certification Program. +Result Type|normative +Suggested Remediation|Ensure that the helm charts under test passed the Red Hat's helm Certification Program (e.g. listed in https://charts.openshift.io/index.yaml). +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2.12 and Section 6.3.3 +#### operator-is-certified + +Property|Description +---|--- +Test Case Name|operator-is-certified +Test Case Label|affiliated-certification-operator-is-certified +Unique ID|http://test-network-function.com/testcases/affiliated-certification/operator-is-certified +Version|v1.0.0 +Description|http://test-network-function.com/testcases/affiliated-certification/operator-is-certified tests whether CNF Operators listed in the configuration file have passed the Red Hat Operator Certification Program (OCP). Result Type|normative Suggested Remediation|Ensure that your Operator has passed Red Hat's Operator Certification Program (OCP). -### http://test-network-function.com/testcases/diagnostic/extract-node-information +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2.12 and Section 6.3.3 + +### lifecycle + +#### container-shutdown Property|Description ---|--- +Test Case Name|container-shutdown +Test Case Label|lifecycle-container-shutdown +Unique ID|http://test-network-function.com/testcases/lifecycle/container-shutdown Version|v1.0.0 -Description|http://test-network-function.com/testcases/diagnostic/extract-node-information extracts informational information about the cluster. -Result Type|informative -Suggested Remediation| -### http://test-network-function.com/testcases/diagnostic/list-cni-plugins +Description|http://test-network-function.com/testcases/lifecycle/container-shutdown Ensure that the containers lifecycle pre-stop management feature is configured. +Result Type|normative +Suggested Remediation| It's considered best-practices to define prestop for proper management of container lifecycle. The prestop can be used to gracefully stop the container and clean resources (e.g., DB connection). The prestop can be configured using : 1) Exec : executes the supplied command inside the container 2) HTTP : executes HTTP request against the specified endpoint. When defined. K8s will handle shutdown of the container using the following: 1) K8s first execute the preStop hook inside the container. 2) K8s will wait for a grace period. 3) K8s will clean the remaining processes using KILL signal. +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### deployment-scaling Property|Description ---|--- +Test Case Name|deployment-scaling +Test Case Label|lifecycle-deployment-scaling +Unique ID|http://test-network-function.com/testcases/lifecycle/deployment-scaling Version|v1.0.0 -Description|http://test-network-function.com/testcases/diagnostic/list-cni-plugins lists CNI plugins +Description|http://test-network-function.com/testcases/lifecycle/deployment-scaling tests that CNF deployments support scale in/out operations. First, The test starts getting the current replicaCount (N) of the deployment/s with the Pod Under Test. Then, it executes the scale-in oc command for (N-1) replicas. Lastly, it executes the scale-out oc command, restoring the original replicaCount of the deployment/s. In case of deployments that are managed by HPA the test is changing the min and max value to deployment Replica - 1 during scale-in and the original replicaCount again for both min/max during the scale-out stage. lastly its restoring the original min/max replica of the deployment/s Result Type|normative -Suggested Remediation| -### http://test-network-function.com/testcases/diagnostic/nodes-hw-info +Suggested Remediation|Make sure CNF deployments/replica sets can scale in/out successfully. +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### image-pull-policy Property|Description ---|--- +Test Case Name|image-pull-policy +Test Case Label|lifecycle-image-pull-policy +Unique ID|http://test-network-function.com/testcases/lifecycle/image-pull-policy Version|v1.0.0 -Description|http://test-network-function.com/testcases/diagnostic/nodes-hw-info list nodes HW info +Description|http://test-network-function.com/testcases/lifecycle/image-pull-policy Ensure that the containers under test are using IfNotPresent as Image Pull Policy.. Result Type|normative -Suggested Remediation| -### http://test-network-function.com/testcases/lifecycle/container-shutdown +Suggested Remediation|Ensure that the containers under test are using IfNotPresent as Image Pull Policy. +Best Practice Reference|https://docs.google.com/document/d/1wRHMk1ZYUSVmgp_4kxvqjVOKwolsZ5hDXjr5MLy-wbg/edit# Section 15.6 +#### liveness Property|Description ---|--- +Test Case Name|liveness +Test Case Label|lifecycle-liveness +Unique ID|http://test-network-function.com/testcases/lifecycle/liveness Version|v1.0.0 -Description|http://test-network-function.com/testcases/lifecycle/container-shutdown Ensure that the containers lifecycle pre-stop management feature is configured. +Description|http://test-network-function.com/testcases/lifecycle/liveness Checks that all pods under test have a liveness probe defined. Result Type|normative -Suggested Remediation| It's considered best-practices to define prestop for proper management of container lifecycle. The prestop can be used to gracefully stop the container and clean resources (e.g., DB connexion). The prestop can be configured using : 1) Exec : executes the supplied command inside the container 2) HTTP : executes HTTP request against the specified endpoint. When defined. K8s will handle shutdown of the container using the following: 1) K8s first execute the preStop hook inside the container. 2) K8s will wait for a grace perdiod. 3) K8s will clean the remaining processes using KILL signal. -### http://test-network-function.com/testcases/lifecycle/pod-high-availability +Suggested Remediation|Ensure that all CNF's pods under test have a liveness probe defined. +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### pod-high-availability Property|Description ---|--- +Test Case Name|pod-high-availability +Test Case Label|lifecycle-pod-high-availability +Unique ID|http://test-network-function.com/testcases/lifecycle/pod-high-availability Version|v1.0.0 Description|http://test-network-function.com/testcases/lifecycle/pod-high-availability ensures that CNF Pods specify podAntiAffinity rules and replica value is set to more than 1. Result Type|informative Suggested Remediation|In high availability cases, Pod podAntiAffinity rule should be specified for pod scheduling and pod replica value is set to more than 1 . -### http://test-network-function.com/testcases/lifecycle/pod-owner-type +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### pod-owner-type Property|Description ---|--- +Test Case Name|pod-owner-type +Test Case Label|lifecycle-pod-owner-type +Unique ID|http://test-network-function.com/testcases/lifecycle/pod-owner-type Version|v1.0.0 -Description|http://test-network-function.com/testcases/lifecycle/pod-owner-type tests that CNF Pod(s) are deployed as part of a ReplicaSet(s). +Description|http://test-network-function.com/testcases/lifecycle/pod-owner-type tests that CNF Pod(s) are deployed as part of a ReplicaSet(s)/StatefulSet(s). Result Type|normative -Suggested Remediation|Deploy the CNF using DaemonSet or ReplicaSet. -### http://test-network-function.com/testcases/lifecycle/pod-recreation +Suggested Remediation|Deploy the CNF using ReplicaSet/StatefulSet. +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.3.3 and 6.3.8 +#### pod-recreation Property|Description ---|--- +Test Case Name|pod-recreation +Test Case Label|lifecycle-pod-recreation +Unique ID|http://test-network-function.com/testcases/lifecycle/pod-recreation Version|v1.0.0 Description|http://test-network-function.com/testcases/lifecycle/pod-recreation tests that a CNF is configured to support High Availability. First, this test cordons and drains a Node that hosts the CNF Pod. Next, the test ensures that OpenShift can re-instantiate the Pod on another Node, and that the actual replica count matches the desired replica count. Result Type|normative Suggested Remediation|Ensure that CNF Pod(s) utilize a configuration that supports High Availability. Additionally, ensure that there are available Nodes in the OpenShift cluster that can be utilized in the event that a host Node fails. -### http://test-network-function.com/testcases/lifecycle/pod-scheduling +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### pod-scheduling Property|Description ---|--- +Test Case Name|pod-scheduling +Test Case Label|lifecycle-pod-scheduling +Unique ID|http://test-network-function.com/testcases/lifecycle/pod-scheduling Version|v1.0.0 Description|http://test-network-function.com/testcases/lifecycle/pod-scheduling ensures that CNF Pods do not specify nodeSelector or nodeAffinity. In most cases, Pods should allow for instantiation on any underlying Node. Result Type|informative Suggested Remediation|In most cases, Pod's should not specify their host Nodes through nodeSelector or nodeAffinity. However, there are cases in which CNFs require specialized hardware specific to a particular class of Node. As such, this test is purely informative, and will not prevent a CNF from being certified. However, one should have an appropriate justification as to why nodeSelector and/or nodeAffinity is utilized by a CNF. -### http://test-network-function.com/testcases/lifecycle/pod-termination-grace-period +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### readiness Property|Description ---|--- +Test Case Name|readiness +Test Case Label|lifecycle-readiness +Unique ID|http://test-network-function.com/testcases/lifecycle/readiness Version|v1.0.0 -Description|http://test-network-function.com/testcases/lifecycle/pod-termination-grace-period tests whether the terminationGracePeriod is CNF-specific, or if the default (30s) is utilized. This test is informative, and will not affect CNF Certification. In many cases, the default terminationGracePeriod is perfectly acceptable for a CNF. -Result Type|informative -Suggested Remediation|Choose a terminationGracePeriod that is appropriate for your given CNF. If the default (30s) is appropriate, then feel free to ignore this informative message. This test is meant to raise awareness around how Pods are terminated, and to suggest that a CNF is configured based on its requirements. In addition to a terminationGracePeriod, consider utilizing a termination hook in the case that your application requires special shutdown instructions. -### http://test-network-function.com/testcases/networking/icmpv4-connectivity +Description|http://test-network-function.com/testcases/lifecycle/readiness Checks that all pods under test have a readiness probe defined. +Result Type|normative +Suggested Remediation|Ensure that all CNF's pods under test have a readiness probe defined. +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### statefulset-scaling + +Property|Description +---|--- +Test Case Name|statefulset-scaling +Test Case Label|lifecycle-statefulset-scaling +Unique ID|http://test-network-function.com/testcases/lifecycle/statefulset-scaling +Version|v1.0.0 +Description|http://test-network-function.com/testcases/lifecycle/statefulset-scaling tests that CNF statefulsets support scale in/out operations. First, The test starts getting the current replicaCount (N) of the statefulset/s with the Pod Under Test. Then, it executes the scale-in oc command for (N-1) replicas. Lastly, it executes the scale-out oc command, restoring the original replicaCount of the statefulset/s. In case of statefulsets that are managed by HPA the test is changing the min and max value to statefulset Replica - 1 during scale-in and the original replicaCount again for both min/max during the scale-out stage. lastly its restoring the original min/max replica of the statefulset/s +Result Type|normative +Suggested Remediation|Make sure CNF statefulsets/replica sets can scale in/out successfully. +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 + +### networking + +#### icmpv4-connectivity Property|Description ---|--- +Test Case Name|icmpv4-connectivity +Test Case Label|networking-icmpv4-connectivity +Unique ID|http://test-network-function.com/testcases/networking/icmpv4-connectivity Version|v1.0.0 -Description|http://test-network-function.com/testcases/networking/icmpv4-connectivity checks that each CNF Container is able to communicate via ICMPv4 on the Default OpenShift network. This test case requires the Deployment of the [CNF Certification Test Partner](https://github.com/test-network-function/cnf-certification-test-partner/blob/main/test-partner/partner.yaml). The test ensures that all CNF containers respond to ICMPv4 requests from the Partner Pod, and vice-versa. +Description|http://test-network-function.com/testcases/networking/icmpv4-connectivity checks that each CNF Container is able to communicate via ICMPv4 on the Default OpenShift network. This test case requires the Deployment of the debug daemonset. Result Type|normative -Suggested Remediation|Ensure that the CNF is able to communicate via the Default OpenShift network. In some rare cases, CNFs may require routing table changes in order to communicate over the Default network. In other cases, if the Container base image does not provide the "ip" or "ping" binaries, this test may not be applicable. For instructions on how to exclude a particular container from ICMPv4 connectivity tests, consult: [README.md](https://github.com/test-network-function/test-network-function#issue-161-some-containers-under-test-do-nto-contain-ping-or-ip-binary-utilities). -### http://test-network-function.com/testcases/networking/service-type +Suggested Remediation|Ensure that the CNF is able to communicate via the Default OpenShift network. In some rare cases, CNFs may require routing table changes in order to communicate over the Default network. To exclude a particular pod from ICMPv4 connectivity tests, add the test-network-function.com/skip_connectivity_tests label to it. The label value is not important, only its presence. +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### icmpv4-connectivity-multus Property|Description ---|--- +Test Case Name|icmpv4-connectivity-multus +Test Case Label|networking-icmpv4-connectivity-multus +Unique ID|http://test-network-function.com/testcases/networking/icmpv4-connectivity-multus +Version|v1.0.0 +Description|http://test-network-function.com/testcases/networking/icmpv4-connectivity-multus checks that each CNF Container is able to communicate via ICMPv4 on the Multus network(s). This test case requires the Deployment of the debug daemonset. +Result Type|normative +Suggested Remediation|Ensure that the CNF is able to communicate via the Multus network(s). In some rare cases, CNFs may require routing table changes in order to communicate over the Multus network(s). To exclude a particular pod from ICMPv4 connectivity tests, add the test-network-function.com/skip_connectivity_tests label to it. The label value is not important, only its presence. +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### icmpv6-connectivity + +Property|Description +---|--- +Test Case Name|icmpv6-connectivity +Test Case Label|networking-icmpv6-connectivity +Unique ID|http://test-network-function.com/testcases/networking/icmpv6-connectivity +Version|v1.0.0 +Description|http://test-network-function.com/testcases/networking/icmpv6-connectivity checks that each CNF Container is able to communicate via ICMPv6 on the Default OpenShift network. This test case requires the Deployment of the debug daemonset. +Result Type|normative +Suggested Remediation|Ensure that the CNF is able to communicate via the Default OpenShift network. In some rare cases, CNFs may require routing table changes in order to communicate over the Default network. To exclude a particular pod from ICMPv6 connectivity tests, add the test-network-function.com/skip_connectivity_tests label to it. The label value is not important, only its presence. +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### icmpv6-connectivity-multus + +Property|Description +---|--- +Test Case Name|icmpv6-connectivity-multus +Test Case Label|networking-icmpv6-connectivity-multus +Unique ID|http://test-network-function.com/testcases/networking/icmpv6-connectivity-multus +Version|v1.0.0 +Description|http://test-network-function.com/testcases/networking/icmpv6-connectivity-multus checks that each CNF Container is able to communicate via ICMPv6 on the Multus network(s). This test case requires the Deployment of the debug daemonset. +Result Type|normative +Suggested Remediation|Ensure that the CNF is able to communicate via the Multus network(s). In some rare cases, CNFs may require routing table changes in order to communicate over the Multus network(s). To exclude a particular pod from ICMPv6 connectivity tests, add the test-network-function.com/skip_connectivity_tests label to it.The label value is not important, only its presence. +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### service-type + +Property|Description +---|--- +Test Case Name|service-type +Test Case Label|networking-service-type +Unique ID|http://test-network-function.com/testcases/networking/service-type Version|v1.0.0 Description|http://test-network-function.com/testcases/networking/service-type tests that each CNF Service does not utilize NodePort(s). Result Type|normative -Suggested Remediation|Ensure Services are not configured to not use NodePort(s). -### http://test-network-function.com/testcases/operator/install-source +Suggested Remediation|Ensure Services are not configured to use NodePort(s). +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.3.1 +#### undeclared-container-ports-usage + +Property|Description +---|--- +Test Case Name|undeclared-container-ports-usage +Test Case Label|networking-undeclared-container-ports-usage +Unique ID|http://test-network-function.com/testcases/networking/undeclared-container-ports-usage +Version|v1.0.0 +Description|http://test-network-function.com/testcases/networking/undeclared-container-ports-usage check that containers don't listen on ports that weren't declared in their specification +Result Type|normative +Suggested Remediation|ensure the CNF apps don't listen on undeclared containers' ports +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 16.3.1.1 + +### observability + +#### container-logging + +Property|Description +---|--- +Test Case Name|container-logging +Test Case Label|observability-container-logging +Unique ID|http://test-network-function.com/testcases/observability/container-logging +Version|v1.0.0 +Description|http://test-network-function.com/testcases/observability/container-logging check that all containers under test use standard input output and standard error when logging +Result Type|informative +Suggested Remediation|make sure containers are not redirecting stdout/stderr +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 11.1 +#### crd-status + +Property|Description +---|--- +Test Case Name|crd-status +Test Case Label|observability-crd-status +Unique ID|http://test-network-function.com/testcases/observability/crd-status +Version|v1.0.0 +Description|http://test-network-function.com/testcases/observability/crd-status checks that all CRDs have a status subresource specification. +Result Type|informative +Suggested Remediation|make sure that all the CRDs have a meaningful status specification. +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 + +### operator + +#### install-source Property|Description ---|--- +Test Case Name|install-source +Test Case Label|operator-install-source +Unique ID|http://test-network-function.com/testcases/operator/install-source Version|v1.0.0 Description|http://test-network-function.com/testcases/operator/install-source tests whether a CNF Operator is installed via OLM. Result Type|normative Suggested Remediation|Ensure that your Operator is installed via OLM. -### http://test-network-function.com/testcases/operator/install-status +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2.12 and Section 6.3.3 +#### install-status Property|Description ---|--- +Test Case Name|install-status +Test Case Label|operator-install-status +Unique ID|http://test-network-function.com/testcases/operator/install-status Version|v1.0.0 -Description|http://test-network-function.com/testcases/operator/install-status Ensures that CNF Operators abide by best practices. The following is tested: 1. The Operator CSV reports "Installed" status. 2. TODO: Describe operator scc check. +Description|http://test-network-function.com/testcases/operator/install-status Ensures that CNF Operators abide by best practices. The following is tested: 1. The Operator CSV reports "Installed" status. 2. The operator is not installed with privileged rights. Test passes if clusterPermissions is not present in the CSV manifest or is present with no resourceNames under its rules. Result Type|normative Suggested Remediation|Ensure that your Operator abides by the Operator Best Practices mentioned in the description. -### http://test-network-function.com/testcases/platform-alteration/base-image +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2.12 and Section 6.3.3 + +### platform-alteration + +#### base-image Property|Description ---|--- +Test Case Name|base-image +Test Case Label|platform-alteration-base-image +Unique ID|http://test-network-function.com/testcases/platform-alteration/base-image Version|v1.0.0 Description|http://test-network-function.com/testcases/platform-alteration/base-image ensures that the Container Base Image is not altered post-startup. This test is a heuristic, and ensures that there are no changes to the following directories: 1) /var/lib/rpm 2) /var/lib/dpkg 3) /bin 4) /sbin 5) /lib 6) /lib64 7) /usr/bin 8) /usr/sbin 9) /usr/lib 10) /usr/lib64 Result Type|normative Suggested Remediation|Ensure that Container applications do not modify the Container Base Image. In particular, ensure that the following directories are not modified: 1) /var/lib/rpm 2) /var/lib/dpkg 3) /bin 4) /sbin 5) /lib 6) /lib64 7) /usr/bin 8) /usr/sbin 9) /usr/lib 10) /usr/lib64 Ensure that all required binaries are built directly into the container image, and are not installed post startup. -### http://test-network-function.com/testcases/platform-alteration/boot-params +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2.2 +#### boot-params Property|Description ---|--- +Test Case Name|boot-params +Test Case Label|platform-alteration-boot-params +Unique ID|http://test-network-function.com/testcases/platform-alteration/boot-params Version|v1.0.0 Description|http://test-network-function.com/testcases/platform-alteration/boot-params tests that boot parameters are set through the MachineConfigOperator, and not set manually on the Node. Result Type|normative -Suggested Remediation|Ensure that boot parameters are set directly through the MachineConfigOperator, or indirectly through the PerfromanceAddonOperator. Boot parameters should not be changed directly through the Node, as OpenShift should manage the changes for you. -### http://test-network-function.com/testcases/platform-alteration/hugepages-config +Suggested Remediation|Ensure that boot parameters are set directly through the MachineConfigOperator, or indirectly through the PerformanceAddonOperator. Boot parameters should not be changed directly through the Node, as OpenShift should manage the changes for you. +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2.13 and 6.2.14 +#### hugepages-config Property|Description ---|--- +Test Case Name|hugepages-config +Test Case Label|platform-alteration-hugepages-config +Unique ID|http://test-network-function.com/testcases/platform-alteration/hugepages-config Version|v1.0.0 Description|http://test-network-function.com/testcases/platform-alteration/hugepages-config checks to see that HugePage settings have been configured through MachineConfig, and not manually on the underlying Node. This test case applies only to Nodes that are configured with the "worker" MachineConfigSet. First, the "worker" MachineConfig is polled, and the Hugepage settings are extracted. Next, the underlying Nodes are polled for configured HugePages through inspection of /proc/meminfo. The results are compared, and the test passes only if they are the same. Result Type|normative -Suggested Remediation|HugePage settings should be configured either directly through the MachineConfigOperator or indirectly using the PeformanceAddonOperator. This ensures that OpenShift is aware of the special MachineConfig requirements, and can provision your CNF on a Node that is part of the corresponding MachineConfigSet. Avoid making changes directly to an underlying Node, and let OpenShift handle the heavy lifting of configuring advanced settings. -### http://test-network-function.com/testcases/platform-alteration/tainted-node-kernel +Suggested Remediation|HugePage settings should be configured either directly through the MachineConfigOperator or indirectly using the PerformanceAddonOperator. This ensures that OpenShift is aware of the special MachineConfig requirements, and can provision your CNF on a Node that is part of the corresponding MachineConfigSet. Avoid making changes directly to an underlying Node, and let OpenShift handle the heavy lifting of configuring advanced settings. +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### isredhat-release Property|Description ---|--- +Test Case Name|isredhat-release +Test Case Label|platform-alteration-isredhat-release +Unique ID|http://test-network-function.com/testcases/platform-alteration/isredhat-release +Version|v1.0.0 +Description|http://test-network-function.com/testcases/platform-alteration/isredhat-release verifies if the container base image is redhat. +Result Type|normative +Suggested Remediation|build a new docker image that's based on UBI (redhat universal base image). +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### sysctl-config + +Property|Description +---|--- +Test Case Name|sysctl-config +Test Case Label|platform-alteration-sysctl-config +Unique ID|http://test-network-function.com/testcases/platform-alteration/sysctl-config +Version|v1.0.0 +Description|http://test-network-function.com/testcases/platform-alteration/sysctl-config tests that no one has changed the node's sysctl configs after the node was created, the tests works by checking if the sysctl configs are consistent with the MachineConfig CR which defines how the node should be configured +Result Type|normative +Suggested Remediation|You should recreate the node or change the sysctls, recreating is recommended because there might be other unknown changes +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2 +#### tainted-node-kernel + +Property|Description +---|--- +Test Case Name|tainted-node-kernel +Test Case Label|platform-alteration-tainted-node-kernel +Unique ID|http://test-network-function.com/testcases/platform-alteration/tainted-node-kernel Version|v1.0.0 Description|http://test-network-function.com/testcases/platform-alteration/tainted-node-kernel ensures that the Node(s) hosting CNFs do not utilize tainted kernels. This test case is especially important to support Highly Available CNFs, since when a CNF is re-instantiated on a backup Node, that Node's kernel may not have the same hacks.' Result Type|normative Suggested Remediation|Test failure indicates that the underlying Node's' kernel is tainted. Ensure that you have not altered underlying Node(s) kernels in order to run the CNF. - +Best Practice Reference|[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf) Section 6.2.14 ## Test Case Building Blocks Catalog A number of Test Case Building Blocks, or `tnf.Test`s, are included out of the box. This is a summary of the available implementations: -### http://test-network-function.com/tests/clusterrolebinding +### automountservice Property|Description ---|--- +Test Name|automountservice +Unique ID|http://test-network-function.com/tests/automountservice +Version|v1.0.0 +Description|check if automount service account token is set to false +Result Type|normative +Intrusive|false +Modifications Persist After Test|false +Runtime Binaries Required|`oc` + +### clusterVersion +Property|Description +---|--- +Test Name|clusterVersion +Unique ID|http://test-network-function.com/tests/clusterVersion +Version|v1.0.0 +Description|Extracts OCP versions from the cluster +Result Type|normative +Intrusive|false +Modifications Persist After Test|false +Runtime Binaries Required|`oc` + +### clusterrolebinding +Property|Description +---|--- +Test Name|clusterrolebinding +Unique ID|http://test-network-function.com/tests/clusterrolebinding Version|v1.0.0 Description|A generic test used to test ClusterRoleBindings of CNF pod's ServiceAccount. Result Type|normative @@ -213,9 +489,23 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc` -### http://test-network-function.com/tests/container/pod +### command +Property|Description +---|--- +Test Name|command +Unique ID|http://test-network-function.com/tests/command +Version|v1.0.0 +Description|A generic test used with any command and would match any output. The caller is responsible for interpreting the output and extracting data from it. +Result Type|normative +Intrusive|false +Modifications Persist After Test|false +Runtime Binaries Required| + +### container-pod Property|Description ---|--- +Test Name|container-pod +Unique ID|http://test-network-function.com/tests/container/pod Version|v1.0.0 Description|A container-specific test suite used to verify various aspects of the underlying container. Result Type|normative @@ -223,9 +513,35 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`jq`, `oc` -### http://test-network-function.com/tests/currentKernelCmdlineArgs +### crdStatusExistence +Property|Description +---|--- +Test Name|crdStatusExistence +Unique ID|http://test-network-function.com/tests/crdStatusExistence +Version|v1.0.0 +Description|Checks whether a give CRD has status subresource specification. +Result Type|normative +Intrusive|false +Modifications Persist After Test|false +Runtime Binaries Required|`oc`, `jq` + +### csiDriver +Property|Description +---|--- +Test Name|csiDriver +Unique ID|http://test-network-function.com/tests/csiDriver +Version|v1.0.0 +Description|extracts the csi driver info in the cluster +Result Type|normative +Intrusive|false +Modifications Persist After Test|false +Runtime Binaries Required|`oc` + +### currentKernelCmdlineArgs Property|Description ---|--- +Test Name|currentKernelCmdlineArgs +Unique ID|http://test-network-function.com/tests/currentKernelCmdlineArgs Version|v1.0.0 Description|A generic test used to get node's /proc/cmdline Result Type|normative @@ -233,19 +549,23 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`cat` -### http://test-network-function.com/tests/deployments +### daemonset Property|Description ---|--- +Test Name|daemonset +Unique ID|http://test-network-function.com/tests/daemonset Version|v1.0.0 -Description|A generic test used to read namespace's deployments +Description|check whether a given daemonset was deployed successfully Result Type|normative Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc` -### http://test-network-function.com/tests/deploymentsnodes +### deploymentsnodes Property|Description ---|--- +Test Name|deploymentsnodes +Unique ID|http://test-network-function.com/tests/deploymentsnodes Version|v1.0.0 Description|A generic test used to drain node from its deployment pods Result Type|normative @@ -253,9 +573,11 @@ Intrusive|true Modifications Persist After Test|true Runtime Binaries Required|`jq`, `echo` -### http://test-network-function.com/tests/deploymentsnodes +### deploymentsnodes Property|Description ---|--- +Test Name|deploymentsnodes +Unique ID|http://test-network-function.com/tests/deploymentsnodes Version|v1.0.0 Description|A generic test used to read node names of pods owned by deployments in namespace Result Type|normative @@ -263,9 +585,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc`, `grep` -### http://test-network-function.com/tests/generic/cnf_fs_diff +### generic-cnf_fs_diff Property|Description ---|--- +Test Name|generic-cnf_fs_diff +Unique ID|http://test-network-function.com/tests/generic/cnf_fs_diff Version|v1.0.0 Description|A test used to check if there were no installation during container runtime Result Type|normative @@ -273,19 +597,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`grep`, `cut` -### http://test-network-function.com/tests/generic/containerId -Property|Description ----|--- -Version|v1.0.0 -Description|A test used to check what is the id of the crio generated container this command is run from -Result Type|normative -Intrusive|false -Modifications Persist After Test|false -Runtime Binaries Required|`cat` - -### http://test-network-function.com/tests/generic/version +### generic-version Property|Description ---|--- +Test Name|generic-version +Unique ID|http://test-network-function.com/tests/generic/version Version|v1.0.0 Description|A generic test used to determine if a target container/machine is based on RHEL. Result Type|normative @@ -293,9 +609,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`cat` -### http://test-network-function.com/tests/gracePeriod +### gracePeriod Property|Description ---|--- +Test Name|gracePeriod +Unique ID|http://test-network-function.com/tests/gracePeriod Version|v1.0.0 Description|A generic test used to extract the CNF pod's terminationGracePeriod. Result Type|normative @@ -303,9 +621,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`grep`, `cut` -### http://test-network-function.com/tests/grubKernelCmdlineArgs +### grubKernelCmdlineArgs Property|Description ---|--- +Test Name|grubKernelCmdlineArgs +Unique ID|http://test-network-function.com/tests/grubKernelCmdlineArgs Version|v1.0.0 Description|A generic test used to get node's next boot kernel args Result Type|normative @@ -313,29 +633,23 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`ls`, `sort`, `head`, `cut`, `oc` -### http://test-network-function.com/tests/hostname +### imagepullpolicy Property|Description ---|--- +Test Name|imagepullpolicy +Unique ID|http://test-network-function.com/tests/imagepullpolicy Version|v1.0.0 -Description|A generic test used to check the hostname of a target machine/container. +Description|A generic test used to get Image Pull Policy type. Result Type|normative Intrusive|false Modifications Persist After Test|false -Runtime Binaries Required|`hostname` - -### http://test-network-function.com/tests/hugepages -Property|Description ----|--- -Version|v1.0.0 -Description|A generic test used to read cluster's hugepages configuration -Result Type|normative -Intrusive|false -Modifications Persist After Test|false -Runtime Binaries Required|`grep`, `cut`, `oc`, `grep` +Runtime Binaries Required|`oc` -### http://test-network-function.com/tests/ipaddr +### ipaddr Property|Description ---|--- +Test Name|ipaddr +Unique ID|http://test-network-function.com/tests/ipaddr Version|v1.0.0 Description|A generic test used to derive the default network interface IP address of a target container. Result Type|normative @@ -343,9 +657,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`ip` -### http://test-network-function.com/tests/logging +### logging Property|Description ---|--- +Test Name|logging +Unique ID|http://test-network-function.com/tests/logging Version|v1.0.0 Description|A test used to check logs are redirected to stderr/stdout Result Type|normative @@ -353,9 +669,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc`, `wc` -### http://test-network-function.com/tests/mckernelarguments +### mckernelarguments Property|Description ---|--- +Test Name|mckernelarguments +Unique ID|http://test-network-function.com/tests/mckernelarguments Version|v1.0.0 Description|A generic test used to get an mc's kernel arguments Result Type|normative @@ -363,9 +681,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc`, `jq`, `echo` -### http://test-network-function.com/tests/node/uncordon +### node-uncordon Property|Description ---|--- +Test Name|node-uncordon +Unique ID|http://test-network-function.com/tests/node/uncordon Version|v1.0.0 Description|A generic test used to uncordon a node Result Type|normative @@ -373,9 +693,11 @@ Intrusive|true Modifications Persist After Test|true Runtime Binaries Required|`oc` -### http://test-network-function.com/tests/nodedebug +### nodedebug Property|Description ---|--- +Test Name|nodedebug +Unique ID|http://test-network-function.com/tests/nodedebug Version|v1.0.0 Description|A generic test used to execute a command in a node Result Type|normative @@ -383,19 +705,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc`, `echo` -### http://test-network-function.com/tests/nodehugepages -Property|Description ----|--- -Version|v1.0.0 -Description|A generic test used to verify a node's hugepages configuration -Result Type|normative -Intrusive|false -Modifications Persist After Test|false -Runtime Binaries Required|`oc`, `grep` - -### http://test-network-function.com/tests/nodemcname +### nodemcname Property|Description ---|--- +Test Name|nodemcname +Unique ID|http://test-network-function.com/tests/nodemcname Version|v1.0.0 Description|A generic test used to get a node's current mc Result Type|normative @@ -403,9 +717,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc`, `grep` -### http://test-network-function.com/tests/nodenames +### nodenames Property|Description ---|--- +Test Name|nodenames +Unique ID|http://test-network-function.com/tests/nodenames Version|v1.0.0 Description|A generic test used to get node names Result Type|normative @@ -413,9 +729,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc` -### http://test-network-function.com/tests/nodeport +### nodeport Property|Description ---|--- +Test Name|nodeport +Unique ID|http://test-network-function.com/tests/nodeport Version|v1.0.0 Description|A generic test used to test services of CNF pod's namespace. Result Type|normative @@ -423,9 +741,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc`, `grep` -### http://test-network-function.com/tests/nodes +### nodes Property|Description ---|--- +Test Name|nodes +Unique ID|http://test-network-function.com/tests/nodes Version|v1.0.0 Description|Polls the state of the OpenShift cluster nodes using "oc get nodes -o json". Result Type| @@ -433,9 +753,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc` -### http://test-network-function.com/tests/nodehugepages +### nodeselector Property|Description ---|--- +Test Name|nodeselector +Unique ID|http://test-network-function.com/tests/nodeselector Version|v1.0.0 Description|A generic test used to verify a pod's nodeSelector and nodeAffinity configuration Result Type|normative @@ -443,19 +765,23 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc`, `grep` -### http://test-network-function.com/tests/nodetainted +### nodetainted Property|Description ---|--- +Test Name|nodetainted +Unique ID|http://test-network-function.com/tests/nodetainted Version|v1.0.0 Description|A generic test used to test whether node is tainted -Result Type|normative +Result Type|informative Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc`, `cat`, `echo` -### http://test-network-function.com/tests/operator +### operator Property|Description ---|--- +Test Name|operator +Unique ID|http://test-network-function.com/tests/operator Version|v1.0.0 Description|An operator-specific test used to exercise the behavior of a given operator. In the current offering, we check if the operator ClusterServiceVersion (CSV) is installed properly. A CSV is a YAML manifest created from Operator metadata that assists the Operator Lifecycle Manager (OLM) in running the Operator. Result Type|normative @@ -463,9 +789,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`jq`, `oc` -### http://test-network-function.com/tests/operator/check-subscription +### operator-check-subscription Property|Description ---|--- +Test Name|operator-check-subscription +Unique ID|http://test-network-function.com/tests/operator/check-subscription Version|v1.0.0 Description|A test used to check the subscription of a given operator Result Type|normative @@ -473,19 +801,23 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc` -### http://test-network-function.com/tests/owners +### owners Property|Description ---|--- +Test Name|owners +Unique ID|http://test-network-function.com/tests/owners Version|v1.0.0 -Description|A generic test used to verify pod is managed by a ReplicaSet +Description|A generic test used to verify pod is managed by a ReplicaSet/StatefulSet Result Type|normative Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`cat` -### http://test-network-function.com/tests/ping +### ping Property|Description ---|--- +Test Name|ping +Unique ID|http://test-network-function.com/tests/ping Version|v1.0.0 Description|A generic test used to test ICMP connectivity from a source machine/container to a target destination. Result Type|normative @@ -493,9 +825,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`ping` -### http://test-network-function.com/tests/podnodename +### podnodename Property|Description ---|--- +Test Name|podnodename +Unique ID|http://test-network-function.com/tests/podnodename Version|v1.0.0 Description|A generic test used to get a pod's node Result Type|normative @@ -503,19 +837,23 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc` -### http://test-network-function.com/tests/readRemoteFile +### podsets Property|Description ---|--- +Test Name|podsets +Unique ID|http://test-network-function.com/tests/podsets Version|v1.0.0 -Description|A generic test used to read a specified file at a specified node +Description|A generic test used to read namespace's deployments/statefulsets Result Type|normative Intrusive|false Modifications Persist After Test|false -Runtime Binaries Required|`echo` +Runtime Binaries Required|`oc` -### http://test-network-function.com/tests/rolebinding +### rolebinding Property|Description ---|--- +Test Name|rolebinding +Unique ID|http://test-network-function.com/tests/rolebinding Version|v1.0.0 Description|A generic test used to test RoleBindings of CNF pod's ServiceAccount. Result Type|normative @@ -523,19 +861,23 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`cat`, `oc` -### http://test-network-function.com/tests/serviceaccount +### scaling Property|Description ---|--- +Test Name|scaling +Unique ID|http://test-network-function.com/tests/scaling Version|v1.0.0 -Description|A generic test used to extract the CNF pod's ServiceAccount name. +Description|A test to check the deployments scale in/out. The tests issues the oc scale command on a deployment for a given number of replicas and checks whether the command output is valid. Result Type|normative -Intrusive|false +Intrusive|true Modifications Persist After Test|false -Runtime Binaries Required|`grep`, `cut` +Runtime Binaries Required|`oc` -### http://test-network-function.com/tests/shutdown +### shutdown Property|Description ---|--- +Test Name|shutdown +Unique ID|http://test-network-function.com/tests/shutdown Version|v1.0.0 Description|A test used to check pre-stop lifecycle is defined Result Type|normative @@ -543,9 +885,23 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`oc` -### http://test-network-function.com/tests/sysctlConfigFilesList +### sysctlAllConfigsArgs +Property|Description +---|--- +Test Name|sysctlAllConfigsArgs +Unique ID|http://test-network-function.com/tests/sysctlAllConfigsArgs +Version|v1.0.0 +Description|A test used to find all sysctl configuration args +Result Type|normative +Intrusive|false +Modifications Persist After Test|false +Runtime Binaries Required|`sysctl` + +### sysctlConfigFilesList Property|Description ---|--- +Test Name|sysctlConfigFilesList +Unique ID|http://test-network-function.com/tests/sysctlConfigFilesList Version|v1.0.0 Description|A generic test used to get node's list of sysctl config files Result Type|normative @@ -553,9 +909,11 @@ Intrusive|false Modifications Persist After Test|false Runtime Binaries Required|`cat` -### http://test-network-function.com/tests/testPodHighAvailability +### testPodHighAvailability Property|Description ---|--- +Test Name|testPodHighAvailability +Unique ID|http://test-network-function.com/tests/testPodHighAvailability Version|v1.0.0 Description|A generic test used to check pod's replica and podAntiAffinity configuration in high availability mode Result Type|normative diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8af1b58e5..6ef9cbde3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,16 +6,11 @@ conduct themselves in a professional and respectful manner. ## Peer review -Although this is an open source project, a review is required from one of the following committers prior to merging a -Pull Request: +Although this is an open source project, an approval is required from at least two of the +[CNF Cert team members with write privileges]https://github.com/orgs/test-network-function/teams/cnfcert/members) +prior to merging a Pull Request. -* Ryan Goulding (rgoulding@redhat.com) -* Charlie Wheeler-Robinson (cwheeler@redhat.com) -* David Spence (dspence@redhat.com) - -This list is expected to grow over time. - -*No Self Review is allowed.* Each Pull Request should be peer reviewed prior to merge. +*No Self Review is allowed.* Each Pull Request will be peer reviewed prior to merge. ## Workflow @@ -42,7 +37,7 @@ multiple small commits where possible. As always, you should ensure that tests Request. To run the unit tests issue the following command: ```bash -make unit-tests +make test ``` Changes are more likely to be accepted if they are made up of small and self-contained commits, which leads on to @@ -57,7 +52,7 @@ was made. Commit messages are again something that has been widely written abou here. Contributors should follow [these seven rules](https://chris.beams.io/posts/git-commit/#seven-rules) and keep individual -commits focussed (`git add -p` will help with this). +commits focused (`git add -p` will help with this). ## Test Implementation guidelines @@ -81,7 +76,7 @@ As always, you should ensure that tests should pass prior to submitting a Pull R following command: ```bash -make unit-tests +make test ``` ## Configuration guidelines @@ -108,12 +103,12 @@ that the accompanying documentation and guides are updated to include that infor Ensure `goimports` has been run against all Pull Requests prior to submission. -In addition, te `test-network-function` project committers expect all Pull Requests have no linting errors when the +In addition, the `test-network-function` project committers expect all Pull Requests have no linting errors when the configured linters are used. Please ensure you run `make lint` and resolve any issues in your changes before submitting your PR. Disabled linting must be justified. Finally, all contributions should follow the guidance of [Effective Go](https://golang.org/doc/effective_go.html) -unless there is a clear and considered reason not to. Contribution are more likely to be accepted quickly if any +unless there is a clear and considered reason not to. Contributions are more likely to be accepted quickly if any divergence from the guidelines is justified before someone has to ask about it. ## Mock guidelines diff --git a/DEVELOPING.md b/DEVELOPING.md index 208fa415d..049149a2d 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -250,19 +250,13 @@ Now that you have a sample JSON test defined, you can go ahead and run your JSON In order to run the test, you must first make the jsontest CLI. Issue the following command: ```shell-script -make jsontest-cli -``` - -After that completes, issue the following command: - -```shell-script -./jsontest run shell examples/ping.json +./tnf jsontest shell examples/ping.json ``` You will get something similar to the following: ```shell-script -% ./jsontest-cli run shell examples/ping.json +% ./tnf jsontest shell examples/ping.json INFO[0000] Running examples/ping.json from a local shell context 2020/12/06 13:32:53 Sent: "ping -c 5 www.redhat.com\n" 2020/12/06 13:32:57 Match for RE: "(?m)(\\d+) packets transmitted, (\\d+)( packets){0,1} received, (?:\\+(\\d+) errors)?.*$" found: ["5 packets transmitted, 5 packets received, 0.0% packet loss" "5" "5" " packets" ""] Buffer: "PING e3396.dscx.akamaiedge.net (23.34.95.235): 56 data bytes\n64 bytes from 23.34.95.235: icmp_seq=0 ttl=59 time=17.661 ms\n64 bytes from 23.34.95.235: icmp_seq=1 ttl=59 time=25.993 ms\n64 bytes from 23.34.95.235: icmp_seq=2 ttl=59 time=26.353 ms\n64 bytes from 23.34.95.235: icmp_seq=3 ttl=59 time=25.725 ms\n64 bytes from 23.34.95.235: icmp_seq=4 ttl=59 time=22.403 ms\n\n--- e3396.dscx.akamaiedge.net ping statistics ---\n5 packets transmitted, 5 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 17.661/23.627/26.353/3.302 ms\n" @@ -325,7 +319,7 @@ Note that `testResult` is 1, indicating `tnf.SUCCESS`. If you wish to explore the `oc` and `ssh` variants of `jsontest-cli`, please consult the following: ```shell-script -./jsontest-cli run -h +./jsontest -h ``` ### Including a JSON-based test in a Ginkgo Test Suite @@ -535,7 +529,7 @@ called once. The logic for determining the test result is up to the test writer. This particular implementation analyzes the match output to determine the result. -1) If the provided `destination` results in an `Indvalid Argument`, then `tnf.ERROR` is returned. +1) If the provided `destination` results in an `Invalid Argument`, then `tnf.ERROR` is returned. 2) If the ping summary regular expression matched, then: * `tnf.ERROR` if there were PING transmit errors * `tnf.SUCCESS` if a maximum of a single packet was lost @@ -587,7 +581,7 @@ of such PTY implementations can be found in [examples/pty](./examples/pty). The current tests frequently use `jq` to process structured output from `oc -o json`. `oc` also allows use of [Go Templates](https://www.openshift.com/blog/customizing-oc-output-with-go-templates) for processing structured output. -This is potentially more powerful than using `jq` as it allows building highly customised output of multiple resources +This is potentially more powerful than using `jq` as it allows building highly customized output of multiple resources simultaneously without adding dependencies. Conversely `jq` is widely available and commonly used, and has been sufficient for all cases so far. It is up to the author of a contribution to decide which approach is best suited to the task at hand. @@ -604,34 +598,70 @@ The same result could be achieved using a Go Template: oc get pod %s -n %s -o go-template='{{len .spec.containers}}{{"\n"}}' ``` +## Adding new handler + +To facilitate adding new handlers, the "tnf" utility has been created to help developers to avoid writing repetitive code. The tnf tool [source code is here](cmd/tnf) and can be built with the following command: +```shell-script +make build-tnf-tool +``` + +To generate a new handler named MyHandler, use the options "generate handler" as in the next example: +```shell-script +./tnf generate handler MyHandler +``` + +The generated code has a template and creates the necessary headers. +The result is folder "myhandler" located in /pkg/tnf/handlers/myhandler that includes 3 files by handler template. +The command relays on golang templates located in [pkg/tnf/handlers/handler_template](pkg/tnf/handlers/handler_template), so in case the "tnf" utility is executed outside the test-network-function root folder, the user can export the environment variable TNF_HANDLERS_SRC pointing to an existing "handlers" relative/absolute folder path. +```shell-script + export TNF_HANDLERS_SRC=other/path/pkg/tnf/handlers +``` + ## Adding information to claim file The result of each test execution is included in the claim file. Sometimes it is convenient to add informational messages regarding the test execution. -For this purpose we have an additional section in the claim file (see `suite.extraInfoKey` in suite_test.go). -In order to add informational messages to your test use the function `tnf.CreateTestExtraInfoWriter`. -This function adds an entry for your test in the claim file. -The return value is a function. -The returned function can be called to add a message to the test entry. +In order to add informational messages to your test use the function `ginkgo.GinkgoWriter`. +This function adds an additional message that will appear in the `CapturedTestOutput` section of the claim file, together with the output of the by directives. Each added message will be written to claim file even if test failed or error occurred in the middle of the test. Example usage: ```go -ginkgo.It("Should do what I tell it to do", func(){ - // do some work - // create test info writer - myWriter := tnf.CreateTestExtraInfoWriter() +ginkgo.It("Should do what I tell it to do", ginkgo.Label("test-label"), func(){ // do some more work // add info - myWriter("important info part 1") + _, err := ginkgo.GinkgoWriter.Write([]byte("important info part 1")) + if err != nil { + log.Errorf("Ginkgo writer could not write because: %s", err) + } + // more work // more info - myWriter("important info part 2") + _, err := ginkgo.GinkgoWriter.Write([]byte("important info part 2")) + if err != nil { + log.Errorf("Ginkgo writer could not write because: %s", err) + } // error if err != nil { return } // last info - myWriter("important info part last") + _, err := ginkgo.GinkgoWriter.Write([]byte("important info part last")) + if err != nil { + log.Errorf("Ginkgo writer could not write because: %s", err) + } }) -``` \ No newline at end of file +``` + +## Specifying the test ID as test label + +Ginkgo supports specifying a [spec label](https://onsi.github.io/ginkgo/#spec-labels), which can be used to filter tests at runtime. +All new tests should use it as part of its `ginkgo.It()` call. + +Example: +```go + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestCrdsStatusSubresourceIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + // test here + } +``` diff --git a/Dockerfile b/Dockerfile index 9674d1c52..e0a71a203 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,9 @@ FROM registry.access.redhat.com/ubi8/ubi:latest AS build +ARG TNF_PARTNER_DIR=/usr/tnf-partner -ENV GOLANGCI_VERSION=v1.32.2 -ENV OPENSHIFT_VERSION=4.6.32 +ENV TNF_PARTNER_SRC_DIR=$TNF_PARTNER_DIR/src + +ENV OPENSHIFT_VERSION=4.7.7 ENV TNF_DIR=/usr/tnf ENV TNF_SRC_DIR=${TNF_DIR}/tnf-src @@ -11,10 +13,12 @@ ENV TEMP_DIR=/tmp # Install dependencies RUN yum install -y gcc git jq make wget - +RUN wget https://get.helm.sh/helm-v3.8.2-linux-amd64.tar.gz && \ + tar -xvf helm-v3.8.2-linux-amd64.tar.gz && \ + cp linux-amd64/helm /usr/bin/helm # Install Go binary ENV GO_DL_URL="https://golang.org/dl" -ENV GO_BIN_TAR="go1.14.12.linux-amd64.tar.gz" +ENV GO_BIN_TAR="go1.17.9.linux-amd64.tar.gz" ENV GO_BIN_URL_x86_64=${GO_DL_URL}/${GO_BIN_TAR} ENV GOPATH="/root/go" RUN if [[ "$(uname -m)" -eq "x86_64" ]] ; then \ @@ -37,30 +41,43 @@ RUN wget --directory-prefix=${TEMP_DIR} ${OC_DL_URL} && \ # Add go and oc binary directory to $PATH ENV PATH=${PATH}:"/usr/local/go/bin":${GOPATH}/"bin" -# golangci-lint -RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /usr/bin ${GOLANGCI_VERSION} - # Git identifier to checkout ARG TNF_VERSION -ARG TNF_SRC_URL=https://github.com/test-network-function/test-network-function +ARG TNF_SRC_URL=$TNF_SRC_URL ARG GIT_CHECKOUT_TARGET=$TNF_VERSION +# Git identifier to checkout for partner +ARG TNF_PARTNER_VERSION +ARG TNF_PARTNER_SRC_URL=https://github.com/test-network-function/cnf-certification-test-partner +ARG GIT_PARTNER_CHECKOUT_TARGET=$TNF_PARTNER_VERSION + # Clone the TNF source repository and checkout the target branch/tag/commit RUN git clone --no-single-branch --depth=1 ${TNF_SRC_URL} ${TNF_SRC_DIR} RUN git -C ${TNF_SRC_DIR} fetch origin ${GIT_CHECKOUT_TARGET} RUN git -C ${TNF_SRC_DIR} checkout ${GIT_CHECKOUT_TARGET} +# Clone the partner source repository and checkout the target branch/tag/commit +RUN git clone --no-single-branch --depth=1 ${TNF_PARTNER_SRC_URL} ${TNF_PARTNER_SRC_DIR} +RUN git -C ${TNF_PARTNER_SRC_DIR} fetch origin ${GIT_PARTNER_CHECKOUT_TARGET} +RUN git -C ${TNF_PARTNER_SRC_DIR} checkout ${GIT_PARTNER_CHECKOUT_TARGET} + # Build TNF binary WORKDIR ${TNF_SRC_DIR} + +# golangci-lint +RUN make install-lint + # TODO: RUN make install-tools RUN make install-tools && \ make mocks && \ make update-deps && \ - make build-cnf-tests + make build-cnf-tests-debug # Extract what's needed to run at a seperate location RUN mkdir ${TNF_BIN_DIR} && \ cp run-cnf-suites.sh ${TNF_DIR} && \ + mkdir ${TNF_DIR}/script && \ + cp script/results.html ${TNF_DIR}/script && \ # copy all JSON files to allow tests to run cp --parents `find -name \*.json*` ${TNF_DIR} && \ # copy all go template files to allow tests to run @@ -72,7 +89,7 @@ WORKDIR ${TNF_DIR} RUN ln -s ${TNF_DIR}/config/testconfigure.yml ${TNF_DIR}/test-network-function/testconfigure.yml # Remove most of the build artefacts -RUN yum remove -y gcc git make wget && \ +RUN yum remove -y gcc git wget && \ yum clean all && \ rm -rf ${TNF_SRC_DIR} && \ rm -rf ${TEMP_DIR} && \ @@ -85,9 +102,12 @@ RUN yum remove -y gcc git make wget && \ # Copy the state into a new flattened image to reduce size. # TODO run as non-root FROM scratch +ARG TNF_PARTNER_DIR=/usr/tnf-partner COPY --from=build / / ENV TNF_CONFIGURATION_PATH=/usr/tnf/config/tnf_config.yml ENV KUBECONFIG=/usr/tnf/kubeconfig/config +ENV TNF_PARTNER_SRC_DIR=$TNF_PARTNER_DIR/src +ENV PATH="/usr/local/oc/bin:${PATH}" WORKDIR /usr/tnf ENV SHELL=/bin/bash -CMD ["./run-cnf-suites.sh", "-o", "claim", "diagnostic", "generic"] +CMD ["./run-cnf-suites.sh", "-o", "claim", "-f", "diagnostic"] diff --git a/LICENSE b/LICENSE index bbb0c6193..4824fa5cc 100644 --- a/LICENSE +++ b/LICENSE @@ -291,7 +291,7 @@ convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - Copyright (C) 2020-2021 Red Hat, Inc. + Copyright (C) 2020-2022 Red Hat, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/Makefile b/Makefile index 3b54ad58c..e5587ead1 100644 --- a/Makefile +++ b/Makefile @@ -14,15 +14,14 @@ # Tasks provide shortcuts to common operations that occur frequently during # development. This includes running configured linters and executing unit tests +GO_PACKAGES=$(shell go list ./... | grep -v vendor) + .PHONY: build \ mocks \ clean \ lint \ test \ - build-jsontest-cli \ - build-gradetool \ - build-catalog-json \ - build-catalog-md \ + coverage-html \ build-cnf-tests \ run-cnf-tests \ run-generic-cnf-tests \ @@ -33,23 +32,31 @@ run-container-tests \ clean-mocks \ update-deps \ - install-tools + install-tools \ + vet # Get default value of $GOBIN if not explicitly set +GO_PATH=$(shell go env GOPATH) ifeq (,$(shell go env GOBIN)) - GOBIN=$(shell go env GOPATH)/bin + GOBIN=${GO_PATH}/bin else GOBIN=$(shell go env GOBIN) endif COMMON_GO_ARGS=-race +GIT_COMMIT=$(shell git rev-list -1 HEAD) +GIT_RELEASE=$(shell git tag --points-at HEAD | head -n 1) +GIT_PREVIOUS_RELEASE=$(shell git tag --no-contains HEAD --sort=v:refname | tail -n 1) +GOLANGCI_VERSION=v1.45.2 # Run the unit tests and build all binaries build: make test make build-cnf-tests - make build-jsontest-cli - make build-gradetool + + +build-tnf-tool: + go build -o tnf -v cmd/tnf/main.go # (Re)generate mock files as needed mocks: pkg/tnf/interactive/mocks/mock_spawner.go \ @@ -62,58 +69,37 @@ clean: make clean-mocks rm -f ./test-network-function/test-network-function.test rm -f ./test-network-function/cnf-certification-tests_junit.xml + rm -f ./tnf # Run configured linters lint: - golint -set_exit_status `go list ./... | grep -v vendor` - golangci-lint run + golangci-lint run --timeout 5m0s # Build and run unit tests test: mocks go build ${COMMON_GO_ARGS} ./... - go test -coverprofile=cover.out `go list ./... | grep -v "github.com/test-network-function/test-network-function/test-network-function" | grep -v mock` - -# build the binary that can be used to run JSON-defined tests. -build-jsontest-cli: - go build -o jsontest-cli -v cmd/generic/main.go + UNIT_TEST="true" go test -coverprofile=cover.out ./... -# build the binary that can be used to run gradetool. -build-gradetool: - go build -o gradetool -v cmd/gradetool/main.go +coverage-html: test + go tool cover -html cover.out # generate the test catalog in JSON -build-catalog-json: - go run cmd/catalog/main.go generate json > catalog.json +build-catalog-json: build-tnf-tool + ./tnf generate catalog json > catalog.json # generate the test catalog in Markdown -build-catalog-md: - go run cmd/catalog/main.go generate markdown > CATALOG.md +build-catalog-md: build-tnf-tool + ./tnf generate catalog markdown > CATALOG.md # build the CNF test binary build-cnf-tests: - PATH=${PATH}:${GOBIN} ginkgo build ./test-network-function + PATH=${PATH}:${GOBIN} ginkgo build -ldflags "-X github.com/test-network-function/test-network-function/test-network-function.GitCommit=${GIT_COMMIT} -X github.com/test-network-function/test-network-function/test-network-function.GitRelease=${GIT_RELEASE} -X github.com/test-network-function/test-network-function/test-network-function.GitPreviousRelease=${GIT_PREVIOUS_RELEASE}" ./test-network-function make build-catalog-md build-cnf-tests-debug: - PATH=${PATH}:${GOBIN} ginkgo build -gcflags "all=-N -l" -ldflags "-extldflags '-z relro -z now'" ./test-network-function + PATH=${PATH}:${GOBIN} ginkgo build -gcflags "all=-N -l" -ldflags "-X github.com/test-network-function/test-network-function/test-network-function.GitCommit=${GIT_COMMIT} -X github.com/test-network-function/test-network-function/test-network-function.GitRelease=${GIT_RELEASE} -X github.com/test-network-function/test-network-function/test-network-function.GitPreviousRelease=${GIT_PREVIOUS_RELEASE} -extldflags '-z relro -z now'" ./test-network-function make build-catalog-md -# run all CNF tests -run-cnf-tests: build-cnf-tests - ./run-cnf-suites.sh diagnostic generic multus operator container - -# run only the generic CNF tests -run-generic-cnf-tests: build-cnf-tests - ./run-cnf-suites.sh diagnostic generic - -# Run operator CNF tests -run-operator-tests: build-cnf-tests - ./run-cnf-suites.sh diagnostic operator - -# Run container CNF tests -run-container-tests: build-cnf-tests - ./run-cnf-suites.sh diagnostic container - # Each mock depends on one source file pkg/tnf/interactive/mocks/mock_spawner.go: pkg/tnf/interactive/spawner.go mockgen -source=pkg/tnf/interactive/spawner.go -destination=pkg/tnf/interactive/mocks/mock_spawner.go @@ -139,7 +125,16 @@ update-deps: # Install build tools and other required software. install-tools: - go get github.com/onsi/ginkgo/ginkgo - go get github.com/onsi/gomega/... - go get golang.org/x/lint/golint - go get github.com/golang/mock/mockgen + go install github.com/onsi/ginkgo/v2/ginkgo@v2.1.3 + go install github.com/onsi/gomega + go install github.com/golang/mock/mockgen@v1.6.0 + wget https://get.helm.sh/helm-v3.8.2-linux-amd64.tar.gz && \ + tar -xvf helm-v3.8.2-linux-amd64.tar.gz && \ + cp linux-amd64/helm /usr/local/bin/helm + +# Install golangci-lint +install-lint: + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ${GO_PATH}/bin ${GOLANGCI_VERSION} + +vet: + go vet ${GO_PACKAGES} diff --git a/README.md b/README.md index 56392d659..deff8273e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ + # Test Network Function ![build](https://github.com/test-network-function/test-network-function/actions/workflows/merge.yaml/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/test-network-function/test-network-function)](https://goreportcard.com/report/github.com/test-network-function/test-network-function) +## Deprecation + +Please note that this repository has been *DEPRECATED* and further development is being done in our new repository [cnf-certification-test](https://github.com/test-network-function/cnf-certification-test). + + +## Overview + ![overview](docs/images/overview-new.svg) + This repository contains a set of Cloud-Native Network Functions (CNFs) test cases and the framework to build more. The tests and framework are intended to test the interaction of CNFs with OpenShift Container Platform. It also generates a report (claim.json) after running tests. @@ -10,109 +19,86 @@ Please consult [CATALOG.md](./CATALOG.md) for a catalog of the included test cas The suite is provided here in part so that CNF Developers can use the suite to test their CNFs readiness for certification. Please see "CNF Developers" below for more information. -## Overview - ![overview](docs/images/overview.svg) - In the diagram above: -- the `CNF under test` is the CNF to be certified. The certification suite identifies the pods belonging to the CNF via a label, see [Pod Roles](#pod_roles) -- the `Certification container/exec` is the certification test suite running on the platform or in a container. The executable verifies the CNF under test configuration and its interactions with openshift -- the `Partner pod` can be any pod with the required tools (e.g. only ping at the moment) in the same namespace as the `CNF under test`. During connectivity tests, the partner pod will generate pings towards the `CNF under test` to verify connectivity. - -These roles are configured in the [Test Configuration](#test-configuration) section below. +- the `CNF` is the CNF to be certified. The certification suite identifies the resources (containers/pods/operators etc) belonging to the CNF via labels or static data entries in the config file +- the `Certification container/exec` is the certification test suite running on the platform or in a container. The executable verifies the CNF under test configuration and its interactions with openshift +- the `Debug` pods are part of a daemonset responsible to run various privileged commands on kubernetes nodes. Debug pods are useful to run platform tests and test commands (e.g. ping) in container namespaces without changing the container image content. The debug daemonset is instantiated via the cnf-certification-test-partner repository [repo](https://github.com/test-network-function/cnf-certification-test-partner). ## Test Configuration -Detailed configuration of the individual specs is explained in [config.md](docs/config.md). +The Test Network Function support autodiscovery using labels and annotations. The following sections describe how to configure the TNF via labels/annotation and the corresponding settings in the config file. A sample config file can be found [here](test-network-function/tnf_config.yml). -By leveraging resource labels. Automatic configuration will happen by default, that being said, it can be disabled if the environment variable `TNF_DISABLE_CONFIG_AUTODISCOVER` is set +### targetNameSpaces -```shell -TNF_DISABLE_CONFIG_AUTODISCOVER=true +Multiple namespaces can be specified in the [configuration file](test-network-function/tnf_config.yml). Namespaces will be used by autodiscovery to find the Pods under test. +``` shell script +targetNameSpaces: + - name: firstnamespace + - name: secondnamespace ``` - -Pods can be labelled at creation by including the label in their definition, or at any time using the `oc label` -command. Annotations are almost identical: - -```shell -oc label pod test -n tnf test-network-function.com/generic=target -oc annotate pod test -n tnf test-network-function.com/skip_connectivity_tests=true +### targetPodLabels +The goal of this section is to specify the labels to be used to identify the CNF resources under test. It's highly recommended that the labels should be defined in pod definition rather than added after pod is created, as labels added later on will be lost in case the pod gets rescheduled. In case of pods defined as part of a deployment, it's best to use the same label as the one defined in the `spec.selector.matchLabels` section of the deployment yaml. The prefix field can be used to avoid naming collision with other labels. +```shell script +targetPodLabels: + - prefix: test-network-function.com + name: generic + value: target ``` +The corresponding label used to match pods is: +```shell script +test-network-function.com/generic: target +``` -**NOTE** Currently when this environment variable is set the `generic` and `cnfs` sections of the config file will be -ENTIRELY replaced with the autodiscovered configuration, to avoid potentially non-obvious errors. +Once the pods are found, all of their containers are also added to the target container list. A target deployments list will also be created with all the deployments which the test pods belong to. -### Pod Roles +### targetCrds +In order to autodiscover the CRDs to be tested, an array of search filters can be set under the "targetCrdFilters" label. The autodiscovery mechanism will iterate through all the filters to look for all the CRDs that match it. Currently, filters only work by name suffix. -* The test partner pod is identified by the label `test-network-function.com/generic=orchestrator`. This must identify a -single pod with a single container. This is equivalent to having an entry listed under `partnerContainers` in the config file. -* Each pod under test is identified by the label `test-network-function.com/generic=target`. There must be at least -one such pod. This is equivalent to having a pod listed under `containersUnderTest` in the config file. -* If intrusive test is NOT disabled (refer to [intrusive tests](#disable-intrusive-tests), the pod under test may get -recreated with a different name and lose the "target" label. It's **important** to populate the matching labels from the -deployment in the targetPodLabels section of the config file. -* If an FS Diff Master Pod is present it should be identified with, `test-network-function.com/generic=fs_diff_master`. This -is equivalent to listing the pod under `fsDiffMasterContainer` in the config file. -* If a pod is not suitable for network connectivity tests because it lacks binaries (e.g. `ping`), it should be -given the label `test-network-function.com/skip_connectivity_tests` to exclude it from those tests. The label value is -not important, only its presence. Equivalent to `excludeContainersFromConnectivityTests` in the config file. +```shell-script +targetCrdFilters: + - nameSuffix: "group1.tnf.com" + - nameSuffix: "anydomain.com" +``` -The autodiscovery mechanism will attempt to identify the default network device and all the IP addresses of the pods it -needs, though that information can be explicitly set using annotations if needed. The following rules apply: +The autodiscovery mechanism will create a list of all CRD names in the cluster whose names have the suffix "group1.tnf.com" or "anydomain.com", e.g. "crd1.group1.tnf.com" or "mycrd.mygroup.anydomain.com". -### Pod IPs +### testTarget +#### podsUnderTest / containersUnderTest +The autodiscovery mechanism will attempt to identify the default network device and all the IP addresses of the pods it needs for network connectivity tests, though that information can be explicitly set using annotations if needed. For Pod IPs: -* The annotation `test-network-function.com/multusips` is the highest priority, and must contain a JSON-encoded list of -IP addresses to be tested for the pod. This must be explicitly set. -* If the above is not present, the `k8s.v1.cni.cncf.io/networks-status` annotation is checked and all IPs from it are -used. This annotation is automatically managed in OpenShift but may not be present in K8s. -* If neither of the above is present, then only known IPs associated with the pod are used (the pod `.status.ips` field). +* The annotation test-network-function.com/multusips is the highest priority, and must contain a JSON-encoded list of IP addresses to be tested for the pod. This must be explicitly set. +* If the above is not present, the k8s.v1.cni.cncf.io/networks-status annotation is checked and all IPs from it are used. This annotation is automatically managed in OpenShift but may not be present in K8s. +* If neither of the above is present, then only known IPs associated with the pod are used (the pod .status.ips field). -### Network Interfaces +For Network Interfaces: -* The annotation `test-network-function.com/defaultnetworkinterface` is the highest priority, and must contain a -JSON-encoded string of the primary network interface for the pod. This must be explicitly set if needed. Examples can -be seen in [cnf-certification-test-partner](https://github.com/test-network-function/cnf-certification-test-partner/local-test-infra/local-pod-under-test/local-partner-pod.yaml) -* If the above is not present, the `k8s.v1.cni.cncf.io/networks-status` annotation is checked and the `"interface"` from -the first entry found with `"default"=true` is used. This annotation is automatically managed in OpenShift but may not -be present in K8s. +* The annotation test-network-function.com/defaultnetworkinterface is the highest priority, and must contain a JSON-encoded string of the primary network interface for the pod. This must be explicitly set if needed. Examples can be seen in cnf-certification-test-partner +* If the above is not present, the k8s.v1.cni.cncf.io/networks-status annotation is checked and the "interface" from the first entry found with "default"=true is used. This annotation is automatically managed in OpenShift but may not be present in K8s. -### Container/pod based Specs +The label test-network-function.com/skip_connectivity_tests excludes pods from connectivity tests. The label value is not important, only its presence. +The label test-network-function.com/skip_multus_connectivity_tests excludes pods from Multus connectivity tests only. The label value is not important, only its presence. Note: if both labels are present the test-network-function.com/skip_connectivity_tests takes precedence. -* Pods to be tested by the `container` spec are identified with the `test-network-function.com/container=target` -label. Any value is permitted but `target` is used here for consistency with the `generic` spec. -* If specific tests are to be run on the pod then they can be listed as a JSON-encoded list of strings under the -`test-network-function.com/container_tests` annotation. If the annotation is not found then by default every -group of CNF tests defined in `testconfigure.yml` will be run. +#### operators -For example: -```yaml -... - labels: - test-network-function.com/container: target - annotations: - test-network-function.com/container_tests: "[\"PRIVILEGED_POD\",\"PRIVILEGED_ROLE\"]" # optional -... -``` -### Operator Spec +The section can be configured as well as auto discovered. For manual configuration, see the commented part of the [sample config](test-network-function/tnf_config.yml). For autodiscovery: -* CSVs to be tested by the `operator` spec are identified with the `test-network-function-com/operator=target` +* CSVs to be tested by the `operator` spec are identified with the `test-network-function.com/operator=target` label. Any value is permitted but `target` is used here for consistency with the other specs. -* Defining which tests are to be run on the operator is done using the `test-network-function.com/operator_tests` -annotation. This is equivalent to the `test-network-function.com/container_tests` and behaves the same. * `test-network-function.com/subscription_name` is optional and should contain a JSON-encoded string that's the name of the subscription for this CSV. If unset, the CSV name will be used. -### Runtime environement variables to skip tests during development -#### Turn off openshift required tests when CNF run on kubernetes only environment -when test on CNFs that run on k8s only environment, execute shell command below before compile tool and run test shell script. +### certifiedcontainerinfo and certifiedoperatorinfo -```shell-command -export TNF_MINIKUBE_ONLY=true -``` +The `certifiedcontainerinfo` and `certifiedoperatorinfo` sections contain information about Containers and Operators that are +to be checked for certification status on Red Hat catalogs. + +### checkDiscoveredContainerCertificationStatus +This boolean flag can be turned on when you intent to have the test suite check the certification status of the container images used by the autodiscoverd test target pods in addition to the configured image list. -#### Disable intrusive tests +## Runtime environement variables +### Disable intrusive tests If you would like to skip intrusive tests which may disrupt cluster operations, issue the following: ```shell script @@ -121,58 +107,84 @@ export TNF_NON_INTRUSIVE_ONLY=true Likewise, to enable intrusive tests, set the following: -```shell-command +```shell script export TNF_NON_INTRUSIVE_ONLY=false ``` -#### Execute test suites from openshift-kni/cnf-feature-deploy -The test suites from openshift-kni/cnf-feature-deploy can be run prior to the actual CNF certification test execution and the results are incorporated in the same claim file if the following environment variable is set: +### Specifiy the location of the partner repo +This env var is optional, but highly recommended if running the test suite from a clone of this github repo. It's not needed or used if running the tnf image. +To set it, clone the partner [repo](https://github.com/test-network-function/cnf-certification-test-partner) and set TNF_PARTNER_SRC_DIR to point to it. -```shell-command -export VERIFY_CNF_FEATURES=true +```shell script +export TNF_PARTNER_SRC_DIR=/home/userid/code/cnf-certification-test-partner ``` -Currently, these suites are skipped: -* performance -* sriov -* ptp -* sctp -* xt_u32 -* dpdk -* ovn +When this variable is set, the run-cnf-suites.sh script will deploy/refresh the partner deployments/pods in the cluster before starting the test run. -For more information on the test suites, refer to [the cnf-features-deploy repository](https://github.com/openshift-kni/cnf-features-deploy/tree/release-4.6) +### Disconnected environment +In a disconnected environment, only specific versions of images are mirrored to the local repo. For those environments, +the debug pod image `quay.io/testnetworkfunction/debug-partner` should be mirrored +and `TNF_PARTNER_REPO` should be set to the local repo, e.g.: + +```shell-script +export TNF_PARTNER_REPO="registry.dfwt5g.lab:5000/testnetworkfunction" +``` + +### Execute test suites from openshift-kni/cnf-feature-deploy +The test suites from openshift-kni/cnf-feature-deploy can be run prior to the actual CNF certification test execution and the results are incorporated in the same claim file if the following environment variable is set: + + +```shell script +export TNF_RUN_CFD_TEST=true +``` + +By default, the image with release tag `4.6` is used and the ginkgo skip argument is set to `performance|sriov|ptp|sctp|xt_u32|dpdk|ovn`. To override the default behavior, set these environment variables: `TNF_CFD_IMAGE_TAG` and `TNF_CFD_SKIP`. For more information on the test suites, refer to [the cnf-features-deploy repository](https://github.com/openshift-kni/cnf-features-deploy) ## Running the tests with in a prebuild container -A ready to run container is available at this repository: [quay.io](https://quay.io/repository/testnetworkfunction/test-network-function) +### Pulling test image +An image is built and is available at this repository: [quay.io](https://quay.io/repository/testnetworkfunction/test-network-function) +The image can be pulled using : +```shell script +docker pull quay.io/testnetworkfunction/test-network-function +``` +### Cluster requirement +* OCP cluster should allow interactive shell sessions to pods/containers to stay alive when being idle for more than a few minutes. If it is not the case, consult the maintainer of the cluster infrastructure on how it can be enabled. Also, make sure the firewalls/load balancers on the path do not timeout idle connections too quickly. +* OCP cluster should provide enough resources to drain nodes and reschedule pods. If that's not the case, then ``lifecycle-pod-recreation`` test should be skipped. +### Check cluster resources +Some tests suites such as platform-alteration require node access to get node configuration like hugepage. +In order to get the required information, the test suite does not ssh into nodes, but instead rely on [oc debug tools ](https://docs.openshift.com/container-platform/3.7/cli_reference/basic_cli_operations.html#debug). This tool makes it easier to fetch information from nodes and also to debug running pods. + +In short, oc debug tool will launch a new container ending with "-debug" suffix, the container will be destroyed once the debug session is done. To be able to create the debug pod, the cluster should have enough resources, otherwise those tests would fail. -To pull the latest container and run the tests you use the following command. There are several required arguments: +**Note:** +It's recommended to clean up disk space and make sure there's enough resources to deploy another container image in every node before starting the tests. +### Run the tests +``./run-tnf-container.sh`` script is used to launch the tests. + +There are several required arguments: * `-t` gives the local directory that contains tnf config files set up for the test. -* `-o` gives the local directory that the test results will be available in once the container exits. -* Finally, list the specs to be run must be specified, space-separated. +* `-o` gives the local directory that the test results will be available in once the container exits. This directory must exist in order for the claim file to be written. Optional arguments are: - +* `-f` gives the list of suites to be run, space separated. +* `-s` gives the name of tests that should be skipped. This flag is discarded if no `-f` was set. * `-i` gives a name to a custom TNF container image. Supports local images, as well as images from external registries. -* `-k` gives a path to one or more kubeconfig files soto be used by the container to authenticate with the cluster. Paths must be separated by a colon. -* `-n` gives the network mode of the container. Defaults to `bridge`. See the [docker run --network parameter reference](https://docs.docker.com/engine/reference/run/#network-settings) for more information on how to configure network settings. +* `-k` gives a path to one or more kubeconfig files to be used by the container to authenticate with the cluster. Paths must be separated by a colon. +* `-n` gives the network mode of the container. Defaults set to `host`, which requires selinux to be disabled. Alternatively, `bridge` mode can be used with selinux if TNF_CONTAINER_CLIENT is set to `docker` or running the test as root. See the [docker run --network parameter reference](https://docs.docker.com/engine/reference/run/#network-settings) for more information on how to configure network settings. + +If `-f` is not specified, the tnf will run in 'diagnostic' mode. In this mode, no test case will run: it will only get information from the cluster (PUTs, CRDs, nodes info, etc...) to save it in the claim file. This can be used to make sure the configuration was properly set and the autodiscovery found the right pods/crds... If `-k` is not specified, autodiscovery is performed. The autodiscovery first looks for paths in the `$KUBECONFIG` environment variable on the host system, and if the variable is not set or is empty, the default configuration stored in `$HOME/.kube/config` is checked. -```shell-script -./run-tnf-container.sh -k ~/.kube/config -t ~/tnf/config -o ~/tnf/output diagnostic access-control +```shell script +./run-tnf-container.sh -k ~/.kube/config -t ~/tnf/config -o ~/tnf/output -f networking access-control -s access-control-host-resource-PRIVILEGED_POD ``` -*Note*: Tests must be specified after all other arguments! see [General tests](#general-tests) for a list of available keywords. - -*Note*: The `run-tnf-container.sh` script performs autodiscovery of selected TNF environment variables. -Currently supported environment variables include: -- `TNF_MINIKUBE_ONLY` -- `TNF_ENABLE_CONFIG_AUTODISCOVER` +See [General tests](#general-tests) for a list of available keywords. ### Running using `docker` instead of `podman` @@ -189,13 +201,13 @@ export TNF_CONTAINER_CLIENT="docker" You can build an image locally by using the command below. Use the value of `TNF_VERSION` to set a branch, a tag, or a hash of a commit that will be installed into the image. -```shell-script +```shell script docker build -t test-network-function:v1.0.5 --build-arg TNF_VERSION=v1.0.5 . ``` To build an image that installs TNF from an unofficial source (e.g. a fork of the TNF repository), use the `TNF_SRC_URL` build argument to override the URL to a source repository. -```shell-script +```shell script docker build -t test-network-function:v1.0.5 \ --build-arg TNF_VERSION=v1.0.5 \ --build-arg TNF_SRC_URL=https://github.com/test-network-function/test-network-function . @@ -203,44 +215,36 @@ docker build -t test-network-function:v1.0.5 \ To make `run-tnf-container.sh` use the newly built image, specify the custom TNF image using the `-i` parameter. -```shell-script -./run-tnf-container.sh -i test-network-function:v1.0.5 -t ~/tnf/config -o ~/tnf/output diagnostic access-control +```shell script +./run-tnf-container.sh -i test-network-function:v1.0.5 -t ~/tnf/config -o ~/tnf/output -f networking access-control ``` Note: see [General tests](#general-tests) for a list of available keywords. ## Building and running the standalone test executable -Currently, all available tests are part of the "CNF Certification Test Suite" test suite, which serves as the entrypoint -to run all test specs. `CNF Certification 3.0` is not containerized, and involves pulling, building, then running the -tests. - +Currently, all available tests are part of the "CNF Certification Test Suite" test suite, which serves as the entrypoint to run all test specs. By default, `test-network-function` emits results to `test-network-function/cnf-certification-tests_junit.xml`. - -The included default configuration is for running `generic` and `multus` suites on the trivial example at -[cnf-certification-test-partner](https://github.com/test-network-function/cnf-certification-test-partner). To configure for your -own environment, please see [config.md](docs/config.md). - ### Dependencies At a minimum, the following dependencies must be installed *prior* to running `make install-tools`. Dependency|Minimum Version ---|--- -[GoLang](https://golang.org/dl/)|1.14 -[golangci-lint](https://golangci-lint.run/usage/install/)|1.32.2 +[GoLang](https://golang.org/dl/)|1.17 +[golangci-lint](https://golangci-lint.run/usage/install/)|1.45.2 [jq](https://stedolan.github.io/jq/)|1.6 -[OpenShift Client](https://docs.openshift.com/container-platform/4.4/welcome/index.html)|4.4 +[OpenShift Client](https://mirror.openshift.com/pub/openshift-v4/clients/ocp/)|4.7 Other binary dependencies required to run tests can be installed using the following command: -```shell-script +```shell script make install-tools ``` Finally the source dependencies can be installed with -```shell-script +```shell script make update-deps ``` @@ -254,7 +258,7 @@ make update-deps In order to pull the code, issue the following command: -```shell-script +```shell script mkdir ~/workspace cd ~/workspace git clone git@github.com:test-network-function/test-network-function.git @@ -264,8 +268,7 @@ cd test-network-function ### Building the Tests In order to build the test executable, first make sure you have satisfied the [dependencies](#dependencies). - -```shell-script +```shell script make build-cnf-tests ``` @@ -278,25 +281,48 @@ script. Run any combination of the suites keywords listed at in the [General tests](#general-tests) section, e.g. -```shell-script -./run-cnf-suites.sh diagnostic -./run-cnf-suites.sh diagnostic lifecycle -./run-cnf-suites.sh diagnostic networking operator -./run-cnf-suites.sh diagnostic platform-alteration -./run-cnf-suites.sh diagnostic generic lifecycle affiliated-certification operator +```shell script +./run-cnf-suites.sh -f lifecycle +./run-cnf-suites.sh -f networking lifecycle +./run-cnf-suites.sh -f operator networking +./run-cnf-suites.sh -f networking platform-alteration +./run-cnf-suites.sh -f networking lifecycle affiliated-certification operator ``` +As with "run-tnf-container.sh", if `-f` is not specified here, the tnf will run in 'diagnostic' mode. See [Run the tests](#run-the-tests) section for more info. + By default the claim file will be output into the same location as the test executable. The `-o` argument for `run-cnf-suites.sh` can be used to provide a new location that the output files will be saved to. For more detailed control over the outputs, see the output of `test-network-function.test --help`. -```shell-script +```shell script cd test-network-function && ./test-network-function.test --help ``` -*Gotcha:* The generic test suite requires that the CNF has both `ping` and `ip` binaries installed. Please add them -manually if the CNF under test does not include these. Automated installation of missing dependencies is targeted -for a future version. +*Gotcha:* check that OCP cluster has resources to deploy [debug image](#check-cluster-resources) + +#### Running a single test or a subset + +All tests have unique labels, which can be used to filter which tests are to be run. This is useful when debugging +a single test. + +You can select the test to be executed when running `run-cnf-suites.sh` with the following command-line: + +```shell script +./run-cnf-suites.sh -f operator -l operator-install-source +``` + +Note that the `-l` parameter will be treated as a regular expression, so you can select more than one test by +their labels. + +You can find all test labels by running the following commands: + +```shell script +cd test-network-function +./test-network-function.test --ginkgo.dry-run --ginkgo.v +``` + +You can also check the [CATALOG.md](CATALOG.md) to find all test labels. ## Available Test Specs @@ -317,14 +343,13 @@ appropriate for the CNF(s) under test. Test suites group tests by topic area: Suite|Test Spec Description|Minimum OpenShift Version ---|---|--- -`access-control`|The access-control test suite is used to test service account, namespace and cluster/pod role binding for the pods under test. It also tests the pods/containers configuration.|4.4.3 -`affiliated-certification`|The affiliated-certification test suite verifies that the containers in the pod under test and operator under test are certified by Redhat|4.4.3 -`diagnostic`|The diagnostic test suite is used to gather node information from an OpenShift cluster. The diagnostic test suite should be run whenever generating a claim.json file.|4.4.3 -`lifecycle`| The lifecycle test suite verifies the pods deployment, creation, shutdown and survivability. |4.4.3 -`networking`|The networking test suite contains tests that check connectivity and networking config related best practices.|4.4.3 -`operator`|The operator test suite is designed to test basic Kubernetes Operator functionality.|4.4.3 -`platform-alteration`| verifies that key platform configuration is not modified by the CNF under test|4.4.3 - +`access-control`|The access-control test suite is used to test service account, namespace and cluster/pod role binding for the pods under test. It also tests the pods/containers configuration.|4.6.0 +`affiliated-certification`|The affiliated-certification test suite verifies that the containers and operators listed in the configuration file or used by the CNF are certified by Redhat|4.6.0 +`lifecycle`| The lifecycle test suite verifies the pods deployment, creation, shutdown and survivability. |4.6.0 +`networking`|The networking test suite contains tests that check connectivity and networking config related best practices.|4.6.0 +`operator`|The operator test suite is designed to test basic Kubernetes Operator functionality.|4.6.0 +`platform-alteration`| verifies that key platform configuration is not modified by the CNF under test|4.6.0 +`observability`| the observability test suite contains tests that check CNF logging is following best practices and that CRDs have status fields|4.6.0 Please consult [CATALOG.md](CATALOG.md) for a detailed description of tests in each suite. @@ -468,6 +493,8 @@ operator /Users/$USER/cnf-cert/test-network-function/test-network-function/operator/suite.go:152 ``` +## Log level +The optional LOG_LEVEL environment variable sets the log level. Defaults to "info" if not set. Valid values are: trace, debug, info, warn, error, fatal, panic. ## Grading Tool ### Overview @@ -494,7 +521,7 @@ please contact [Red Hat](https://redhat-connect.gitbook.io/red-hat-partner-conne Refer to the rest of the documentation in this file to see how to install and run the tests as well as how to interpret the results. -You will need an [OpenShift 4.4 installation](https://docs.openshift.com/container-platform/4.4/welcome/index.html) +You will need an [OpenShift 4.6 (or newer) installation](https://docs.openshift.com/container-platform/4.6/welcome/index.html) running your CNF, and at least one other machine available to host the test suite. The [cnf-certification-test-partner](https://github.com/test-network-function/cnf-certification-test-partner) repository has a very simple example of this you can model your setup on. @@ -521,20 +548,14 @@ output. For example: ```shell script -TNF_DEFAULT_BUFFER_SIZE=32768 ./run-cnf-suites.sh diagnostic generic +TNF_DEFAULT_BUFFER_SIZE=32768 ./run-cnf-suites.sh -f networking ``` -## Issue-161 Some containers under test do not contain `ping` or `ip` binary utilities - -In some cases, containers do not provide ping or ip binary utilities. Since these binaries are required for the -connectivity tests, we must exclude such containers from the connectivity test suite. In order to exclude these -containers, please issue add the following to `test-network-function/generic_test_configuration.yaml`: - -```yaml -excludeContainersFromConnectivityTests: - - namespace: - podName: - containerName: +# Testing certified operator +to test if operator certified need to label it with this command +```shell script +oc label csv -n "test-network-function.com/operator=target" ``` - -Note: Future work may involve installing missing binary dependencies. +# Testing certified helm charts +to test if the helm chart is certified its need to be deployed under the namespace that are under test. +if there is a need to skip a spisific helm need to add his name into the tnf_config. \ No newline at end of file diff --git a/cmd/catalog/cmd/data/INTRO.md b/cmd/catalog/cmd/data/INTRO.md deleted file mode 100644 index 7ca07b3a1..000000000 --- a/cmd/catalog/cmd/data/INTRO.md +++ /dev/null @@ -1,5 +0,0 @@ -# test-network-function test case catalog - -test-network-function contains a variety of `Test Cases`, as well as `Test Case Building Blocks`. -* Test Cases: Traditional JUnit testcases, which are specified internally using `Ginkgo.It`. Test cases often utilize several Test Case Building Blocks. -* Test Case Building Blocks: Self-contained building blocks, which perform a small task in the context of `oc`, `ssh`, `shell`, or some other `Expecter`. diff --git a/cmd/catalog/cmd/doc.go b/cmd/catalog/cmd/doc.go deleted file mode 100644 index e0dfed820..000000000 --- a/cmd/catalog/cmd/doc.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -// Package cmd provides a CLI implementation for generating the test catalog in JSON or markdown. -package cmd diff --git a/cmd/claim/main.go b/cmd/claim/main.go deleted file mode 100644 index 1e95145cb..000000000 --- a/cmd/claim/main.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package main - -import ( - "encoding/json" - "flag" - "fmt" - - "github.com/test-network-function/test-network-function-claim/pkg/claim" - "github.com/test-network-function/test-network-function/pkg/junit" - - "io/ioutil" - "log" - "os" - "path/filepath" -) - -const ( - argsLen = 2 - claimAdd = "claim-add" - claimFilePermissions = 0644 -) - -var ( - // claim-add subcommand flag pointers - // Adding a new choice for --claimfile of 'substring' and a new --reportFiles flag - claimFileTextPtr *string - reportFilesTextPtr *string -) - -func main() { - // Subcommands - claimAddCommand := flag.NewFlagSet("claim-add", flag.ExitOnError) - - // claim-add subcommand flag pointers - // Adding a new choice for --claimfile of 'substring' and a new --reportFiles flag - claimFileTextPtr = claimAddCommand.String("claimfile", "", "existing claim file. (Required)") - reportFilesTextPtr = claimAddCommand.String("reportdir", "", "dir of JUnit XML reports. (Required)") - - // Verify that a subcommand has been provided - // os.Arg[0] is the main command - // os.Arg[1] will be the subcommand - if len(os.Args) < argsLen { - log.Fatalf("claim-add subcommand is required") - } - - // Switch on the subcommand - // Parse the flags for appropriate FlagSet - // FlagSet.Parse() requires a set of arguments to parse as input - // os.Args[2:] will be all arguments starting after the subcommand at os.Args[1] - switch os.Args[1] { - case claimAdd: - if err := claimAddCommand.Parse(os.Args[2:]); err != nil { - log.Fatalf("Error reading argument %v", err) - } - default: - flag.PrintDefaults() - os.Exit(1) - } - - if claimAddCommand.Parsed() { - // Required Flags - if *claimFileTextPtr == "" { - claimAddCommand.PrintDefaults() - os.Exit(1) - } - if *reportFilesTextPtr == "" { - claimAddCommand.PrintDefaults() - os.Exit(1) - } - claimUpdate() - } -} - -func claimUpdate() { - fileUpdated := false - dat, err := ioutil.ReadFile(*claimFileTextPtr) - if err != nil { - log.Fatalf("Error reading claim file :%v", err) - } - - claimRoot := readClaim(&dat) - junitMap := claimRoot.Claim.RawResults - - items, _ := ioutil.ReadDir(*reportFilesTextPtr) - - for _, item := range items { - fileName := item.Name() - extension := filepath.Ext(fileName) - reportKeyName := fileName[0 : len(fileName)-len(extension)] - - if _, ok := junitMap[reportKeyName]; ok { - log.Printf("Skipping: %s already exists in supplied `%s` claim file", reportKeyName, *claimFileTextPtr) - } else { - junitMap[reportKeyName], err = junit.ExportJUnitAsMap(fmt.Sprintf("%s/%s", *reportFilesTextPtr, item.Name())) - if err != nil { - log.Fatalf("Error reading JUnit XML file into JSON: %v", err) - } - fileUpdated = true - } - } - claimRoot.Claim.RawResults = junitMap - payload, err := json.MarshalIndent(claimRoot, "", " ") - if err != nil { - log.Fatalf("Failed to generate the claim: %v", err) - } - err = ioutil.WriteFile(*claimFileTextPtr, payload, claimFilePermissions) - if err != nil { - log.Fatalf("Error writing claim data:\n%s", string(payload)) - } - if fileUpdated { - log.Printf("Claim file `%s` updated\n", *claimFileTextPtr) - } else { - log.Printf("No changes were applied to `%s`\n", *claimFileTextPtr) - } -} - -func readClaim(contents *[]byte) *claim.Root { - var claimRoot claim.Root - err := json.Unmarshal(*contents, &claimRoot) - if err != nil { - log.Fatalf("Error reading claim constents file into type: %v", err) - } - return &claimRoot -} diff --git a/cmd/gradetool/doc.go b/cmd/gradetool/doc.go deleted file mode 100644 index e02028a86..000000000 --- a/cmd/gradetool/doc.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -// Package main introduces an executable (gradetool) used proposing a grade for a CNF based on test results. -package main diff --git a/cmd/gradetool/main.go b/cmd/gradetool/main.go deleted file mode 100644 index 35dacff98..000000000 --- a/cmd/gradetool/main.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package main - -import ( - "flag" - "fmt" - "os" - - "github.com/test-network-function/test-network-function/pkg/gradetool" -) - -const ( - flagResultsPath = "results" - flagPolicyPath = "policy" - flagOutputPath = "o" -) - -func main() { - resultsPath := flag.String(flagResultsPath, "", "Path to the input test results file") - policyPath := flag.String(flagPolicyPath, "", "Path to the input policy file") - outputPath := flag.String(flagOutputPath, "", "Path to the output file") - flag.Parse() - if resultsPath == nil || *resultsPath == "" { - flag.Usage() - return - } - if policyPath == nil || *policyPath == "" { - flag.Usage() - return - } - if outputPath == nil || *outputPath == "" { - flag.Usage() - return - } - - err := gradetool.GenerateGrade(*resultsPath, *policyPath, *outputPath) - if err != nil { - fmt.Println(err) - os.Exit(1) - } -} diff --git a/cmd/oc/doc.go b/cmd/oc/doc.go index 9df6de497..c90a44326 100644 --- a/cmd/oc/doc.go +++ b/cmd/oc/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/cmd/oc/main.go b/cmd/oc/main.go index ae3477cd0..d1ad15b1f 100644 --- a/cmd/oc/main.go +++ b/cmd/oc/main.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -34,10 +34,16 @@ const ( // mandatoryNumArgs is the number of positional arguments required. mandatoryNumArgs = 3 + + // testTimeoutSecs timeout. + testTimeoutSecs = 2 + + // testPingCount number of ping packets to send. + testPingCount = 5 ) func parseArgs() (*interactive.Oc, <-chan error, string, time.Duration, error) { //nolint:gocritic //permit unnamed return values - timeout := flag.Int("t", 2, "Timeout in seconds") + timeout := flag.Int("t", testTimeoutSecs, "Timeout in seconds") flag.Usage = func() { fmt.Fprintf(os.Stderr, "usage: %s [-t timeout] pod container targetIpAddress ?oc-exec-opt ... oc-exec-opt?\n", os.Args[0]) flag.PrintDefaults() @@ -70,7 +76,7 @@ func main() { os.Exit(tnf.ExitCodeMap[result]) } - request := ping.NewPing(timeoutDuration, targetIPAddress, 5) + request := ping.NewPing(timeoutDuration, targetIPAddress, testPingCount) chain := []reel.Handler{request} test, err := tnf.NewTest(oc.GetExpecter(), request, chain, ch) diff --git a/cmd/ping/doc.go b/cmd/ping/doc.go index e0e01b38b..ae11fbb2b 100644 --- a/cmd/ping/doc.go +++ b/cmd/ping/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/cmd/ping/main.go b/cmd/ping/main.go index 39d2eb8b6..de448476b 100644 --- a/cmd/ping/main.go +++ b/cmd/ping/main.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -34,11 +34,17 @@ const ( // mandatoryNumArgs is the number of positional arguments required. mandatoryNumArgs = 1 + + // testTimeoutSecs timeout. + testTimeoutSecs = 2 + + // testNumRequests number of requests to send. + testNumRequests = 1 ) func parseArgs() (*ping.Ping, time.Duration) { - timeout := flag.Int("t", 2, "Timeout in seconds") - count := flag.Int("c", 1, "Number of requests to send") + timeout := flag.Int("t", testTimeoutSecs, "Timeout in seconds") + count := flag.Int("c", testNumRequests, "Number of requests to send") flag.Usage = func() { fmt.Fprintf(os.Stderr, "usage: %s [-t timeout] [-c count] host\n", os.Args[0]) flag.PrintDefaults() diff --git a/cmd/ssh/doc.go b/cmd/ssh/doc.go index 4e7423bc0..9cc439892 100644 --- a/cmd/ssh/doc.go +++ b/cmd/ssh/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/cmd/ssh/main.go b/cmd/ssh/main.go index 23f57663b..984053d09 100644 --- a/cmd/ssh/main.go +++ b/cmd/ssh/main.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -34,10 +34,16 @@ const ( // mandatoryNumArgs is the number of positional arguments required. mandatoryNumArgs = 3 + + // testTimeoutSecs timeout. + testTimeoutSecs = 2 + + // testPingCount number of ping packets to send. + testPingCount = 5 ) func parseArgs() (*interactive.Context, string, time.Duration, error) { - timeout := flag.Int("t", 2, "Timeout in seconds") + timeout := flag.Int("t", testTimeoutSecs, "Timeout in seconds") flag.Usage = func() { fmt.Fprintf(os.Stderr, "usage: %s [-t timeout] user host targetIpAddress\n", os.Args[0]) flag.PrintDefaults() @@ -52,7 +58,7 @@ func parseArgs() (*interactive.Context, string, time.Duration, error) { timeoutDuration := time.Duration(*timeout) * time.Second goExpectSpawner := interactive.NewGoExpectSpawner() var spawner interactive.Spawner = goExpectSpawner - context, err := interactive.SpawnSSH(&spawner, args[0], args[1], timeoutDuration, interactive.Verbose(true)) + context, err := interactive.SpawnSSH(&spawner, args[0], args[1], timeoutDuration, interactive.Verbose(true), interactive.SendTimeout(timeoutDuration)) return context, args[2], timeoutDuration, err } @@ -67,7 +73,7 @@ func main() { os.Exit(tnf.ExitCodeMap[result]) } - request := ping.NewPing(timeoutDuration, targetIPAddress, 5) + request := ping.NewPing(timeoutDuration, targetIPAddress, testPingCount) chain := []reel.Handler{request} test, err := tnf.NewTest(context.GetExpecter(), request, chain, context.GetErrorChannel()) diff --git a/cmd/tnf/addclaim/addclaim.go b/cmd/tnf/addclaim/addclaim.go new file mode 100644 index 000000000..e1cbbefe1 --- /dev/null +++ b/cmd/tnf/addclaim/addclaim.go @@ -0,0 +1,113 @@ +package claim + +import ( + "fmt" + "os" + "path/filepath" + + "encoding/json" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/test-network-function/test-network-function-claim/pkg/claim" + "github.com/test-network-function/test-network-function/pkg/junit" +) + +var ( + Reportdir string + Claim string + + addclaim = &cobra.Command{ + Use: "claim", + Short: "The test suite generates a \"claim\" file", + RunE: claimUpdate, + } + claimAddFile = &cobra.Command{ + Use: "add", + Short: "The test suite generates a \"claim\" file", + RunE: claimUpdate, + } +) + +const ( + claimFilePermissions = 0644 +) + +func claimUpdate(cmd *cobra.Command, args []string) error { + claimFileTextPtr := &Claim + reportFilesTextPtr := &Reportdir + fileUpdated := false + dat, err := os.ReadFile(*claimFileTextPtr) + if err != nil { + log.Fatalf("Error reading claim file :%v", err) + } + + claimRoot := readClaim(&dat) + junitMap := claimRoot.Claim.RawResults + + items, _ := os.ReadDir(*reportFilesTextPtr) + + for _, item := range items { + fileName := item.Name() + extension := filepath.Ext(fileName) + reportKeyName := fileName[0 : len(fileName)-len(extension)] + + if _, ok := junitMap[reportKeyName]; ok { + log.Printf("Skipping: %s already exists in supplied `%s` claim file", reportKeyName, *claimFileTextPtr) + } else { + junitMap[reportKeyName], err = junit.ExportJUnitAsMap(fmt.Sprintf("%s/%s", *reportFilesTextPtr, item.Name())) + if err != nil { + log.Fatalf("Error reading JUnit XML file into JSON: %v", err) + } + fileUpdated = true + } + } + claimRoot.Claim.RawResults = junitMap + payload, err := json.MarshalIndent(claimRoot, "", " ") + if err != nil { + log.Fatalf("Failed to generate the claim: %v", err) + } + err = os.WriteFile(*claimFileTextPtr, payload, claimFilePermissions) + if err != nil { + log.Fatalf("Error writing claim data:\n%s", string(payload)) + } + if fileUpdated { + log.Printf("Claim file `%s` updated\n", *claimFileTextPtr) + } else { + log.Printf("No changes were applied to `%s`\n", *claimFileTextPtr) + } + + return nil +} + +func readClaim(contents *[]byte) *claim.Root { + var claimRoot claim.Root + err := json.Unmarshal(*contents, &claimRoot) + if err != nil { + log.Fatalf("Error reading claim constents file into type: %v", err) + } + return &claimRoot +} + +func NewCommand() *cobra.Command { + claimAddFile.Flags().StringVarP( + &Reportdir, "reportdir", "r", "", + "dir of JUnit XML reports. (Required)", + ) + + err := claimAddFile.MarkFlagRequired("reportdir") + if err != nil { + return nil + } + + claimAddFile.Flags().StringVarP( + &Claim, "claim", "c", "", + "existing claim file. (Required)", + ) + err = claimAddFile.MarkFlagRequired("claim") + if err != nil { + return nil + } + addclaim.AddCommand(claimAddFile) + return addclaim +} diff --git a/cmd/tnf/generate/catalog/INTRO.md b/cmd/tnf/generate/catalog/INTRO.md new file mode 100644 index 000000000..25386e2a4 --- /dev/null +++ b/cmd/tnf/generate/catalog/INTRO.md @@ -0,0 +1,7 @@ +# test-network-function Catalog +The catalog for test-network-function contains a variety of `Test Cases`, as well as `Test Case Building Blocks`. + * Test Cases: Traditional JUnit testcases, which are specified internally using `Ginkgo.It`. Test cases often utilize several Test Case Building Blocks. + * Test Case Building Blocks: Self-contained building blocks, which perform a small task in the context of `oc`, `ssh`, `shell`, or some other `Expecter`. + +So, a Test Case could be composed by one or many Test Case Building Blocks. + diff --git a/cmd/catalog/cmd/data/TEST_CASE_BUILDING_BLOCKS_CATALOG.md b/cmd/tnf/generate/catalog/TEST_CASE_BUILDING_BLOCKS_CATALOG.md similarity index 100% rename from cmd/catalog/cmd/data/TEST_CASE_BUILDING_BLOCKS_CATALOG.md rename to cmd/tnf/generate/catalog/TEST_CASE_BUILDING_BLOCKS_CATALOG.md diff --git a/cmd/catalog/cmd/data/TEST_CASE_CATALOG.md b/cmd/tnf/generate/catalog/TEST_CASE_CATALOG.md similarity index 89% rename from cmd/catalog/cmd/data/TEST_CASE_CATALOG.md rename to cmd/tnf/generate/catalog/TEST_CASE_CATALOG.md index 9d87bd94a..3f909a3fb 100644 --- a/cmd/catalog/cmd/data/TEST_CASE_CATALOG.md +++ b/cmd/tnf/generate/catalog/TEST_CASE_CATALOG.md @@ -1,3 +1,3 @@ ## Test Case Catalog -Test Cases are the specifications used to perform a meaningful test. Test cases may run once, or several times against several targets. CNF Certification includes a number of normative and informative tests to ensure CNFs follow best practices. Here is the list of available Test Cases: +Test Cases are the specifications used to perform a meaningful test. Test cases may run once, or several times against several targets. CNF Certification includes a number of normative and informative tests to ensure CNFs follow best practices. Here is the list of available diff --git a/cmd/catalog/cmd/generate.go b/cmd/tnf/generate/catalog/catalog.go similarity index 64% rename from cmd/catalog/cmd/generate.go rename to cmd/tnf/generate/catalog/catalog.go index e3eca3f9a..fab11874f 100644 --- a/cmd/catalog/cmd/generate.go +++ b/cmd/tnf/generate/catalog/catalog.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -14,21 +14,20 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -package cmd +package catalog import ( "encoding/json" "fmt" - "io/ioutil" "os" "path" "sort" "strings" + "github.com/spf13/cobra" "github.com/test-network-function/test-network-function-claim/pkg/claim" "github.com/test-network-function/test-network-function/test-network-function/identifiers" - "github.com/spf13/cobra" "github.com/test-network-function/test-network-function/pkg/tnf/identifier" ) @@ -52,7 +51,7 @@ var ( introMDFile = path.Join(mdDirectory, introMDFilename) // mdDirectory is the path to the directory of files that contain static text for CATALOG.md. - mdDirectory = path.Join("cmd", "catalog", "cmd", "data") + mdDirectory = path.Join("cmd", "tnf", "generate", "catalog") // tccFile is the path to the file that contains the test case catalog section introductory text for CATALOG.md. tccFile = path.Join(mdDirectory, tccFilename) @@ -61,15 +60,9 @@ var ( // for CATALOG.md tccbbFile = path.Join(mdDirectory, tccbbFilename) - // rootCmd is the root of the "catalog" CLI program. - rootCmd = &cobra.Command{ - Use: "catalog", - Short: "A CLI for creating the test catalog.", - } - // generateCmd is the root of the "catalog generate" CLI program. generateCmd = &cobra.Command{ - Use: "generate", + Use: "catalog", Short: "Generates the test catalog", } @@ -88,6 +81,12 @@ var ( } ) +type catalogElement struct { + testName string + testLabel string + identifier claim.Identifier // {url and version} +} + // cmdJoin is a utility method abstracted from strings.Join which shims in better formatting for markdown files. func cmdJoin(elems []string, sep string) string { switch len(elems) { @@ -115,7 +114,7 @@ func cmdJoin(elems []string, sep string) string { // emitTextFromFile is a utility method to stream file contents to stdout. This allows more natural specification of // the non-dynamic aspects of CATALOG.md. func emitTextFromFile(filename string) error { - text, err := ioutil.ReadFile(filename) + text, err := os.ReadFile(filename) if err != nil { return err } @@ -123,6 +122,65 @@ func emitTextFromFile(filename string) error { return nil } +// createPrintableCatalogFromIdentifiers creates an structured catalogue. +// Decompose claim.Identifier urls like http://test-network-function.com/testcases/SuiteName/TestName +// to get SuiteNames and TestNames and build a "more printable" catalogue in the way of: +// { +// suiteNameA: [ +// {testName, identifier{url, version}}, +// {testName2, identifier{url, version}} +// ] +// suiteNameB: [ +// {testName3, identifier{url, version}}, +// {testName4, identifier{url, version}} +// ] +// } +func createPrintableCatalogFromIdentifiers(keys []claim.Identifier) map[string][]catalogElement { + catalog := make(map[string][]catalogElement) + // we need the list of suite's names + for _, i := range keys { + suiteTest := identifiers.GetSuiteAndTestFromIdentifier(i) + if suiteTest == nil { + fmt.Fprintf(os.Stderr, "Identifier Url not valid\n") + return nil + } + suiteName := suiteTest[0] + testName := suiteTest[1] + testLabel := suiteName + "-" + testName + catalog[suiteName] = append(catalog[suiteName], catalogElement{ + testName: testName, + testLabel: testLabel, + identifier: i, + }) + } + return catalog +} + +func getSuitesFromIdentifiers(keys []claim.Identifier) []string { + var suites []string + + for _, i := range keys { + suites = append(suites, identifiers.GetSuiteAndTestFromIdentifier(i)[0]) + } + + return Unique(suites) +} + +func Unique(slice []string) []string { + // create a map with all the values as key + uniqMap := make(map[string]struct{}) + for _, v := range slice { + uniqMap[v] = struct{}{} + } + + // turn the map keys into a slice + uniqSlice := make([]string, 0, len(uniqMap)) + for v := range uniqMap { + uniqSlice = append(uniqSlice, v) + } + return uniqSlice +} + // outputTestCases outputs the Markdown representation for test cases from the catalog to stdout. func outputTestCases() { // Building a separate data structure to store the key order for the map @@ -136,18 +194,33 @@ func outputTestCases() { return keys[i].Url < keys[j].Url }) - // Iterating the map by sorted identifier URL - for _, k := range keys { - fmt.Fprintf(os.Stdout, "### %s\n", identifiers.Catalog[k].Identifier.Url) - fmt.Println() - fmt.Println("Property|Description") - fmt.Println("---|---") - fmt.Fprintf(os.Stdout, "Version|%s\n", identifiers.Catalog[k].Identifier.Version) - fmt.Fprintf(os.Stdout, "Description|%s\n", strings.ReplaceAll(identifiers.Catalog[k].Description, "\n", " ")) - fmt.Fprintf(os.Stdout, "Result Type|%s\n", identifiers.Catalog[k].Type) - fmt.Fprintf(os.Stdout, "Suggested Remediation|%s\n", strings.ReplaceAll(identifiers.Catalog[k].Remediation, "\n", " ")) + catalog := createPrintableCatalogFromIdentifiers(keys) + if catalog == nil { + return + } + // we need the list of suite's names + suites := getSuitesFromIdentifiers(keys) + + // Sort the list of suite names + sort.Strings(suites) + + // Iterating the map by test and suite names + for _, suite := range suites { + fmt.Fprintf(os.Stdout, "\n### %s\n\n", suite) + for _, k := range catalog[suite] { + fmt.Fprintf(os.Stdout, "#### %s\n\n", k.testName) + fmt.Println("Property|Description") + fmt.Println("---|---") + fmt.Fprintf(os.Stdout, "Test Case Name|%s\n", k.testName) + fmt.Fprintf(os.Stdout, "Test Case Label|%s\n", k.testLabel) + fmt.Fprintf(os.Stdout, "Unique ID|%s\n", k.identifier.Url) + fmt.Fprintf(os.Stdout, "Version|%s\n", k.identifier.Version) + fmt.Fprintf(os.Stdout, "Description|%s\n", strings.ReplaceAll(identifiers.Catalog[k.identifier].Description, "\n", " ")) + fmt.Fprintf(os.Stdout, "Result Type|%s\n", identifiers.Catalog[k.identifier].Type) + fmt.Fprintf(os.Stdout, "Suggested Remediation|%s\n", strings.ReplaceAll(identifiers.Catalog[k.identifier].Remediation, "\n", " ")) + fmt.Fprintf(os.Stdout, "Best Practice Reference|%s\n", strings.ReplaceAll(identifiers.Catalog[k.identifier].BestPracticeReference, "\n", " ")) + } } - fmt.Println() fmt.Println() } @@ -165,10 +238,13 @@ func outputTestCaseBuildingBlocks() { // Iterating the map by sorted identifier URL for _, k := range keys { - fmt.Fprintf(os.Stdout, "### %s", identifier.Catalog[k].Identifier.URL) + testName := identifier.GetShortNameFromIdentifier(identifier.Catalog[k].Identifier) + fmt.Fprintf(os.Stdout, "### %s", testName) fmt.Println() fmt.Println("Property|Description") fmt.Println("---|---") + fmt.Fprintf(os.Stdout, "Test Name|%s\n", testName) + fmt.Fprintf(os.Stdout, "Unique ID|%s\n", identifier.Catalog[k].Identifier.URL) fmt.Fprintf(os.Stdout, "Version|%s\n", identifier.Catalog[k].Identifier.SemanticVersion) fmt.Fprintf(os.Stdout, "Description|%s\n", identifier.Catalog[k].Description) fmt.Fprintf(os.Stdout, "Result Type|%s\n", identifier.Catalog[k].Type) @@ -213,8 +289,7 @@ func runGenerateJSONCmd(_ *cobra.Command, _ []string) error { } // Execute executes the "catalog" CLI. -func Execute() error { +func NewCommand() *cobra.Command { generateCmd.AddCommand(jsonGenerateCmd, markdownGenerateCmd) - rootCmd.AddCommand(generateCmd) - return rootCmd.Execute() + return generateCmd } diff --git a/cmd/tnf/generate/handler/handler.go b/cmd/tnf/generate/handler/handler.go new file mode 100644 index 000000000..e2a8c76e8 --- /dev/null +++ b/cmd/tnf/generate/handler/handler.go @@ -0,0 +1,137 @@ +package handler + +import ( + "bufio" + "os" + "path" + "strings" + "text/template" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +type myHandler struct { + UpperHandlername string + LowerHandlername string +} + +const ( + envHandlersFolder = "TNF_HANDLERS_SRC" + docFileName = "doc.go" + handlerFolderPerms = 0755 +) + +var ( + handler = &cobra.Command{ + Use: "handler", + Short: "adding new handler.", + RunE: generateHandlerFiles, + } + defaultHandlersFolder = path.Join("pkg", "tnf", "handlers") +) + +func getHandlersDirectory() (string, error) { + handlersDirectory := os.Getenv(envHandlersFolder) + + if handlersDirectory == "" { + log.Warnf("Environment variable %s not set. Handlers base folder will be set to ./%s", + envHandlersFolder, defaultHandlersFolder) + + handlersDirectory = defaultHandlersFolder + } else { + log.Infof("Env var %s found. Handlers directory: %s", envHandlersFolder, handlersDirectory) + } + + // Convert to absolute path. + if !path.IsAbs(handlersDirectory) { + cwd, err := os.Getwd() + if err != nil { + return "", err + } + + handlersDirectory = path.Join(cwd, handlersDirectory) + } + + return handlersDirectory, nil +} + +func generateHandlerFilesFromTemplates(handlerTemplatesDirectory, newHandlerDirectory string, myhandler myHandler) error { + type fileToRender struct { + templatePath string + renderedFileName string + } + + filesToRender := []fileToRender{ + {templatePath: path.Join(handlerTemplatesDirectory, "doc.tmpl"), renderedFileName: docFileName}, + {templatePath: path.Join(handlerTemplatesDirectory, "handler_test.tmpl"), renderedFileName: myhandler.LowerHandlername + "_test.go"}, + {templatePath: path.Join(handlerTemplatesDirectory, "handler.tmpl"), renderedFileName: myhandler.LowerHandlername + ".go"}, + } + + for _, renderedFileName := range filesToRender { + if err := createfile(renderedFileName.templatePath, renderedFileName.renderedFileName, myhandler, newHandlerDirectory); err != nil { + log.Errorf("Unable to create rendered file %s on %s", renderedFileName, newHandlerDirectory) + return err + } + } + + return nil +} + +func generateHandlerFiles(cmd *cobra.Command, args []string) error { + handlername := args[0] + myhandler := myHandler{LowerHandlername: strings.ToLower(handlername), UpperHandlername: strings.Title(handlername)} + + handlersDirectory, err := getHandlersDirectory() + if err != nil { + log.Fatalf("Unable to get handlers path.") + return err + } + + handlerTemplatesDirectory := path.Join(handlersDirectory, "handler_template") + + log.Infof("Using absolute path for tnf handlers directory: %s", handlersDirectory) + newHandlerDirectory := path.Join(handlersDirectory, myhandler.LowerHandlername) + + err = os.Mkdir(newHandlerDirectory, handlerFolderPerms) + if err != nil { + log.Fatal("Unable to create handler directory " + newHandlerDirectory) + os.Exit(1) + } + + err = generateHandlerFilesFromTemplates(handlerTemplatesDirectory, newHandlerDirectory, myhandler) + if err != nil { + return err + } + + log.Infof("Handler files for %s successfully created in %s\n", myhandler.UpperHandlername, path.Join(newHandlerDirectory)) + return nil +} + +func createfile(templateFilePath, outputFileName string, myhandler myHandler, newHandlerDirectory string) error { + ftpl, err := template.ParseFiles(templateFilePath) + if err != nil { + return err + } + + temp := path.Join(newHandlerDirectory, outputFileName) + f, err := os.Create(temp) + if err != nil { + return err + } + + defer f.Close() + w := bufio.NewWriter(f) + + err = ftpl.Execute(w, myhandler) + if err != nil { + return err + } + w.Flush() + + return nil +} + +func NewCommand() *cobra.Command { + return handler +} diff --git a/cmd/tnf/grade/grade.go b/cmd/tnf/grade/grade.go new file mode 100644 index 000000000..3255011d2 --- /dev/null +++ b/cmd/tnf/grade/grade.go @@ -0,0 +1,63 @@ +package grade + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/test-network-function/test-network-function/pkg/gradetool" +) + +var ( + results string + policy string + OutputPath string + + grade = &cobra.Command{ + Use: "gradetool", + Short: "gradetool", + RunE: runGradetool, + } +) + +func runGradetool(cmd *cobra.Command, args []string) error { + resultsPath := results + policyPath := policy + outputPath := OutputPath + + err := gradetool.GenerateGrade(resultsPath, policyPath, outputPath) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + return nil +} + +func NewCommand() *cobra.Command { + grade.Flags().StringVarP( + &results, "results", "r", "", + "Path to the input test results file", + ) + + grade.Flags().StringVarP( + &policy, "policy", "p", "", + "Path to the input policy file", + ) + grade.Flags().StringVarP( + &OutputPath, "OutputPath", "o", "", + "Path to the output file", + ) + err := grade.MarkFlagRequired("results") + if err != nil { + return nil + } + err = grade.MarkFlagRequired("policy") + if err != nil { + return nil + } + err = grade.MarkFlagRequired("OutputPath") + if err != nil { + return nil + } + return grade +} diff --git a/cmd/generic/cmd/jsontest.go b/cmd/tnf/jsontest/jsontest.go similarity index 96% rename from cmd/generic/cmd/jsontest.go rename to cmd/tnf/jsontest/jsontest.go index f9728960e..99ae5b94f 100644 --- a/cmd/generic/cmd/jsontest.go +++ b/cmd/tnf/jsontest/jsontest.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -14,7 +14,7 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -package cmd +package jsontest import ( "encoding/json" @@ -88,18 +88,13 @@ const ( var ( // rootCmd is the jsontest executable root. Currently, the jsontest entrypoint has only one sub-command called // "run", which runs a generic JSON test. - rootCmd = &cobra.Command{ - Use: "jsontest-cli", + runCmd = &cobra.Command{ + Use: "jsontest", Short: "A CLI for creating, validating, and running JSON test-network-function tests.", - Long: `jsontest is a CLI library included in test-network-function used to prototype JSON test cases.`, + Long: `jsontest is a CLI library included in test-network-function used to prototype JSON test cases. The JSON test case can be run using oc, ssh, or local shell.`, } // runCmd is the json test executable option to run a JSON test. - runCmd = &cobra.Command{ - Use: "run", - Short: "run a JSON test case", - Long: `run is a CLI library included in test-network-function used to run a JSON test case. The JSON test case can be run using oc, ssh, or local shell.`, - } // shellCmd is the entrypoint for running a test case on the local shell. shellCmd = &cobra.Command{ @@ -224,7 +219,7 @@ func runSSHCmd(_ *cobra.Command, args []string) { // SSH shell creation. goExpectSpawner := interactive.NewGoExpectSpawner() var spawnContext interactive.Spawner = goExpectSpawner - context, err := interactive.SpawnSSH(&spawnContext, user, host, (*tester).Timeout(), interactive.Verbose(true)) + context, err := interactive.SpawnSSH(&spawnContext, user, host, (*tester).Timeout(), interactive.Verbose(true), interactive.SendTimeout((*tester).Timeout())) if err != nil { fatalError("could not create the ssh expecter", err, testExpecterCreationFailedExitCode) } @@ -251,7 +246,7 @@ func runOcCmd(_ *cobra.Command, args []string) { // oc shell creation. goExpectSpawner := interactive.NewGoExpectSpawner() var spawnContext interactive.Spawner = goExpectSpawner - oc, ch, err := interactive.SpawnOc(&spawnContext, pod, container, namespace, (*tester).Timeout(), interactive.Verbose(true)) + oc, ch, err := interactive.SpawnOc(&spawnContext, pod, container, namespace, (*tester).Timeout(), interactive.Verbose(true), interactive.SendTimeout((*tester).Timeout())) if err != nil { fatalError("could not create the oc expecter", err, testExpecterCreationFailedExitCode) } @@ -353,8 +348,7 @@ func runPTYTemplateCmd(_ *cobra.Command, args []string) { } // Execute executes the jsontest program, returning any applicable errors. -func Execute() error { +func NewCommand() *cobra.Command { runCmd.AddCommand(ocCmd, sshCmd, shellCmd, ptyCmd, ptyTemplateCmd) - rootCmd.AddCommand(runCmd) - return rootCmd.Execute() + return runCmd } diff --git a/cmd/tnf/main.go b/cmd/tnf/main.go new file mode 100644 index 000000000..5664b12ff --- /dev/null +++ b/cmd/tnf/main.go @@ -0,0 +1,36 @@ +package main + +import ( + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + claim "github.com/test-network-function/test-network-function/cmd/tnf/addclaim" + "github.com/test-network-function/test-network-function/cmd/tnf/generate/catalog" + "github.com/test-network-function/test-network-function/cmd/tnf/generate/handler" + "github.com/test-network-function/test-network-function/cmd/tnf/grade" + "github.com/test-network-function/test-network-function/cmd/tnf/jsontest" +) + +var ( + rootCmd = &cobra.Command{ + Use: "tnf", + Short: "A CLI for creating, validating , and test-network-function tests.", + } + + generate = &cobra.Command{ + Use: "generate", + Short: "generator tool for various tnf artifacts.", + } +) + +func main() { + rootCmd.AddCommand(claim.NewCommand()) + rootCmd.AddCommand(generate) + generate.AddCommand(catalog.NewCommand()) + generate.AddCommand(handler.NewCommand()) + rootCmd.AddCommand(jsontest.NewCommand()) + rootCmd.AddCommand(grade.NewCommand()) + if err := rootCmd.Execute(); err != nil { + log.Fatal(err) + } +} diff --git a/docs/config.md b/docs/config.md deleted file mode 100644 index 6eb4ceeef..000000000 --- a/docs/config.md +++ /dev/null @@ -1,59 +0,0 @@ -## Test Configuration - -Configuration is accomplished with `tnf_config.yml` by default. An alternative configuration can be provided using the -`TNF_CONFIGURATION_PATH` environment variable. - -This config file contains several sections, each of which configures one or more test specs: - -Config Section|Purpose ----|--- -tagetPodLabels|A list of labels that will be used to discover the pods/containers under test -generic|Describes containers to be tested with the `generic` and `multus` specs, if they are run. -cnfs|Defines which containers are to be tested by the `container` spec. -operators|Defines which containers are to be tested by the `operator` spec. -certifiedcontainerinfo|Describes cnf names and repositories to be checked for certification status. -certifiedoperatorinfo|Describes operator names and organisations to be checked for certification status. - -`testconfigure.yml` defines roles, and which tests are appropriate for which roles. It should not be necessary to modify this. - -### targetPodLabels - -This section contains a list of labels that will be used to discover the pods/containers under test **in addition to** the ones -explicitly configured in the sections below or discovered through the default labels used by auto discovery. The containers -discovered in this way will be targeted for `generic`/`multus`/`container` specs if they are in focus. - -### generic - -The `generic` section contains three subsections: - -* `containersUnderTest:` describes the CNFs that will be tested. Each container is defined by the combination of its -`namespace`, `podName`, and `containerName`, which are also used to connect to the container when required. - - * Each entry for `containersUnderTest` must also define the `defaultNetworkDevice` of that container. There is also - an optional `multusIpAddresses` that can be omitted if the multus tests are not run. - -* `partnerContainers:` describes the containers that support the testing. Multiple `partnerContainers` allows -for more complex testing scenarios. At the time of writing, only one is used, which will also be the test -orchestrator. - -* `testOrchestrator:` references a partner containers that is used for the generic test suite. The test partner is used -to send various types of traffic to each container under test. For example the orchestrator is used to ping a container -under test, and to be the ping target of a container under test. - -The [included default](../test-network-function/tnf_config.yml) defines a single container to be tested, -and a single partner to do the testing. - -### cnfs and operators - -The `cnfs` and `operators` sections define the roles under which operators and containers are to be tested. - -[The default config](../test-network-function/tnf_config.yml) is set up with some examples of this: -It will run the `"OPERATOR_STATUS"` tests (as defined in `testconfigure.yml`) against an etcd operator, and the -`"PRIVILEGED_POD"` and `"PRIVILEGED_ROLE"` tests against an nginx container. - -A more extensive example of all these sections is provided in [example/example_config.yaml](../example/example_config.yaml) - -### certifiedcontainerinfo and certifiedoperatorinfo - -The `certifiedcontainerinfo` and `certifiedoperatorinfo` sections contain information about CNFs and Operators that are -to be checked for certification status on Red Hat catalogs. diff --git a/docs/images/overview-new b/docs/images/overview-new new file mode 100644 index 000000000..39223a8a1 --- /dev/null +++ b/docs/images/overview-new @@ -0,0 +1 @@ +7V3ZcqM4FP2aPMYlNgGPaaczS/WSmkxVT+YNg2wzweBm6djz9SMZhEESBtus7nG6OpYAYZ97de/RkUTulPlm90tobdefAwd5dzJwdnfK450sK5Is35F/wNmnNbpuphWr0HXSKulY8eL+i7JKkNUmroOi0olxEHixuy1X2oHvIzsu1VlhGLyXT1sGXvmuW2uFuIoX2/L42m+uE6/TWkMDx/pfkbta0ztLIDuysejJWUW0tpzgvVClfLxT5mEQxOm7zW6OPAIexSW97qniaP7BQuTHTS5wPy8ew83ub5DY8NPz93vw8Du8l6CWtvPD8pLsK2cfN95TDMIg8R1EmgF3yof3tRujl61lk6Pv2Oq4bh1vPFyS8Nul63nzwAvCw7XKUiM/uD6Kw+ANFY7Aw4tcEfhxoT594frsg6EwRrvK7yzlSGIXRMEGxeEen5JdcC/J1FKZ/0kwK78frWmqWd26YEmZnmhlHrTKWz+CjN9kOJ+BudIy4m3gBPUyTDLgYcqhK8EEuoJJ5mD6FoRvKMR1X3CYwb943DACcRmcstv5gY8YH82qLM9d+bhoY8TwPZQPBE8Xx4GH7MDGdRxyG6E1yvZqxSBqvUFMoT26MkcDt11hHLaV3z4L0daCng7OR4XpzbIicFNZAIvWGSy8m7YbP5dL2bZF8dOBC6jBpu52wqY83IOhKekcmnOcHSzXR6HUKq6OhYylEFdoG2ix7ATXrBm2a0ucD+sC0KXOQBdRAOjFWWo+9FyKDvyeEL6CsVEAfmHPLFTBFfn9iBbJijaAP1DaRnqMVi9CWvP89ZHczdoQc/mLaJsDf6mhm0b89s27LxuqYE8RwVC6sie92agitaIOHalVacKRWgViuIdDk897U4zUVbiKI7WgZ/caqVWeg+Wgt+vMIwJdHhp0tRp05VZBF/DqfkG/jJPYNmElPCeZf3k6k5GMm4FUGXMsDISn8dch2gipGkZiQDauDC5xqAaHE6tx8Ln0djQOQ6m3SK8ah2rW+23nzNlg89/gGgdteJrM2RTDPRya/Dhkksy5AtdxahwaT+J61TikqWkcVeYdCcPQGkxb9R6ph9c4cKycbqRObTqmSH0banQVruPUODSeE09R4zgP9KE1Do0nvlPUOM4DfWiNA4podW8ax5UBrHsGUmXMkTAQ2PZ0QiOkahiJaoxO44AyhxOrcfAR5nY0DpVdsTC0xgHHsI5DZbW4wTUOKBogToU5w7Gt4xCsKpwic67CdZwaB+SZc2sax5E6VHAMzCiUqWkcVeYdC8MYgxrNRurhNQ7ap6YZqcemRuu3oUZX4TpOjUPnOfEUNY7zQB9a49BPLJ6ZkMZxHuhDaxz6ZfMuDTSOhoxk5Axk3LMsetubgxohVbelBzQYUvcscuj8vMtnK4p/ls0qEpDrTdKrymE04M79ey6fEgf3XJNn06zn3vISJCmPsWPxXAnww/hHtLQSnOPGSFLO3YCpCiiJcANmd/tdJH7Ms8HwJpHQ2a8aSxo2Eo8lF4ZGpOGuQNaGB5kf4+QgtzvGQZKjIV0Esgl1xWo8YD8bZH14kDte79FTkNBZkckU5EUBsGp3wLa9gnkYYCEYHbAikXp6wKrK6IBtm/EOk8o04zJgO3taQy7HThxYVRodsB1PufYUCkyqkowmFMgdP9Olr6GDpI8tyNKoXwASOSv0khWDMF4Hq8C3vI/HWmbUejznUxBsM4D/QXG8zx5+ZCVxUIYfAxbu/yLXzzRafM2aOxQed6XSPi85D+QRSLi48AL7La16cr3L7ZoeoU9CUg41VhiztzlUFm6UokagOu0MMsAXrtApyTSjZrzbhMizYvdH+Q4iF8gufQ7cgwCdk1Ctoh/TNqIgCW2UXXb0JPzdrX3htC05IbrgRk9NL1AV7fQFmnnyAvwm/dDHnpDjeE3nUAfpHDs3LvQNXHotHDn2DFKgHaPks5kyVXTZRp2H7wqXhskm/SJ1vhPw08FpbQdK9whc0X+uDKF8cvrNd2PXionWuHX91SHAY1Bl4EepTMhN+QQ+PupQVQz4iNfGbkej5DauCtKgIUiDnU0LSQocJg2OJZ0VErJsysWULM1AXq7Myrj0jEIXG4O4V8FPWkmQKWlqPz+ySUUxzJnWKEPybVUOn2lL6be8PtfWZELBJ5MHSZ36/6nzojFxfY/poCMYYGYo5ZBsNOOKjRpT2PFLRWdoz/t4Ta4+JYNDDk4lfenmczD3fEyTX+wrmibsMAebU8jBDTry5HJwBxFF12ZAPZ0Rm+dWgBsrviDTsMSm7daSLaOEqdmu+1Mf9dQFHSVbqoZ1JY/1NB+pXThpxqaq9kIS3RI3DIuh7w/K2EynlEZMYwrRA+hniWodECBS41jR+oCCdG14aiIBdDREkCSmQxtMG42DGP8QHNgvJxJsQ/+TLD4FUeISWsTudGEVCrRDdpLuvMBUifAlmy42bjXY4FBjOKoo2BjyQoHtLX5gxkVNN+x1N30k2LPOh59jR7Q9K4pcW0RHi9zjkjFR855Zq+Llflfbh82KPlwwiCawB627Wi1n181dzFcgK2/1PfwR7MP/I/H9dNgTo4hwHpBEafktWSA7Jo0GS/KffejZm43lOxHnfrcz/uHm4iTB2vR8eqyfyTjBznJst6hgNMcNibH2dLTqpwtat9j1l0G44aP2D9cil6U7H8E2IDblIv3pPY43ZHPAiGeC9W7C/QjdrY2F/I6OwRim3JBh5oTu9ZhWBuaXgyQstaPBM45E+oyhnZfmIkOVmaa4nZdtDZZVszzgV+uEaeZ8BfYhTAsei8DE2EwXDNHWw7GKkl3ssTFaJuQGESLnzfN4Oi9y4aw8m81+qsCqM/KxzgVWRUSmOwysooXEpX16R0vQTXnkwH10CJoP+ARZ3e6KO/bEO/sWhx9+Z9/XLfKjtbuMK59hdLPuYDBUWvQnqkTzux0yK2OQNJtrMudlzFJq6yh9Ns+Y9fOzajd5UDUZPRA2m52tzxW4ePwzdenpxz/2p3z8Dw== \ No newline at end of file diff --git a/docs/images/overview-new.drawio b/docs/images/overview-new.drawio new file mode 100644 index 000000000..80e1c3715 --- /dev/null +++ b/docs/images/overview-new.drawio @@ -0,0 +1 @@ +7V1Zc6M4EP41eYyLU8BjxpnsUXOkNls1m33DINtsMHg4Jvb++pUMwiAJAzZY4FlnamIJJOyvW91ft47cqfPN7pfI3q4/hy707xTJ3d2pj3eKosqKcof/Se4+qzEMK6tYRZ6bVcnHihfvX5hXSnlt6rkwrtyYhKGfeNtqpRMGAXSSSp0dReF79bZl6FefurVXkKl4cWyfrf3muck6qzV16Vj/K/RWa/JkWcqvbGxyc14Rr203fC9VqR/v1HkUhkn2brObQx+DR3DJ2j3VXC0+WASDpE0D7/PiMdrs/pZSB3x6/n4vPfwO7mWgZ/38sP00/8r5x032BIMoTAMX4m6kO/XD+9pL4MvWdvDVdyR1VLdONj4qyejt0vP9eeiH0aGtutTxD6qPkyh8g6Ur4PDCLcIgKdVnL1SffzAYJXBX+53lAkmkgjDcwCTao1vyBveyQiSV658M8vL7UZqWltetS5JUyI12rkGrovcjyOhNjnMHzNWeEe8DJ2BUYVIkFqYCugpM0lAwKQxM38LoDUao7gsyM+gXixtCIKmCU1W7IAwgpaN5le17qwAVHYQYeob6AePpITvwkF/YeK6LH8OVRlVevQhEaxaIxZXHUOJoobYrhMO29tvnJtpekNul7qhQo1lROWqqcGDRB4OFVdN+7edyqTgOz366YAF00FbdTsiUhVsYmrLBoDlH3sH2AhjJveLq2tBccnEFjgkXy0FwzbuhhrbM6rDBAV0eDHSzHvR+lXlEoCuCQSdfiwe6equgc6z1dTWdR3aBn+Qk9OCjCDrge4qZOcJGldAL2eBSFVjh349wka5IB+gDZX1k10j1IiI1z18f8dPsDRZXsIi3BfDnCrott+lfvPWWi0el1aHkSR42Kk6iaqI5icbalulwEk3iwy0OTZbhTZGT1OE6Tk6isdHGFDlJN9BFcxJNuwlO0g100ZxEO4+TOA5mJSwnmX956shIxs1A6oQ5FgbCBqyXIdoKqQZGYgLarghP5mlsjEln81hfejvZPFNtlshVs3ma1ay3gzNnk/Z/wrN5pONpMmeLD7c4NE/kOKbEnGtwHSdz1k+EK1Nizp1AF82c9RPhypSYcyfQRTNnnQ1XrprNk6eWzasT70i4tN5iKcLVOYn4bB5iBdPlJJlMx8RJbmOGsQ7XkXKS25hh7Aa6cE7ChnhT5CTdQBfNSUhWRUw270IDNjwDqRPmSBgI6HvirBVSDYxEM0eXzQNsjEln81gLczvZPI1ehSY6mwfGsDZPo7POwrN5gBcgToU5g7GtzeOsFJ8ic67DdZzMGbDB3xSZczfQRTNncCJGnBBz7ga6cObMxoiVjFyF6KpTS73VyWIsxHcM04E0gRCfeiMDYJoEYmzTgcZtTAfW4TpOAmGwodokCcSkpgON25gO7Aa6aAJhnDcd2CL1dqQeNcm4jJGMnIGMe/LP6HsfciukmnYPSy0yPVfOvRlsRPjZjpOfZV+sLCnNIrlq8s1swZ2vr7msSxSuuRbLpmnNveU1oHJhY8eiubLEi7mXdop83BhJStezHjQOJeGe9TDchkOZjXk2CN405ir7RbGk6UB+LLkwdTxjMRTIuniQ2RinALnfGAfKrg4NHsgWMFS7dcDeGWRDPMgDL0O6kpEw6CSTxfGLHGC14YDtewuJGGCBNDpgWe82RWA1dXTA9s14xbgy3TwP2MEOhirSsRMHVpNHB+zAKwGuZAoskiUZjSlQBj4+7lqhg2yMzcgSq18CEror+JIXwyhZh6swsP2Px1oqaj3e8ykMtznA/8Ak2efnLNppElbhR4BF+79w+5lOiq95d4fC465S2hcl9wGftoiKCz903rKqJ88/X67ZFXLoonqosaOEfsyhsvSgDDUM1WllUCTUcAVPpUxzasaqTQR9O/F+VJ/AU4G86XPoHRLQBQnVa8Yx6SMO08iBebOjJqHvbu9Lt23xDfEZD3pq20BT9dMNdOtkA/Qm+9DHkVDgeMng0IQMjp2XlMYGKr2WrhxHBi6QgVHR2TwzVVbZVoOHHQrnmsk24yJTvhPwk+C0cQBlW1cuGD8XmlDWOf0WeIlnJzjXuPWC1cHAI1AVKYizNCEz5RMG6KpLsmJSANnc2O3kKJmTAzhu0OS4wcGmhWQViHGDY3FnJYesWErZJcszqSjXemVUeoaRh4SB1aukJ704yIw09e8faaeimtZMb+Uh2b5qw2fSU/YtL/e1DZ6Q88kUIa7T+N91nhUTN4+YAQaCKc1MtWqSzXZcsVVnKh2/1AyG/rSPzck1u2Tp4IOzlL588z6YOYrbkhkfzJsmHNAHW1PwwS0G8uR88AAWxdBnknbaI7b3rRLqrPwCVMcy7bZ7c7ZUJkzLD4M49VFPNRjI2ZJs2FDpsSvNR+pnTprRrqo/k0R2aophMeT9ITM2Mwil4dOYkvWQjE5JtQEIEK5x7Xh9QEG+1Dy1SQEMFCLIMjWgTaqP1kaMPYUMXJcTcU5H+BMvPpXi1MO0iN7pQmco4A46abbzAlElzJccsti4V2ODTI3pajxjYyoLFfS3+IGKi9ruIx1u+ohzlAJrfo4D0fHtOPYcHh0tc49zYqL2I7Mxi1foXeMYtmrGcEkgOkcepO7ibDm9bu5svgLo9Na1wx/O8RB/pEGQhT0JjDHnkdI4K7+lC+gkuNNwif9zDiN7s7EDN2bU73biH2YuTuasTS+mx64zGcc58ADJLS4JzfUiLKw9iVaDbEHrFqn+Mow2rNX+4dm4WbbFUdqGWKaMpT+9x/GGZC5RyTPOejfufoTh1sYCdkeHMIaptGSYBaF7PboVwfxSiMPSBgqekSUyZhTtPNcXmZpCdcXsvOwrWNasasCvNSWmqftVcI3ENOe0DsrG5nnBCG59ZKsI2UUam8Blih8QQ3zfvLCn8zIXzsuz2eynMqwGlT42GMOq8sj0gIaVt5C4sk/vKAmyKQ9fuI8PRvMB3aBo2115xx5/Z9/i8MPu7Pu6hUG89pZJ7dFaN6sOJkWleX8Nkze/OyCzMoW42SIn081jVlzbQO6zvcdsnp/VhvGDmkXlA0G72dlmX4GKx7+Im91+/LvC6sf/AA== \ No newline at end of file diff --git a/docs/images/overview-new.svg b/docs/images/overview-new.svg new file mode 100644 index 000000000..4f69ecdb1 --- /dev/null +++ b/docs/images/overview-new.svg @@ -0,0 +1,3 @@ + + +
Worker Node 2
Worker Node 2
Container1
Container1
Debug
POD2 
Debug...
Container1
Container1
Container2
Container2
Container3
Container3
CNF
POD2
CNF...
Worker Node 1
Worker Node 1
Container1
Container1
Debug
POD1 
Debug...
Container1
Container1
Container2
Container2
Container3
Container3
CNF
POD1
CNF...
Worker Node 3
Worker Node 3
Container1
Container1
Debug
POD3 
Debug...
Container1
Container1
Container2
Container2
Container3
Container3
CNF
POD3
CNF...
Master Node 2
Master Node 2
Master Node 1
Master Node 1
Default
Default
multus 1
multus 1
multus 2
multus 2
Initiate ping with nsenter
on default net
Initiate ping...
Initiate ping with nsenter on multus1 net
Initiate ping w...
TNF suite 
executable or container
TNF suite...
Running tests using kubectl of oc commands
Running tests using k...
Runs tests directly on node platform
via debug pods  
Runs tests directly on node platform...
Runs tests with replica or stateful sets, pods, containers, ... 
Runs tests with replica or stateful sets, pods, conta...
Openshift
Openshift
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/images/overview.drawio b/docs/images/overview.drawio new file mode 100644 index 000000000..0a651fd12 --- /dev/null +++ b/docs/images/overview.drawio @@ -0,0 +1 @@ +3VrZcqM4FP0aP8YldnhMnGUeekmVa6qnn7pkEIYZjGghx/Z8/UggDEh4DZCMSaqCroRA5567SZkYs9X2hcAs+ooDlEx0EGwnxuNE1zVT1yf8FwS7UuI4XilYkjgQg2rBPP4XCSEQ0nUcoLw1kGKc0DhrC32cpsinLRkkBG/aw0KctN+awSVSBHMfJqr0RxzQqJS6Fqjlf6B4GVVv1oDoWcFqsBDkEQzwpiEynibGjGBMy7vVdoYSDl6FS/nc84He/YcRlNJzHnhJol+zr/DHvfc0/5NqL9rvx7s73S6neYPJWqxYfC3dVRAQvE4DxGcBE+NhE8UUzTPo894NUzqTRXSVsJbGbsM4SWY4waR41gggckOfyXNK8D+o0WP7LlqErEddh1jaGyIUbRsisa4XhFeIkh0bUvU6AmNBsj3mm1plVqWyqKEuvXoQCpos93PXSLIbAeYFwA6Ma2jxn05ci4s/gVPakJdXT3i7Et6GirfmdeENhsJbV/DOcMAEWr+wh6Hud9I5sBe2ZfcDry7Da6vwOpaKrjkUuI4C7uzbMxNwIAn3yCinCs5ssbQNZhu0FKdIQliIYBIvU9b0GYRsfuOBQxczn3wvOlZxEPDXdGqvrd+x2A462T6QPtzTzgWlwT0PfzWqAcyjAhatrRUuf4WUIZ0WEh0Ye11VIY+h88AAJLu/OKxTq2r+FHMUjcetwLxs7U5pIMdr4qMjyzRFvIdkiehp20dBK5Sr+mxGgw51VTKCEkjjt3YC0KVD8YZXHLOV7eligjZdTJkG5brFU82ALU9ktScy5GBVAqNMVFBqv+zrWaZ5KqsYxHPRxIRGeIlTmDzVUsn66jFfMM4EWf5GlO5EpgfXFLfpiLYxLVmmW6L5s9FVk4w3dk3GjcrNKj89SU7zTHJexjpm23DXGJBxEuRHSClxqSLpudwz7ePjDc09Np7dlF/cK0ENNbg3CSr8Xm+M1Bp0FDw7SEglPwDAcgBQMwoAXAjaFG4SuMHnj6Ow7vXN4XdFv+q7G+HP5wlCyFIEGuO0rAYpjFOenDyjLfL7TQJdH3UngQvX4obTS9ZhapL376hpulJse7AMGygojhEOas+uX+Ta6zjinB9HxjCmQeKB4pBtOQvRrsxCDN2ZWsenOpCHXByjXOk98obGqfHu8fG65x0bP0yM2ht+7azImruoojx9bjgqsIA5CkQ1lSu2djPllCEzs6O6PbB5MJRnU+vbiW4nVGyitPRg/17jquMuL7zWPd9mcLNt3cnulvzv9wyleRSHtJqOfV45Y9l/u0q2vLaS9xttTSUboyrZU5TcUA9YwZwWVsgtkuAk4Y2b2bCz7Lbr29tgUx16hzoG21Iy1CRuhgn6PmeyVxb5QkxWzEzgisOaLvKsy1xuSCHWhyukYw9pgGLqcGF0bQF0sKC6omwbIf+riD90Amg6EsOuTQAteSLZRQ+8DcVMTvYU2d5B3HqyZGlSztrhuM0xw6j5efdcGqUisFul4hSY+gkvUrReEYkZTFzvh3Zx+PW+Te0PKxTfp3c1XhOUJcwycsSdxTvP2dTzNMdbdO2XhSGyi82XHoxLcx0pK7IU43JGNS7V0xWHmNt+M58POcQ0PvoQ01TjSIvB6S0wuAvkURmsUliNF5//aPKTHCXqFXqyei/N4di7p7YtpXHWqGmcaf8PEofLKo9B0gPjzPRgmHNFlTm2c5w2Z1PQM6cWqC+J2RojqOPV18jkdD4tOYck2ejkca/1X3IyYY1bg5rup+XHiM5rcLpYEl20a32NTJfeYh1r1v+xWw6v/+/ZePoP \ No newline at end of file diff --git a/examples/example_config.yaml b/examples/example_config.yaml deleted file mode 100644 index dc10a21af..000000000 --- a/examples/example_config.yaml +++ /dev/null @@ -1,77 +0,0 @@ -targetPodLabels: - - namespace: example-cnf.com - name: example-cnf-deployment - value: example-app -generic: - containersUnderTest: - - namespace: tnf - podName: test - containerName: test - defaultNetworkDevice: eth0 - multusIpAddresses: - - 10.217.0.8 - partnerContainers: - - namespace: tnf - podName: partner - containerName: partner - defaultNetworkDevice: eth0 - multusIpAddresses: - - 10.217.0.29 - - namespace: tnf - podName: node-master - containerName: master - defaultNetworkDevice: eth0 - fsDiffMasterContainer: - namespace: tnf - podName: node-master - containerName: master - testOrchestrator: - namespace: tnf - podName: partner - containerName: partner -operators: - - name: etcdoperator.v0.9.4 - namespace: my-etcd - subscriptionName: etcd - status: Succeeded - autogenerate: "true" - crds: - - name: test.crd.one - namespace: default - instances: - - name: Instance_one - - name: test.crd.two - namespace: default - instances: - - name: Instance_two - deployments: - - name: deployment1 - replicas: "1" - permissions: - - name: name - role: clusterrole - cnfs: - - name: cnf_one - namespace: test - status: "" - tests: - - PRIVILEGED_POD - tests: - - CSV_INSTALLED - - SUBSCRIPTION_INSTALLED - - CSV_SCC -cnfs: - - name: cnf_only - namespace: test - status: "" - tests: - - PRIVILEGED_POD -certifiedcontainerinfo: - - name: nginx-116 - repository: rhel8 -certifiedoperatorinfo: - - name: etcd-operator - organization: redhat-marketplace -cnfavailabletestcases: - - PRIVILEGED_POD - - CLUSTER_ROLE diff --git a/examples/generic/template/ping.values.yaml b/examples/generic/template/ping.values.yaml index e68066ff0..40e52852c 100644 --- a/examples/generic/template/ping.values.yaml +++ b/examples/generic/template/ping.values.yaml @@ -1 +1 @@ -HOST: 192.168.1.1 \ No newline at end of file +HOST: 192.168.1.1 diff --git a/examples/pty/ssh.json.tpl.values.yaml b/examples/pty/ssh.json.tpl.values.yaml index 7d746ea1a..4540132a7 100644 --- a/examples/pty/ssh.json.tpl.values.yaml +++ b/examples/pty/ssh.json.tpl.values.yaml @@ -1,4 +1,4 @@ SSH_ARGS: - 192.168.1.5 - -l - - pi \ No newline at end of file + - pi diff --git a/fs-diff-test-utils/Dockerfile.podman-image b/fs-diff-test-utils/Dockerfile.podman-image deleted file mode 100644 index 2f2e94e83..000000000 --- a/fs-diff-test-utils/Dockerfile.podman-image +++ /dev/null @@ -1,3 +0,0 @@ -FROM centos:8.3.2011 -RUN yum -y install podman -COPY diff-fs.sh /diff-fs.sh \ No newline at end of file diff --git a/fs-diff-test-utils/diff-fs.sh b/fs-diff-test-utils/diff-fs.sh deleted file mode 100755 index 705e859a5..000000000 --- a/fs-diff-test-utils/diff-fs.sh +++ /dev/null @@ -1,7 +0,0 @@ -result=`podman diff $1 | cut -d " " -f 2 | grep -E "(^/var/lib/rpm)|(^/var/lib/dpkg)|(^/bin)|(^/sbin)|(^/lib)|(^/lib64)|(^/usr/bin)|(^/usr/sbin)|(^/usr/lib)|(^/usr/lib64)"` -if [ -z "${result}" ] -then -echo empty -else -echo $result -fi \ No newline at end of file diff --git a/fs-diff-test-utils/privilaged-pod.yaml b/fs-diff-test-utils/privilaged-pod.yaml deleted file mode 100644 index b0cb7e11e..000000000 --- a/fs-diff-test-utils/privilaged-pod.yaml +++ /dev/null @@ -1,49 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - labels: - app: node-master - name: node-master - namespace: tnf -spec: - # hostPID: true - containers: - - command: - - tail - - -f - - /dev/null - image: quay.io/isaacdorfman/centos-podman - name: master - resources: - limits: - memory: 512Mi - cpu: 0.25 - volumeMounts: - - mountPath: /var/lib/containers - name: var-lib-containers - - mountPath: /var/run/containers - name: var-run-containers - - mountPath: /run/runc - name: runc-dir - - mountPath: /var/run/containers/storage/overlay-containers - name: overlay-containers - securityContext: - privileged: true - restartPolicy: Always - volumes: - - name: var-lib-containers - hostPath: - path: /var/lib/containers - type: Directory - - name: var-run-containers - hostPath: - path: /var/run/containers - type: Directory - - name: runc-dir - hostPath: - path: /run/runc - type: Directory - - name: overlay-containers - hostPath: - path: /var/run/containers/storage/overlay-containers - type: Directory \ No newline at end of file diff --git a/go.mod b/go.mod index 76a70ccc7..59a08ca3d 100644 --- a/go.mod +++ b/go.mod @@ -1,25 +1,45 @@ module github.com/test-network-function/test-network-function -go 1.14 +go 1.17 + +replace github.com/google/goexpect => github.com/test-network-function/goexpect v0.0.1 require ( github.com/Masterminds/semver/v3 v3.1.1 github.com/basgys/goxml2json v1.1.0 github.com/bitly/go-simplejson v0.5.0 // indirect - github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/golang/mock v1.6.0 github.com/google/goexpect v0.0.0-20210330220015-096e5d1cbd97 github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f - github.com/kr/pretty v0.2.1 // indirect - github.com/onsi/ginkgo v1.16.4 - github.com/onsi/gomega v1.14.0 + github.com/onsi/ginkgo/v2 v2.1.3 + github.com/onsi/gomega v1.19.0 github.com/sirupsen/logrus v1.8.1 - github.com/spf13/cobra v1.2.1 - github.com/stretchr/testify v1.7.0 - github.com/test-network-function/test-network-function-claim v1.0.3 + github.com/spf13/cobra v1.4.0 + github.com/stretchr/testify v1.7.5 + github.com/test-network-function/test-network-function-claim v1.0.5 github.com/xeipuuv/gojsonschema v1.2.0 - golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect - golang.org/x/tools v0.1.5 // indirect - google.golang.org/grpc v1.39.0 + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect + google.golang.org/grpc v1.47.0 gopkg.in/yaml.v2 v2.4.0 ) + +require ( + github.com/go-yaml/yaml v2.1.0+incompatible + github.com/hashicorp/go-version v1.5.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect + golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect + google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 3f0a4e724..613f1ece2 100644 --- a/go.sum +++ b/go.sum @@ -1,115 +1,54 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 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/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/a-h/generate v0.0.0-20190312091541-e59c34d33fb3/go.mod h1:traiLYQ0YD7qUMCdjo6/jSaJRPHXniX4HVs+PhEhYpc= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/basgys/goxml2json v1.1.0 h1:4ln5i4rseYfXNd86lGEB+Vi652IsIXIvggKM/BhUKVw= github.com/basgys/goxml2json v1.1.0/go.mod h1:wH7a5Np/Q4QoECFIU8zTQlZwZkrilY0itPfecMw41Dw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 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/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 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.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -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= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -119,499 +58,171 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/goexpect v0.0.0-20210330220015-096e5d1cbd97 h1:/nu0LtOLsZMlqfk6i50C5fmo5cp5WfciWggZYlwGNAg= -github.com/google/goexpect v0.0.0-20210330220015-096e5d1cbd97/go.mod h1:n1ej5+FqyEytMt/mugVDZLIiqTMO+vsrgY+kM6ohzN0= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4= github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -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/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.1.2/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= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -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= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/go-version v1.5.0 h1:O293SZ2Eg+AAYijkVK3jR786Am1bhDEh2GHT0tIVE5E= +github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.14.0 h1:ep6kpPVwmr/nTbklSx2nrLNSIO62DoYAhnPNIMhK8gI= -github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/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/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/test-network-function/test-network-function-claim v1.0.3 h1:r13sg1pOdbW095/IwF+eFW4jppbcyaOGBQ3CGK66j3o= -github.com/test-network-function/test-network-function-claim v1.0.3/go.mod h1:hDN1y2l8D7K3CX2ZyJThSCBXvVksDLJd1xOMmWqUlaY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/test-network-function/goexpect v0.0.1 h1:eX+QxxHNXKPQubIZZ1qI/GsN/owXCqtoRCS72JdBQVs= +github.com/test-network-function/goexpect v0.0.1/go.mod h1:hpGtPP1qpC3lzxn0akwYH3/TdDl/XW6SSmyMivkdleI= +github.com/test-network-function/test-network-function-claim v1.0.5 h1:6MSI0hX/6Z5JTHIxg2n+IQF8qBHAKxvZRdwpdMW8eC8= +github.com/test-network-function/test-network-function-claim v1.0.5/go.mod h1:jPVWu2/YQ0JbY3LHcL+BuZ48VjBLeUaNzh3QqWip4CE= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/ziutek/telnet v0.0.0-20180329124119-c3b780dc415b/go.mod h1:IZpXDfkJ6tWD3PhBK5YzgQT+xJWh7OsdwiG8hA2MkO4= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/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= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/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= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -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-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= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -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-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= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 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= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= 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/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa h1:I0YcKz0I7OAhddo7ya8kMnvprhcWM045PmkBdMO9zN0= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= 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= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= 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= @@ -620,36 +231,23 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 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 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -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/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/api/api.go b/internal/api/api.go index 0d1a594fc..44dc79f37 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -3,10 +3,14 @@ package api import ( "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" log "github.com/sirupsen/logrus" + + "github.com/go-yaml/yaml" + + "github.com/test-network-function/test-network-function/pkg/config/configsections" ) // Endpoints document can be found here @@ -19,16 +23,10 @@ const apiOperatorCatalogExternalBaseEndPoint = "https://catalog.redhat.com/api/c const apiCatalogByRepositoriesBaseEndPoint = "https://catalog.redhat.com/api/containers/v1/repositories/registry/registry.access.redhat.com/repository" var ( - dataKey = "data" - errorContainer404 = fmt.Errorf("error code 404: A container/operator with the specified identifier was not found") - idKey = "_id" + dataKey = "data" + idKey = "_id" ) -// GetContainer404Error return error object with 404 error string -func GetContainer404Error() error { - return errorContainer404 -} - // HTTPClient Client interface type HTTPClient interface { Do(req *http.Request) (*http.Response, error) @@ -44,83 +42,248 @@ func NewHTTPClient() CertAPIClient { return CertAPIClient{Client: &http.Client{}} } -// IsContainerCertified get container image info by repo/name and checks if container details is present -// If present then returns `true` as certified operators. -func (api CertAPIClient) IsContainerCertified(repository, imageName string) bool { - if imageID, err := api.GetImageIDByRepository(repository, imageName); err != nil || imageID == "" { - return false +type catalogQueryResponse struct { + Page uint `json:"page"` + PageSize uint `json:"page_size"` + Total uint `json:"total"` +} + +type ContainerImageFreshnessGrade struct { + // CreationDate time.Time `json:"creation_date"` + Grade string `json:"grade"` + // StartDate time.Time `json:"start_date"` +} +type ContainerCatalogEntry struct { + ID string `json:"_id"` + /*Links struct { + RpmManifest struct { + Href string `json:"href"` + } `json:"rpm_manifest"` + Vulnerabilities struct { + Href string `json:"href"` + } `json:"vulnerabilities"` + } `json:"_links"` + Architecture string `json:"architecture"` + Brew struct { + Build string `json:"build"` + CompletionDate time.Time `json:"completion_date"` + Nvra string `json:"nvra"` + Package string `json:"package"` + } `json:"brew"` + Certified bool `json:"certified"` + ContentSets []string `json:"content_sets"` + CpeIds []string `json:"cpe_ids"` + CreationDate time.Time `json:"creation_date"` + DockerImageID string `json:"docker_image_id"`*/ + FreshnessGrades []ContainerImageFreshnessGrade `json:"freshness_grades"` + /* + ImageID string `json:"image_id"` + LastUpdateDate time.Time `json:"last_update_date"` + ObjectType string `json:"object_type"` + ParsedData struct { + Architecture string `json:"architecture"` + Command string `json:"command"` + Comment string `json:"comment"` + Created time.Time `json:"created"` + DockerVersion string `json:"docker_version"` + EnvVariables []string `json:"env_variables"` + Labels []struct { + Name string `json:"name"` + Value string `json:"value"` + } `json:"labels"` + Layers []string `json:"layers"` + Os string `json:"os"` + Size int `json:"size"` + UncompressedLayerSizes []struct { + LayerID string `json:"layer_id"` + SizeBytes int `json:"size_bytes"` + } `json:"uncompressed_layer_sizes"` + UncompressedSizeBytes int `json:"uncompressed_size_bytes"` + User string `json:"user"` + } `json:"parsed_data"` + Repositories []struct { + Links struct { + ImageAdvisory struct { + Href string `json:"href"` + } `json:"image_advisory"` + Repository struct { + Href string `json:"href"` + } `json:"repository"` + } `json:"_links"` + Comparison struct { + AdvisoryRpmMapping []struct { + AdvisoryIds []string `json:"advisory_ids"` + Nvra string `json:"nvra"` + } `json:"advisory_rpm_mapping"` + Reason string `json:"reason"` + ReasonText string `json:"reason_text"` + Rpms struct { + Downgrade []interface{} `json:"downgrade"` + New []string `json:"new"` + Remove []string `json:"remove"` + Upgrade []string `json:"upgrade"` + } `json:"rpms"` + WithNvr string `json:"with_nvr"` + } `json:"comparison"` + ContentAdvisoryIds []string `json:"content_advisory_ids"` + ImageAdvisoryID string `json:"image_advisory_id"` + ManifestListDigest string `json:"manifest_list_digest"` + ManifestSchema2Digest string `json:"manifest_schema2_digest"` + Published bool `json:"published"` + PublishedDate time.Time `json:"published_date"` + PushDate time.Time `json:"push_date"` + Registry string `json:"registry"` + Repository string `json:"repository"` + Signatures []struct { + KeyLongID string `json:"key_long_id"` + Tags []string `json:"tags"` + } `json:"signatures"` + Tags []struct { + Links struct { + TagHistory struct { + Href string `json:"href"` + } `json:"tag_history"` + } `json:"_links"` + AddedDate time.Time `json:"added_date"` + Name string `json:"name"` + } `json:"tags"` + } `json:"repositories"` + SumLayerSizeBytes int `json:"sum_layer_size_bytes"` + TopLayerID string `json:"top_layer_id"` + UncompressedTopLayerID string `json:"uncompressed_top_layer_id"`*/ +} +type ChartStruct struct { + Entries map[string][]struct { + Name string `yaml:"name"` + Version string `yaml:"version"` + KubeVersion string `yaml:"kubeVersion"` + } `yaml:"entries"` +} + +func (e ContainerCatalogEntry) GetBestFreshnessGrade() string { + grade := "F" + for _, g := range e.FreshnessGrades { + if g.Grade < grade { + grade = g.Grade + } + } + return grade +} + +type containerCatalogQueryResponse struct { + catalogQueryResponse + Data []ContainerCatalogEntry `json:"data"` +} + +// GetContainerCatalogEntry gets the container image entry with highest freshness grade +func (api CertAPIClient) GetContainerCatalogEntry(id configsections.ContainerImageIdentifier) (*ContainerCatalogEntry, error) { + responseData, err := api.getRequest(CreateContainerCatalogQueryURL(id)) + if err == nil { + var response containerCatalogQueryResponse + err = json.Unmarshal(responseData, &response) + if err == nil && len(response.Data) > 0 { + return &response.Data[0], nil + } + } + return nil, err +} + +func CreateContainerCatalogQueryURL(id configsections.ContainerImageIdentifier) string { + var url string + const defaultTag = "latest" + const arch = "amd64" + if id.Digest == "" { + if id.Tag == "" { + id.Tag = defaultTag + } + url = fmt.Sprintf("%s/%s/%s/images?filter=architecture==%s;repositories.repository==%s/%s;repositories.tags.name==%s", + apiCatalogByRepositoriesBaseEndPoint, id.Repository, id.Name, arch, id.Repository, id.Name, id.Tag) + } else { + url = fmt.Sprintf("%s/%s/%s/images?filter=architecture==%s;image_id==%s", apiCatalogByRepositoriesBaseEndPoint, id.Repository, id.Name, arch, id.Digest) } - return true + return url } // IsOperatorCertified get operator bundle by package name and check if package details is present // If present then returns `true` as certified operators. -func (api CertAPIClient) IsOperatorCertified(org, packageName string) bool { - if imageID, err := api.GetOperatorBundleIDByPackageName(org, packageName); err != nil || imageID == "" { - return false +func (api CertAPIClient) IsOperatorCertified(org, packageName, version string) (bool, error) { + imageID, err := api.GetOperatorBundleIDByPackageName(org, packageName, version) + if err == nil { + if imageID == "" { + return false, nil + } + return true, nil } - return true + return false, err } -// GetImageByID get container image data for the given container Id -func (api CertAPIClient) GetImageByID(id string) (response string, err error) { - var responseData []byte +// GetImageByID get container image data for the given container Id. Returns (response, error). +func (api CertAPIClient) GetImageByID(id string) (string, error) { + var response string url := fmt.Sprintf("%s/images/id/%s", apiContainerCatalogExternalBaseEndPoint, id) - if responseData, err = api.getRequest(url); err == nil { + responseData, err := api.getRequest(url) + if err == nil { response = string(responseData) } - return + return response, err } -// GetImageIDByRepository get container image data for the given container Id -func (api CertAPIClient) GetImageIDByRepository(repository, imageName string) (imageID string, err error) { - var responseData []byte - url := fmt.Sprintf("%s/%s/%s/images?page_size=1", apiCatalogByRepositoriesBaseEndPoint, repository, imageName) - if responseData, err = api.getRequest(url); err == nil { - imageID, err = api.getIDFromResponse(responseData) +// GetOperatorBundleIDByPackageName get published operator bundle Id by organization and package name. +// Returns (ImageID, error). +func (api CertAPIClient) GetOperatorBundleIDByPackageName(org, name, vsersion string) (string, error) { + var imageID string + url := "" + if vsersion != "" { + url = fmt.Sprintf("%s/bundles?page_size=1&filter=organization==%s;csv_name==%s;ocp_version==%s", apiOperatorCatalogExternalBaseEndPoint, org, name, vsersion) + } else { + url = fmt.Sprintf("%s/bundles?page_size=1&filter=organization==%s;csv_name==%s", apiOperatorCatalogExternalBaseEndPoint, org, name) } - return -} -// GetOperatorBundleIDByPackageName get published operator bundle Id by organization and package name -func (api CertAPIClient) GetOperatorBundleIDByPackageName(org, name string) (imageID string, err error) { - var responseData []byte - url := fmt.Sprintf("%s/bundles?page_size=1&organization=%s&package=%s", apiOperatorCatalogExternalBaseEndPoint, org, name) - if responseData, err = api.getRequest(url); err == nil { + responseData, err := api.getRequest(url) + if err == nil { imageID, err = api.getIDFromResponse(responseData) } - return + + return imageID, err +} +func (api CertAPIClient) GetYamlFile() (ChartStruct, error) { + url := ("https://charts.openshift.io/index.yaml") + responseData, err := api.getRequest(url) + var charts ChartStruct + if err != nil { + log.Error("error reading the helm certification list ", err) + return charts, err + } + if err = yaml.Unmarshal(responseData, &charts); err != nil { + log.Error("error while parsing the yaml file of the helm certification list ", err) + } + return charts, err } -// getRequest a http call to rest api, returns byte array or error -func (api CertAPIClient) getRequest(url string) (response []byte, err error) { - req, err := http.NewRequest(http.MethodGet, url, nil) //nolint:noctx +// getRequest a http call to rest api, returns byte array or error. Returns (response, error). +func (api CertAPIClient) getRequest(url string) ([]byte, error) { + req, err := http.NewRequest(http.MethodGet, url, http.NoBody) //nolint:noctx if err != nil { return nil, err } resp, err := api.Client.Do(req) if err != nil { - return + return nil, err } defer resp.Body.Close() - if resp.StatusCode == http.StatusNotFound { - err = GetContainer404Error() - return - } - if response, err = ioutil.ReadAll(resp.Body); err != nil { - err = GetContainer404Error() - return + response, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err } - return + return response, nil } -// getIDFromResponse searches for first occurrence of id and return -func (api CertAPIClient) getIDFromResponse(response []byte) (id string, err error) { +// getIDFromResponse searches for first occurrence of id and return. Returns (id and error). +func (api CertAPIClient) getIDFromResponse(response []byte) (string, error) { var data interface{} - if err = json.Unmarshal(response, &data); err != nil { - log.Errorf("Error calling API Request %v", err.Error()) - err = GetContainer404Error() - return + var id string + if err := json.Unmarshal(response, &data); err != nil { + return id, fmt.Errorf("error unmarshalling payload in API Response %v", err.Error()) } m := data.(map[string]interface{}) for k, v := range m { @@ -138,7 +301,7 @@ func (api CertAPIClient) getIDFromResponse(response []byte) (id string, err erro } } - return + return id, nil } // Find key in interface (recursively) and return value as interface diff --git a/internal/api/api_test.go b/internal/api/api_test.go index 95f60b91e..eaa9354a9 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -3,17 +3,18 @@ package api_test import ( "bytes" "fmt" - "io/ioutil" + "io" "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/test-network-function/test-network-function/internal/api" + "github.com/test-network-function/test-network-function/pkg/config/configsections" ) const ( id = "5ea8cf595a13466876a10215" - imageName = "nginx-116" + imageName = "nginx-120" marketPlaceOrg = "redhat-marketplace" packageName = "amq-streams" redHatOrg = "redhat-operators" @@ -21,31 +22,443 @@ const ( unKnownRepository = "wrong_repo" unKnownImageName = "wrong_id" unknownPackageName = "unknownPackage" + version = "4.8" jsonResponseFound = `{ - "data": [{ - "_id": "5ea8cf595a13466876a10215", - "_links": { - "certification_project": { - "href": "/v1/repositories/registry/registry.access.redhat.com/repository/rhel8/nginx-116/projects/certification" + "data": [ + { + "_id": "61ba0db5d095e30ed5db6330", + "_links": { + "rpm_manifest": { + "href": "/v1/images/id/61ba0db5d095e30ed5db6330/rpm-manifest" + }, + "vulnerabilities": { + "href": "/v1/images/id/61ba0db5d095e30ed5db6330/vulnerabilities" + } }, - "images": { - "href": "/v1/repositories/registry/registry.access.redhat.com/repository/rhel8/nginx-116/images" + "architecture": "amd64", + "brew": { + "build": "nginx-120-container-1-7", + "completion_date": "2021-12-15T15:39:17+00:00", + "nvra": "nginx-120-container-1-7.amd64", + "package": "nginx-120-container" }, - "vendor": { - "href": "/v1/vendors/label/redhat" - } - }, - "application_categories": [ - "Web Services" - ] - }] -}` + "certified": false, + "content_sets": [ + "rhel-8-for-x86_64-baseos-rpms", + "rhel-8-for-x86_64-appstream-rpms" + ], + "cpe_ids": [ + "cpe:/a:redhat:enterprise_linux:8::appstream", + "cpe:/o:redhat:enterprise_linux:8::baseos", + "cpe:/o:redhat:rhel:8.3::baseos", + "cpe:/a:redhat:rhel:8.3::appstream" + ], + "creation_date": "2021-12-15T15:45:57.616000+00:00", + "docker_image_id": "sha256:b9dbffacfeb14acf36c7da686a0874be4484a473a8993e4aaf72a68b80ea4cf6", + "freshness_grades": [ + { + "creation_date": "2021-12-15T15:46:07.851000+00:00", + "grade": "A", + "start_date": "2021-12-15T15:46:00+00:00" + } + ], + "image_id": "sha256:aa34453a6417f8f76423ffd2cf874e9c4a1a5451ac872b78dc636ab54a0ebbc3", + "last_update_date": "2021-12-21T12:10:34.397000+00:00", + "object_type": "containerImage", + "parsed_data": { + "architecture": "amd64", + "command": "['/bin/sh', '-c', '$STI_SCRIPTS_PATH/usage']", + "comment": "", + "created": "2021-12-15T15:31:51.564422Z", + "docker_version": "1.13.1", + "env_variables": [ + "PATH=/opt/app-root/src/bin:/opt/app-root/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "container=oci", + "SUMMARY=Platform for running nginx 1.20 or building nginx-based application", + "DESCRIPTION=Nginx is a web server and a reverse proxy server for HTTP, SMTP, POP3 and IMAP protocols, ", + "STI_SCRIPTS_URL=image:///usr/libexec/s2i", + "STI_SCRIPTS_PATH=/usr/libexec/s2i", + "APP_ROOT=/opt/app-root", + "HOME=/opt/app-root/src", + "PLATFORM=el8", + "NAME=nginx", + "NGINX_VERSION=1.20", + "NGINX_SHORT_VER=120", + "VERSION=0", + "NGINX_CONFIGURATION_PATH=/opt/app-root/etc/nginx.d", + "NGINX_CONF_PATH=/etc/nginx/nginx.conf", + "NGINX_DEFAULT_CONF_PATH=/opt/app-root/etc/nginx.default.d", + "NGINX_CONTAINER_SCRIPTS_PATH=/usr/share/container-scripts/nginx", + "NGINX_APP_ROOT=/opt/app-root", + "NGINX_LOG_PATH=/var/log/nginx", + "NGINX_PERL_MODULE_PATH=/opt/app-root/etc/perl" + ], + "labels": [ + { + "name": "architecture", + "value": "x86_64" + }, + { + "name": "build-date", + "value": "2021-12-15T15:30:27.746206" + }, + { + "name": "com.redhat.build-host", + "value": "cpt-1003.osbs.prod.upshift.rdu2.redhat.com" + }, + { + "name": "com.redhat.component", + "value": "nginx-120-container" + }, + { + "name": "com.redhat.license_terms", + "value": "https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" + }, + { + "name": "description", + "value": "Nginx is a web server and a reverse proxy server for HTTP, SMTP, POP3 and IMAP protocols, " + }, + { + "name": "distribution-scope", + "value": "public" + }, + { + "name": "help", + "value": "For more information visit https://github.com/sclorg/nginx-container" + }, + { + "name": "io.k8s.description", + "value": "Nginx is a web server and a reverse proxy server for HTTP, SMTP, POP3 and IMAP protocols, " + }, + { + "name": "io.k8s.display-name", + "value": "Nginx 1.20" + }, + { + "name": "io.openshift.expose-services", + "value": "8443:https" + }, + { + "name": "io.openshift.s2i.scripts-url", + "value": "image:///usr/libexec/s2i" + }, + { + "name": "io.openshift.tags", + "value": "builder,nginx,nginx-120" + }, + { + "name": "io.s2i.scripts-url", + "value": "image:///usr/libexec/s2i" + }, + { + "name": "maintainer", + "value": "SoftwareCollections.org " + }, + { + "name": "name", + "value": "ubi8/nginx-120" + }, + { + "name": "release", + "value": "7" + }, + { + "name": "summary", + "value": "Platform for running nginx 1.20 or building nginx-based application" + }, + { + "name": "url", + "value": "https://access.redhat.com/containers/#/registry.access.redhat.com/ubi8/nginx-120/images/1-7" + }, + { + "name": "usage", + "value": "s2i build ubi8/nginx-120:latest " + }, + { + "name": "vcs-ref", + "value": "ee2f1c913a5a96f9680f7414a932a7c79558cbaa" + }, + { + "name": "vcs-type", + "value": "git" + }, + { + "name": "vendor", + "value": "Red Hat, Inc." + }, + { + "name": "version", + "value": "1" + } + ], + "layers": [ + "sha256:26c599acaaef776aada58962ad763a181101d2f49e204763ab276e67b37d5d88", + "sha256:0661f10c38ccb1007a5937fd652f834283d016642264a0e031028979fcfb2dbf", + "sha256:adffa69631469a649556cee5b8456f184928818064aac82106bd08bd62e51d4e", + "sha256:26f1167feaf74177f9054bf26ac8775a4b188f25914e23bda9574ef2a759cce4" + ], + "os": "linux", + "ports": "[\\\"8080/tcp\\\", \\\"8443/tcp\\\"]", + "size": 0, + "uncompressed_layer_sizes": [ + { + "layer_id": "sha256:ec1a38375a3346f8d00b725aaab417725bad949ee387422689c69d72ef5d940e", + "size_bytes": 165025139 + }, + { + "layer_id": "sha256:558b534f4e1baf7b63f0a54e8926e2e4ea4a582be73120fa7f6a3b86d7070328", + "size_bytes": 56483974 + }, + { + "layer_id": "sha256:3ba8c926eef966b75b9545c1c2d990d3d114a4063ab71801dcaaf53165a2b130", + "size_bytes": 4719 + }, + { + "layer_id": "sha256:352ba846236b2af884cab10c53aa37d82bba9d9fb0f8797d5af211ccf317e236", + "size_bytes": 215755840 + } + ], + "uncompressed_size_bytes": 437269672, + "user": "1001" + }, + "repositories": [ + { + "_links": { + "image_advisory": { + "href": "/v1/advisories/redhat/id/RHBA-2021:5260" + }, + "repository": { + "href": "/v1/repositories/registry/registry.access.redhat.com/repository/ubi8/nginx-120" + } + }, + "comparison": { + "advisory_rpm_mapping": [ + { + "advisory_ids": [ + "RHBA-2021:5229" + ], + "nvra": "systemd-239-51.el8_5.2.x86_64" + }, + { + "advisory_ids": [ + "RHBA-2021:5229" + ], + "nvra": "systemd-pam-239-51.el8_5.2.x86_64" + }, + { + "advisory_ids": [ + "RHSA-2021:5226" + ], + "nvra": "openssl-libs-1.1.1k-5.el8_5.x86_64" + }, + { + "advisory_ids": [ + "RHBA-2021:5229" + ], + "nvra": "systemd-libs-239-51.el8_5.2.x86_64" + }, + { + "advisory_ids": [ + "RHSA-2021:5226" + ], + "nvra": "openssl-1.1.1k-5.el8_5.x86_64" + } + ], + "reason": "OK", + "reason_text": "No error", + "rpms": { + "downgrade": [], + "new": [], + "remove": [], + "upgrade": [ + "systemd-239-51.el8_5.2.x86_64", + "systemd-pam-239-51.el8_5.2.x86_64", + "openssl-libs-1.1.1k-5.el8_5.x86_64", + "systemd-libs-239-51.el8_5.2.x86_64", + "openssl-1.1.1k-5.el8_5.x86_64" + ] + }, + "with_nvr": "nginx-120-container-1-5.1638356804" + }, + "content_advisory_ids": [ + "RHSA-2021:5226", + "RHBA-2021:5229" + ], + "image_advisory_id": "RHBA-2021:5260", + "manifest_list_digest": "sha256:53f454b7894a3f4c4afea398c881e84bf9f3a375c41b119ba86f732f6eba1f92", + "manifest_schema2_digest": "sha256:aa34453a6417f8f76423ffd2cf874e9c4a1a5451ac872b78dc636ab54a0ebbc3", + "published": true, + "published_date": "2021-12-21T12:04:49+00:00", + "push_date": "2021-12-21T11:48:39+00:00", + "registry": "registry.access.redhat.com", + "repository": "ubi8/nginx-120", + "signatures": [ + { + "key_long_id": "199E2F91FD431D51", + "tags": [ + "1", + "1-7", + "latest" + ] + } + ], + "tags": [ + { + "_links": { + "tag_history": { + "href": "/v1/tag-history/registry/registry.access.redhat.com/repository/ubi8/nginx-120/tag/latest" + } + }, + "added_date": "2021-12-21T12:10:34.397000+00:00", + "name": "latest" + }, + { + "_links": { + "tag_history": { + "href": "/v1/tag-history/registry/registry.access.redhat.com/repository/ubi8/nginx-120/tag/1" + } + }, + "added_date": "2021-12-21T12:10:34.397000+00:00", + "name": "1" + }, + { + "_links": { + "tag_history": { + "href": "/v1/tag-history/registry/registry.access.redhat.com/repository/ubi8/nginx-120/tag/1-7" + } + }, + "added_date": "2021-12-21T12:10:34.397000+00:00", + "name": "1-7" + } + ] + }, + { + "_links": { + "image_advisory": { + "href": "/v1/advisories/redhat/id/RHBA-2021:5260" + }, + "repository": { + "href": "/v1/repositories/registry/registry.access.redhat.com/repository/rhel8/nginx-120" + } + }, + "comparison": { + "advisory_rpm_mapping": [ + { + "advisory_ids": [ + "RHBA-2021:5229" + ], + "nvra": "systemd-239-51.el8_5.2.x86_64" + }, + { + "advisory_ids": [ + "RHBA-2021:5229" + ], + "nvra": "systemd-pam-239-51.el8_5.2.x86_64" + }, + { + "advisory_ids": [ + "RHSA-2021:5226" + ], + "nvra": "openssl-libs-1.1.1k-5.el8_5.x86_64" + }, + { + "advisory_ids": [ + "RHBA-2021:5229" + ], + "nvra": "systemd-libs-239-51.el8_5.2.x86_64" + }, + { + "advisory_ids": [ + "RHSA-2021:5226" + ], + "nvra": "openssl-1.1.1k-5.el8_5.x86_64" + } + ], + "reason": "OK", + "reason_text": "No error", + "rpms": { + "downgrade": [], + "new": [], + "remove": [], + "upgrade": [ + "systemd-239-51.el8_5.2.x86_64", + "systemd-pam-239-51.el8_5.2.x86_64", + "openssl-libs-1.1.1k-5.el8_5.x86_64", + "systemd-libs-239-51.el8_5.2.x86_64", + "openssl-1.1.1k-5.el8_5.x86_64" + ] + }, + "with_nvr": "nginx-120-container-1-5.1638356804" + }, + "content_advisory_ids": [ + "RHSA-2021:5226", + "RHBA-2021:5229" + ], + "image_advisory_id": "RHBA-2021:5260", + "manifest_list_digest": "sha256:53f454b7894a3f4c4afea398c881e84bf9f3a375c41b119ba86f732f6eba1f92", + "manifest_schema2_digest": "sha256:aa34453a6417f8f76423ffd2cf874e9c4a1a5451ac872b78dc636ab54a0ebbc3", + "published": true, + "published_date": "2021-12-21T12:04:52+00:00", + "push_date": "2021-12-21T11:48:39+00:00", + "registry": "registry.access.redhat.com", + "repository": "rhel8/nginx-120", + "signatures": [ + { + "key_long_id": "199E2F91FD431D51", + "tags": [ + "1", + "1-7", + "latest" + ] + } + ], + "tags": [ + { + "_links": { + "tag_history": { + "href": "/v1/tag-history/registry/registry.access.redhat.com/repository/rhel8/nginx-120/tag/1" + } + }, + "added_date": "2021-12-21T12:08:52.904000+00:00", + "name": "1" + }, + { + "_links": { + "tag_history": { + "href": "/v1/tag-history/registry/registry.access.redhat.com/repository/rhel8/nginx-120/tag/latest" + } + }, + "added_date": "2021-12-21T12:08:52.904000+00:00", + "name": "latest" + }, + { + "_links": { + "tag_history": { + "href": "/v1/tag-history/registry/registry.access.redhat.com/repository/rhel8/nginx-120/tag/1-7" + } + }, + "added_date": "2021-12-21T12:08:52.904000+00:00", + "name": "1-7" + } + ] + } + ], + "sum_layer_size_bytes": 162814999, + "top_layer_id": "sha256:26c599acaaef776aada58962ad763a181101d2f49e204763ab276e67b37d5d88", + "uncompressed_top_layer_id": "sha256:ec1a38375a3346f8d00b725aaab417725bad949ee387422689c69d72ef5d940e" + } + ], + "page": 0, + "page_size": 100, + "total": 1 + }` jsonResponseNotFound = `{ - "detail": "The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.", - "status": 404, - "title": "Not Found", - "type": "about:blank" - }` + "data": [], + "page": 0, + "page_size": 1, + "total": 0 + } + ` ) var ( @@ -70,29 +483,47 @@ var ( name string id string expectedError error - expectedResult bool + expectedResult *api.ContainerCatalogEntry responseData string responseStatus int }{ - {repository: repository, name: imageName, expectedError: nil, id: "", expectedResult: true, + {repository: repository, name: imageName, expectedError: nil, id: "", + expectedResult: &api.ContainerCatalogEntry{ID: "61ba0db5d095e30ed5db6330", + FreshnessGrades: []api.ContainerImageFreshnessGrade{{Grade: "A"}}}, responseData: jsonResponseFound, responseStatus: http.StatusAccepted}, - {repository: unKnownRepository, name: unKnownImageName, expectedError: api.GetContainer404Error(), id: "", expectedResult: false, - responseData: jsonResponseNotFound, responseStatus: http.StatusNotFound}, + {repository: unKnownRepository, name: unKnownImageName, expectedError: nil, id: "", expectedResult: nil, + responseData: jsonResponseNotFound, responseStatus: http.StatusAccepted}, } operatorTestCases = []struct { - packageName string - org string - id string - expectedErrorString string - expectedResult bool - responseData string - responseStatus int + packageName string + org string + id string + expectedError error + expectedResult bool + responseData string + responseStatus int + version string }{ - {packageName: packageName, org: redHatOrg, expectedErrorString: "", id: "", expectedResult: true, - responseData: jsonResponseFound, responseStatus: http.StatusAccepted}, - {packageName: unknownPackageName, org: marketPlaceOrg, expectedErrorString: api.GetContainer404Error().Error(), id: "", expectedResult: false, - responseData: jsonResponseNotFound, responseStatus: http.StatusNotFound}, + {packageName: packageName, org: redHatOrg, expectedError: nil, id: "", expectedResult: true, + responseData: jsonResponseFound, responseStatus: http.StatusAccepted, version: version}, + {packageName: unknownPackageName, org: marketPlaceOrg, expectedError: nil, id: "", expectedResult: false, + responseData: jsonResponseNotFound, responseStatus: http.StatusNotFound, version: version}, + } + + containerQueryURLTestCases = []struct { + id configsections.ContainerImageIdentifier + url string + }{ + {id: configsections.ContainerImageIdentifier{Repository: "rhel8", Name: "nginx-120", Tag: "1-7"}, + url: "https://catalog.redhat.com/api/containers/v1/repositories/registry/registry.access.redhat.com/repository/rhel8/nginx-120/" + + "images?filter=architecture==amd64;repositories.repository==rhel8/nginx-120;repositories.tags.name==1-7"}, + {id: configsections.ContainerImageIdentifier{Repository: "rhel8", Name: "nginx-120", Digest: "sha256:aa34453a6417f8f76423ffd2cf874e9c4a1a5451ac872b78dc636ab54a0ebbc3"}, + url: "https://catalog.redhat.com/api/containers/v1/repositories/registry/registry.access.redhat.com/repository/rhel8/nginx-120/" + + "images?filter=architecture==amd64;image_id==sha256:aa34453a6417f8f76423ffd2cf874e9c4a1a5451ac872b78dc636ab54a0ebbc3"}, + {id: configsections.ContainerImageIdentifier{Repository: "rhel8", Name: "nginx-120"}, + url: "https://catalog.redhat.com/api/containers/v1/repositories/registry/registry.access.redhat.com/repository/rhel8/nginx-120/" + + "images?filter=architecture==amd64;repositories.repository==rhel8/nginx-120;repositories.tags.name==latest"}, } ) @@ -102,7 +533,7 @@ func (m *MockClient) Do(req *http.Request) (*http.Response, error) { } func getDoFunc(data string, status int) func(req *http.Request) (*http.Response, error) { - response := ioutil.NopCloser(bytes.NewReader([]byte(data))) + response := io.NopCloser(bytes.NewReader([]byte(data))) defer response.Close() return func(*http.Request) (*http.Response, error) { return &http.Response{ @@ -111,19 +542,28 @@ func getDoFunc(data string, status int) func(req *http.Request) (*http.Response, }, nil } } -func TestApiClient_IsContainerCertified(t *testing.T) { +func TestApiClient_GetContainerCatalogEntry(t *testing.T) { for _, c := range containerTestCases { GetDoFunc = getDoFunc(c.responseData, c.responseStatus) //nolint:bodyclose - result := client.IsContainerCertified(c.repository, c.name) + result, err := client.GetContainerCatalogEntry(configsections.ContainerImageIdentifier{Repository: c.repository, Name: c.name}) assert.Equal(t, c.expectedResult, result) + assert.Equal(t, c.expectedError, err) } } func TestApiClient_IsOperatorCertified(t *testing.T) { for _, c := range operatorTestCases { GetDoFunc = getDoFunc(c.responseData, c.responseStatus) //nolint:bodyclose - result := client.IsOperatorCertified(c.org, c.packageName) + result, err := client.IsOperatorCertified(c.org, c.packageName, c.version) assert.Equal(t, c.expectedResult, result) + assert.Equal(t, c.expectedError, err) + } +} + +func TestCreateContainerCatalogQueryURL(t *testing.T) { + for _, c := range containerQueryURLTestCases { + url := api.CreateContainerCatalogQueryURL(c.id) + assert.Equal(t, url, c.url) } } diff --git a/pkg/config/autodiscover/autodiscover.go b/pkg/config/autodiscover/autodiscover.go index ca2b02c73..a3ecb3c52 100644 --- a/pkg/config/autodiscover/autodiscover.go +++ b/pkg/config/autodiscover/autodiscover.go @@ -19,51 +19,157 @@ package autodiscover import ( "fmt" "os" - "os/exec" "strconv" + "strings" + "time" log "github.com/sirupsen/logrus" "github.com/test-network-function/test-network-function/pkg/config/configsections" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" + "github.com/test-network-function/test-network-function/pkg/utils" ) const ( disableAutodiscoverEnvVar = "TNF_DISABLE_CONFIG_AUTODISCOVER" - tnfNamespace = "test-network-function.com" + tnfLabelPrefix = "test-network-function.com" labelTemplate = "%s/%s" - // anyLabelValue is the value that will allow any value for a label when building the label query. - anyLabelValue = "" + anyLabelValue = "" + ocCommand = "oc get %s -n %s -o json -l %s" + ocAllCommand = "oc get %s -A -o json -l %s" + ocCommandTimeOut = time.Second * 15 +) + +var ( + expectersVerboseModeEnabled = false ) // PerformAutoDiscovery checks the environment variable to see if autodiscovery should be performed -func PerformAutoDiscovery() (doAuto bool) { - doAuto, _ = strconv.ParseBool(os.Getenv(disableAutodiscoverEnvVar)) +func PerformAutoDiscovery() bool { + doAuto, _ := strconv.ParseBool(os.Getenv(disableAutodiscoverEnvVar)) return !doAuto } -func buildLabelName(labelNS, labelName string) string { - if labelNS == "" { +func buildLabelName(labelPrefix, labelName string) string { + if labelPrefix == "" { return labelName } - return fmt.Sprintf(labelTemplate, labelNS, labelName) + return fmt.Sprintf(labelTemplate, labelPrefix, labelName) } func buildAnnotationName(annotationName string) string { - return buildLabelName(tnfNamespace, annotationName) + return buildLabelName(tnfLabelPrefix, annotationName) } func buildLabelQuery(label configsections.Label) string { - namespacedLabel := buildLabelName(label.Namespace, label.Name) + fullLabelName := buildLabelName(label.Prefix, label.Name) if label.Value != anyLabelValue { - return fmt.Sprintf("%s=%s", namespacedLabel, label.Value) + return fmt.Sprintf("%s=%s", fullLabelName, label.Value) + } + return fullLabelName +} + +var executeOcGetCommand = func(resourceType, labelQuery, namespace string) string { + ocCommandToExecute := fmt.Sprintf(ocCommand, resourceType, namespace, labelQuery) + match := utils.ExecuteCommandAndValidate(ocCommandToExecute, ocCommandTimeOut, interactive.GetContext(expectersVerboseModeEnabled), func() { + log.Error("can't run command: ", ocCommandToExecute) + }) + return match +} + +var executeOcGetAllCommand = func(resourceType, labelQuery string) string { + ocCommandToExecute := fmt.Sprintf(ocAllCommand, resourceType, labelQuery) + match := utils.ExecuteCommandAndValidate(ocCommandToExecute, ocCommandTimeOut, interactive.GetContext(expectersVerboseModeEnabled), func() { + log.Error("can't run command: ", ocCommandToExecute) + }) + return match +} + +// getContainersByLabel builds `configsections.Container`s from containers in pods matching a label. +// Returns slice of ContainerConfig, error. +func getContainersByLabel(label configsections.Label) ([]configsections.Container, error) { + pods, err := GetPodsByLabel(label) + if err != nil { + return nil, err + } + containers := []configsections.Container{} + for i := range pods.Items { + containers = append(containers, buildContainers(pods.Items[i])...) } - return namespacedLabel + return containers, nil } -func makeGetCommand(resourceType, labelQuery string) *exec.Cmd { - // TODO: shell expecter - cmd := exec.Command("oc", "get", resourceType, "-A", "-o", "json", "-l", labelQuery) - log.Debug("Issuing get command ", cmd.Args) +// getContainerIdentifiersByLabel builds `config.ContainerIdentifier`s from containers in pods matching a label. +// Returns slice of ContainerIdentifier, error. +func getContainerIdentifiersByLabel(label configsections.Label) ([]configsections.ContainerIdentifier, error) { + containers, err := getContainersByLabel(label) + if err != nil { + return nil, err + } + containerIDs := []configsections.ContainerIdentifier{} + for _, c := range containers { + containerIDs = append(containerIDs, c.ContainerIdentifier) + } + return containerIDs, nil +} + +// buildContainers builds a container list +// Returns slice of Container +func buildContainers(pr *PodResource) []configsections.Container { + containers := []configsections.Container{} + for _, containerResource := range pr.Spec.Containers { + var container configsections.Container + container.Namespace = pr.Metadata.Namespace + container.PodName = pr.Metadata.Name + container.ContainerName = containerResource.Name + container.NodeName = pr.Spec.NodeName + container.ImageSource = buildContainerImageSource(containerResource.Image) + // This is to have access to the pod namespace + for _, cs := range pr.Status.ContainerStatuses { + if cs.Name == container.ContainerName { + container.ContainerUID = "" + split := strings.Split(cs.ContainerID, "://") + if len(split) > 0 { + container.ContainerUID = split[len(split)-1] + container.ContainerRuntime = split[0] + } + } + } + + log.Debugf("added container: %s", container.String()) + containers = append(containers, container) + } + return containers +} + +//nolint:gomnd +func buildContainerImageSource(url string) *configsections.ContainerImageSource { + source := configsections.ContainerImageSource{} + urlSegments := strings.Split(url, "/") + n := len(urlSegments) + if n > 2 { + source.Registry = strings.Join(urlSegments[:n-2], "/") + } + if n > 1 { + source.Repository = urlSegments[n-2] + } + colonIndex := strings.Index(urlSegments[n-1], ":") + atIndex := strings.Index(urlSegments[n-1], "@") + if atIndex == -1 { + if colonIndex == -1 { + source.Name = urlSegments[n-1] + } else { + source.Name = urlSegments[n-1][:colonIndex] + source.Tag = urlSegments[n-1][colonIndex+1:] + } + } else { + source.Name = urlSegments[n-1][:atIndex] + source.Digest = urlSegments[n-1][atIndex+1:] + } + return &source +} - return cmd +// EnableExpectersVerboseMode enables the verbose mode for expecters (Sent/Match output) +func EnableExpectersVerboseMode() { + expectersVerboseModeEnabled = true } diff --git a/pkg/config/autodiscover/autodiscover_cnfs.go b/pkg/config/autodiscover/autodiscover_cnfs.go deleted file mode 100644 index 71f8982a1..000000000 --- a/pkg/config/autodiscover/autodiscover_cnfs.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package autodiscover - -import ( - log "github.com/sirupsen/logrus" - "github.com/test-network-function/test-network-function/pkg/config/configsections" - "github.com/test-network-function/test-network-function/pkg/tnf/testcases" -) - -const ( - cnfLabelName = "container" - configuredTestFile = "testconfigure.yml" -) - -var ( - cnfTestsAnnotationName = buildAnnotationName("container_tests") -) - -// BuildCNFsConfig builds a `[]configsections.Cnf` from the current state of the cluster, -// using labels and annotations to populate the data. -func BuildCNFsConfig() (cnfs []configsections.Cnf) { - pods, err := GetPodsByLabel(configsections.Label{Namespace: tnfNamespace, Name: cnfLabelName, Value: anyLabelValue}) - if err != nil { - log.Fatalf("found no CNFs to test while 'container' spec enabled: %s", err) - } - for i := range pods.Items { - cnfs = append(cnfs, BuildCnfFromPodResource(&pods.Items[i])) - } - return cnfs -} - -// BuildCnfFromPodResource builds a single `configsections.Cnf` from a PodResource -func BuildCnfFromPodResource(pr *PodResource) (cnf configsections.Cnf) { - var err error - cnf.Namespace = pr.Metadata.Namespace - cnf.Name = pr.Metadata.Name - - var tests []string - err = pr.GetAnnotationValue(cnfTestsAnnotationName, &tests) - if err != nil { - log.Warnf("unable to extract tests from annotation on '%s/%s' (error: %s). Attempting to fallback to all tests", cnf.Namespace, cnf.Name, err) - cnf.Tests = getConfiguredCNFTests() - } else { - cnf.Tests = tests - } - return -} - -// getConfiguredCNFTests loads the `configuredTestFile` used by the `operator` and `container` specs, and extracts -// the names of test groups from it. -func getConfiguredCNFTests() (cnfTests []string) { - configuredTests, err := testcases.LoadConfiguredTestFile(configuredTestFile) - if err != nil { - log.Errorf("failed to load %s, continuing with no tests", configuredTestFile) - return []string{} - } - for _, configuredTest := range configuredTests.CnfTest { - cnfTests = append(cnfTests, configuredTest.Name) - } - log.WithField("cnfTests", cnfTests).Infof("got all tests from %s.", configuredTestFile) - return cnfTests -} diff --git a/pkg/config/autodiscover/autodiscover_debug.go b/pkg/config/autodiscover/autodiscover_debug.go new file mode 100644 index 000000000..99722025d --- /dev/null +++ b/pkg/config/autodiscover/autodiscover_debug.go @@ -0,0 +1,113 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package autodiscover + +import ( + "fmt" + "time" + + "github.com/onsi/gomega" + log "github.com/sirupsen/logrus" + "github.com/test-network-function/test-network-function/pkg/config/configsections" + "github.com/test-network-function/test-network-function/pkg/tnf" + ds "github.com/test-network-function/test-network-function/pkg/tnf/handlers/daemonset" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" + "github.com/test-network-function/test-network-function/pkg/utils" +) + +const ( + defaultNamespace = "default" + debugDaemonSet = "debug" + debugLabelName = "test-network-function.com/app" + debugLabelValue = "debug" + nodeLabelName = "test-network-function.com/node" + nodeLabelValue = "target" + addlabelCommand = "oc label node %s %s=%s --overwrite=true" + deletelabelCommand = "oc label node %s %s- --overwrite=true" + dsTimeoutMins = 5 + dsRetryIntervalSecs = 5 +) + +// FindDebugPods completes a `configsections.TestPartner.ContainersDebugList` from the current state of the cluster, +// using labels and annotations to populate the data, if it's not fully configured +func FindDebugPods(tp *configsections.TestPartner) { + label := configsections.Label{Name: debugLabelName, Value: debugLabelValue} + pods, err := GetPodsByLabelByNamespace(label, defaultNamespace) + if err != nil { + log.Panic("can't find debug pods. Error: ", err) + } + if len(pods.Items) == 0 { + log.Panic("can't find debug pods, make sure daemonset debug is deployed properly") + } + for _, pod := range pods.Items { + tp.ContainersDebugList = append(tp.ContainersDebugList, buildContainers(pod)[0]) + } +} + +// AddDebugLabel add debug label to node +func AddDebugLabel(nodeName string) { + log.Info("add label ", nodeLabelName, "=", nodeLabelValue, " to node ", nodeName) + ocCommand := fmt.Sprintf(addlabelCommand, nodeName, nodeLabelName, nodeLabelValue) + _ = utils.ExecuteCommandAndValidate(ocCommand, ocCommandTimeOut, interactive.GetContext(expectersVerboseModeEnabled), func() { + log.Error("error in adding label to node ", nodeName) + }) +} + +// AddDebugLabel remove debug label from node +func DeleteDebugLabel(nodeName string) { + log.Info("delete label ", nodeLabelName, "=", nodeLabelValue, "to node ", nodeName) + ocCommand := fmt.Sprintf(deletelabelCommand, nodeName, nodeLabelName) + _ = utils.ExecuteCommandAndValidate(ocCommand, ocCommandTimeOut, interactive.GetContext(expectersVerboseModeEnabled), func() { + log.Error("error in removing label from node ", nodeName) + }) +} + +// CheckDebugDaemonset checks if the debug pods are deployed properly +// the function will try DefaultTimeout/time.Second times +func CheckDebugDaemonset(expectedDebugPods int) { + gomega.Eventually(func() bool { + log.Debug("check debug daemonset status") + return checkDebugPodsReadiness(expectedDebugPods) + }, dsTimeoutMins*time.Minute, dsRetryIntervalSecs*time.Second).Should(gomega.Equal(true)) +} + +// checkDebugPodsReadiness helper function that returns true if the daemonset debug is deployed properly +func checkDebugPodsReadiness(expectedDebugPods int) bool { + context := interactive.GetContext(expectersVerboseModeEnabled) + tester := ds.NewDaemonSet(DefaultTimeout, debugDaemonSet, defaultNamespace) + test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) + if err != nil { + log.Error("can't run test to detect daemonset status") + return false + } + _, err = test.Run() + if err != nil { + return false + } + dsStatus := tester.GetStatus() + if expectedDebugPods == dsStatus.Desired && + dsStatus.Desired == dsStatus.Current && + dsStatus.Current == dsStatus.Available && + dsStatus.Available == dsStatus.Ready && + dsStatus.Misscheduled == 0 { + log.Info("daemonset is ready") + return true + } + log.Warn("daemonset is not ready") + return false +} diff --git a/pkg/config/autodiscover/autodiscover_debug_test.go b/pkg/config/autodiscover/autodiscover_debug_test.go new file mode 100644 index 000000000..5c351ce5a --- /dev/null +++ b/pkg/config/autodiscover/autodiscover_debug_test.go @@ -0,0 +1,70 @@ +// Copyright (C) 2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package autodiscover + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/config/configsections" +) + +func TestFindDebugPods(t *testing.T) { + testCases := []struct { + jsonFileName string + expectedPodName string + expectedContainerName string + expectedDebugPodAmount int + // expectedContainersDebugList []configsections.Container + }{ + { + jsonFileName: "testdata/pods_with_debug_label.json", + expectedPodName: "test-7dc8cf6b5f-2t4bn", + expectedContainerName: "test", + expectedDebugPodAmount: 1, + }, + { + jsonFileName: "testdata/empty.json", + expectedDebugPodAmount: 0, + }, + } + + // Spoof the executeOcGetCommand + origFunc := executeOcGetCommand + defer func() { + executeOcGetCommand = origFunc + }() + + for _, tc := range testCases { + tp := &configsections.TestPartner{} + + executeOcGetCommand = func(resourceType, labelQuery, namespace string) string { + output, _ := os.ReadFile(tc.jsonFileName) + return string(output) + } + + if tc.expectedDebugPodAmount > 0 { + FindDebugPods(tp) + assert.Len(t, tp.ContainersDebugList, 1) // Only assuming one debug pod in the test YAML + assert.Equal(t, tc.expectedPodName, tp.ContainersDebugList[0].PodName) + assert.Equal(t, tc.expectedContainerName, tp.ContainersDebugList[0].ContainerName) + } else { + assert.Panics(t, func() { FindDebugPods(tp) }) + } + } +} diff --git a/pkg/config/autodiscover/autodiscover_generic.go b/pkg/config/autodiscover/autodiscover_generic.go deleted file mode 100644 index fdb1c1ef0..000000000 --- a/pkg/config/autodiscover/autodiscover_generic.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package autodiscover - -import ( - "fmt" - - log "github.com/sirupsen/logrus" - "github.com/test-network-function/test-network-function/pkg/config/configsections" -) - -const ( - genericLabelName = "generic" - underTestValue = "target" - // partnerLabelValue = "partner" - orchestratorValue = "orchestrator" - fsDiffMasterValue = "fs_diff_master" - skipConnectivityTestsLabel = "skip_connectivity_tests" -) - -// BuildGenericConfig builds a `configsections.TestConfiguration` from the current state of the cluster, -// using labels and annotations to populate the data. -func BuildGenericConfig() (conf configsections.TestConfiguration) { - var partnerContainers []configsections.Container // PartnerContainers is built from all non-target containers - - // an orchestrator must be identified - orchestrator, err := getContainerByLabel(configsections.Label{Namespace: tnfNamespace, Name: genericLabelName, Value: orchestratorValue}) - if err != nil { - log.Fatalf("failed to identify a single test orchestrator container: %s", err) - } - partnerContainers = append(partnerContainers, orchestrator) - conf.TestOrchestrator = orchestrator.ContainerIdentifier - - // there must be containers to test - containersUnderTest, err := GetContainersByLabel(configsections.Label{Namespace: tnfNamespace, Name: genericLabelName, Value: underTestValue}) - if err != nil { - log.Fatalf("found no containers to test: %s", err) - } - conf.ContainersUnderTest = containersUnderTest - - // the FS Diff master container is optional - fsDiffMasterContainer, err := getContainerByLabel(configsections.Label{Namespace: tnfNamespace, Name: genericLabelName, Value: fsDiffMasterValue}) - if err == nil { - partnerContainers = append(partnerContainers, fsDiffMasterContainer) - conf.FsDiffMasterContainer = fsDiffMasterContainer.ContainerIdentifier - } else { - log.Warnf("an error (%s) occurred when getting the FS Diff Master Container. Attempting to continue", err) - } - - // Containers to exclude from connectivity tests are optional - connectivityExcludedContainers, err := getContainerIdentifiersByLabel(configsections.Label{Namespace: tnfNamespace, Name: skipConnectivityTestsLabel, Value: anyLabelValue}) - if err != nil { - log.Warnf("an error (%s) occurred when getting the containers to exclude from connectivity tests. Attempting to continue", err) - } - conf.ExcludeContainersFromConnectivityTests = connectivityExcludedContainers - - conf.PartnerContainers = partnerContainers - - return conf -} - -// GetContainersByLabel builds `config.Container`s from containers in pods matching a label. -func GetContainersByLabel(label configsections.Label) (containers []configsections.Container, err error) { - pods, err := GetPodsByLabel(label) - if err != nil { - return nil, err - } - for i := range pods.Items { - containers = append(containers, BuildContainersFromPodResource(&pods.Items[i])...) - } - return containers, nil -} - -// getContainerIdentifiersByLabel builds `config.ContainerIdentifier`s from containers in pods matching a label. -func getContainerIdentifiersByLabel(label configsections.Label) (containerIDs []configsections.ContainerIdentifier, err error) { - containers, err := GetContainersByLabel(label) - if err != nil { - return nil, err - } - for _, c := range containers { - containerIDs = append(containerIDs, c.ContainerIdentifier) - } - return containerIDs, nil -} - -// getContainerByLabel returns exactly one container with the given label. If any other number of containers is found -// then an error is returned along with an empty `config.Container`. -func getContainerByLabel(label configsections.Label) (container configsections.Container, err error) { - containers, err := GetContainersByLabel(label) - if err != nil { - return container, err - } - if len(containers) != 1 { - return container, fmt.Errorf("expected exactly one container, got %d for label %s/%s=%s", len(containers), label.Namespace, label.Name, label.Value) - } - return containers[0], nil -} - -// BuildContainersFromPodResource builds `configsections.Container`s from a `PodResource` -func BuildContainersFromPodResource(pr *PodResource) (containers []configsections.Container) { - for _, containerResource := range pr.Spec.Containers { - var err error - var container configsections.Container - container.Namespace = pr.Metadata.Namespace - container.PodName = pr.Metadata.Name - container.ContainerName = containerResource.Name - container.DefaultNetworkDevice, err = pr.getDefaultNetworkDeviceFromAnnotations() - if err != nil { - log.Warnf("error encountered getting default network device: %s", err) - } - container.MultusIPAddresses, err = pr.getPodIPs() - if err != nil { - log.Warnf("error encountered getting multus IPs: %s", err) - err = nil - } - - containers = append(containers, container) - } - return -} diff --git a/pkg/config/autodiscover/autodiscover_operator.go b/pkg/config/autodiscover/autodiscover_operator.go deleted file mode 100644 index 573da1e08..000000000 --- a/pkg/config/autodiscover/autodiscover_operator.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package autodiscover - -import ( - log "github.com/sirupsen/logrus" - "github.com/test-network-function/test-network-function/pkg/config/configsections" - "github.com/test-network-function/test-network-function/pkg/tnf/testcases" -) - -const ( - operatorLabelName = "operator" -) - -var ( - operatorTestsAnnotationName = buildAnnotationName("operator_tests") - subscriptionNameAnnotationName = buildAnnotationName("subscription_name") -) - -// BuildOperatorConfig builds a `[]configsections.Operator` from the current state of the cluster, -// using labels and annotations to populate the data. -func BuildOperatorConfig() (operatorsToTest []configsections.Operator) { - csvs, err := GetCSVsByLabel(operatorLabelName, anyLabelValue) - if err != nil { - log.Fatalf("found no CSVs to test while 'operator' spec enabled: %s", err) - } - for i := range csvs.Items { - operatorsToTest = append(operatorsToTest, BuildOperatorFromCSVResource(&csvs.Items[i])) - } - return operatorsToTest -} - -// BuildOperatorFromCSVResource builds a single `configsections.Operator` from a CSVResource -func BuildOperatorFromCSVResource(csv *CSVResource) (op configsections.Operator) { - var err error - op.Name = csv.Metadata.Name - op.Namespace = csv.Metadata.Namespace - - var tests []string - err = csv.GetAnnotationValue(operatorTestsAnnotationName, &tests) - if err != nil { - log.Warnf("unable to extract tests from annotation on '%s/%s' (error: %s). Attempting to fallback to all tests", op.Namespace, op.Name, err) - op.Tests = getConfiguredOperatorTests() - } else { - op.Tests = tests - } - - var subscriptionName string - err = csv.GetAnnotationValue(subscriptionNameAnnotationName, &subscriptionName) - if err != nil { - log.Warnf("unable to get a subscription name annotation from CSV %s (%s), the CSV name will be used", csv.Metadata.Name, err) - } - op.SubscriptionName = subscriptionName - - return op -} - -// getConfiguredOperatorTests loads the `configuredTestFile` used by the `operator` specs and extracts -// the names of test groups from it. -func getConfiguredOperatorTests() (opTests []string) { - configuredTests, err := testcases.LoadConfiguredTestFile(configuredTestFile) - if err != nil { - log.Errorf("failed to load %s, continuing with no tests", configuredTestFile) - return []string{} - } - for _, configuredTest := range configuredTests.OperatorTest { - opTests = append(opTests, configuredTest.Name) - } - log.WithField("opTests", opTests).Infof("got all tests from %s.", configuredTestFile) - return opTests -} diff --git a/pkg/config/autodiscover/autodiscover_targets.go b/pkg/config/autodiscover/autodiscover_targets.go new file mode 100644 index 000000000..54062893d --- /dev/null +++ b/pkg/config/autodiscover/autodiscover_targets.go @@ -0,0 +1,441 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package autodiscover + +import ( + "errors" + "fmt" + "strings" + "time" + + log "github.com/sirupsen/logrus" + "github.com/test-network-function/test-network-function/pkg/config/configsections" + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/nodenames" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" + "github.com/test-network-function/test-network-function/pkg/tnf/testcases" + "github.com/test-network-function/test-network-function/pkg/utils" +) + +const ( + operatorLabelName = "operator" + skipConnectivityTestsLabel = "skip_connectivity_tests" + skipMultusConnectivityTestsLabel = "skip_multus_connectivity_tests" + ocGetClusterCrdNamesCommand = "kubectl get crd -o json | jq '[.items[].metadata.name]'" + DefaultTimeout = 10 * time.Second +) + +var ( + operatorTestsAnnotationName = buildAnnotationName("operator_tests") + subscriptionNameAnnotationName = buildAnnotationName("subscription_name") + podTestsAnnotationName = buildAnnotationName("host_resource_tests") +) + +// FindTestTarget finds test targets from the current state of the cluster, +// using labels and annotations, and add them to the `configsections.TestTarget` passed in. +//nolint:funlen +func FindTestTarget(labels []configsections.Label, target *configsections.TestTarget, namespaces []string, skipHelmChartList []configsections.SkipHelmChartList) { + ns := make(map[string]bool) + for _, n := range namespaces { + ns[n] = true + } + for _, l := range labels { + pods, err := GetPodsByLabel(l) + if err == nil { + for _, pod := range pods.Items { + if ns[pod.Metadata.Namespace] { + target.PodsUnderTest = append(target.PodsUnderTest, buildPodUnderTest(pod)) + target.ContainerList = append(target.ContainerList, buildContainers(pod)...) + } else { + target.NonValidPods = append(target.NonValidPods, buildPodUnderTest(pod)) + } + } + } else { + log.Warnf("failed to query by label: %v %v", l, err) + } + } + // Containers to exclude from connectivity tests are optional + identifiers, err := getContainerIdentifiersByLabel(configsections.Label{Prefix: tnfLabelPrefix, Name: skipConnectivityTestsLabel, Value: anyLabelValue}) + if err != nil { + log.Warnf("an error (%s) occurred when getting the containers to exclude from Default connectivity tests. Attempting to continue", err) + } + for _, id := range identifiers { + if ns[id.Namespace] { + target.ExcludeContainersFromConnectivityTests = append(target.ExcludeContainersFromConnectivityTests, id) + } + } + identifiers, err = getContainerIdentifiersByLabel(configsections.Label{Prefix: tnfLabelPrefix, Name: skipMultusConnectivityTestsLabel, Value: anyLabelValue}) + if err != nil { + log.Warnf("an error (%s) occurred when getting the containers to exclude from Multus connectivity tests. Attempting to continue", err) + } + for _, id := range identifiers { + if ns[id.Namespace] { + target.ExcludeContainersFromMultusConnectivityTests = append(target.ExcludeContainersFromMultusConnectivityTests, id) + } + } + + csvs, err := GetCSVsByLabel(operatorLabelName, anyLabelValue) + if err != nil { + log.Warnf("an error (%s) occurred when looking for operators by label", err) + } + for _, csv := range csvs.Items { + if ns[csv.Metadata.Namespace] { + csv := csv + target.Operators = append(target.Operators, buildOperatorFromCSVResource(&csv, false)) + } + } + dps := FindTestPodSetsByLabel(labels, string(configsections.Deployment)) + target.DeploymentsUnderTest = appendPodsets(dps, ns) + stateFulSet := FindTestPodSetsByLabel(labels, string(configsections.StateFulSet)) + target.StateFulSetUnderTest = appendPodsets(stateFulSet, ns) + target.Nodes = GetNodesList() + target.HelmChart = GethelmCharts(skipHelmChartList, ns) +} +func GethelmCharts(skipHelmChartList []configsections.SkipHelmChartList, ns map[string]bool) (chartslist []configsections.HelmChart) { + charts, err := GetClusterHelmCharts() + if err != nil { + log.Errorf("Failed to get helm charts... is helm installed correctly? err: %s", err) + return nil + } + for _, ch := range charts.Items { + if ns[ch.Namespace] { + if !isSkipHelmChart(ch.Name, skipHelmChartList) { + name, version := getHelmNameVersion(ch.Chart) + chart := configsections.HelmChart{ + Version: version, + Name: name, + } + chartslist = append(chartslist, chart) + } + } + } + return chartslist +} + +// func to check if the helm is exist on the no need to check list that are under the tnf_config.yml +func isSkipHelmChart(helmName string, skipHelmChartList []configsections.SkipHelmChartList) bool { + if len(skipHelmChartList) == 0 { + return false + } + for _, helm := range skipHelmChartList { + if helmName == helm.Name { + log.Infof("Helm chart with name %s was skipped", helmName) + return true + } + } + return false +} + +// func to get the name and verstion need to split the number that have dots and the string valuse +// we could have a chart name like orion-ld-1.0.1 version=1.0.1 and name is orion-ld +func getHelmNameVersion(nameVersion string) (name, version string) { + nameversion := strings.Split(nameVersion, "-") + for k, val := range nameversion { + if strings.Contains(val, ".") { + version = val + continue + } + if k == 0 { + name = val + } else { + name = name + "-" + val + } + } + return name, version +} + +// func for appending the pod sets +func appendPodsets(podsets []configsections.PodSet, ns map[string]bool) (podSet []configsections.PodSet) { + for _, ps := range podsets { + if ns[ps.Namespace] { + podSet = append(podSet, ps) + } + } + return podSet +} + +// GetNodesList Function that return a list of node and what is the type of them. +func GetNodesList() (nodes map[string]configsections.Node) { + nodes = make(map[string]configsections.Node) + var nodeNames []string + context := interactive.GetContext(expectersVerboseModeEnabled) + tester := nodenames.NewNodeNames(DefaultTimeout, map[string]*string{configsections.MasterLabel: nil}) + test, _ := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) + _, err := test.Run() + if err != nil { + log.Error("Unable to get node list ", ". Error: ", err) + return + } + nodeNames = tester.GetNodeNames() + for i := range nodeNames { + nodes[nodeNames[i]] = configsections.Node{ + Name: nodeNames[i], + Labels: []string{configsections.MasterLabel}, + } + } + + tester = nodenames.NewNodeNames(DefaultTimeout, map[string]*string{configsections.WorkerLabel: nil}) + test, _ = tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) + _, err = test.Run() + if err != nil { + log.Error("Unable to get node list ", ". Error: ", err) + } else { + nodeNames = tester.GetNodeNames() + for i := range nodeNames { + if _, ok := nodes[nodeNames[i]]; ok { + var node = nodes[nodeNames[i]] + node.Labels = append(node.Labels, configsections.WorkerLabel) + nodes[nodeNames[i]] = node + } else { + nodes[nodeNames[i]] = configsections.Node{ + Name: nodeNames[i], + Labels: []string{configsections.WorkerLabel}, + } + } + } + } + + return nodes +} + +// FindTestPodSetsByLabel uses the containers' namespace to get its parent deployment/statefulset. Filters out non CNF test podsets,deployment/statefulset, +// currently partner and fs_diff ones. +func FindTestPodSetsByLabel(targetLabels []configsections.Label, resourceTypeDeployment string) (podsets []configsections.PodSet) { + configType := configsections.Deployment + if resourceTypeDeployment == string(configsections.StateFulSet) { + configType = configsections.StateFulSet + } + for _, label := range targetLabels { + podsetResourceList, err := GetTargetPodSetsByLabel(label, resourceTypeDeployment) + if err != nil { + log.Error("Unable to get deployment list Error: ", err) + } else { + for _, podsetResource := range podsetResourceList.Items { + podset := configsections.PodSet{ + Name: podsetResource.GetName(), + Namespace: podsetResource.GetNamespace(), + Replicas: podsetResource.GetReplicas(), + Hpa: podsetResource.GetHpa(), + Type: configType, + } + + podsets = append(podsets, podset) + } + } + } + return podsets +} + +// buildPodUnderTest builds a single `configsections.Pod` from a PodResource +func buildPodUnderTest(pr *PodResource) (podUnderTest *configsections.Pod) { + var err error + podUnderTest = &configsections.Pod{} + podUnderTest.Namespace = pr.Metadata.Namespace + podUnderTest.Name = pr.Metadata.Name + podUnderTest.ServiceAccount = pr.Spec.ServiceAccount + podUnderTest.ContainerCount = len(pr.Spec.Containers) + podUnderTest.DefaultNetworkDevice, err = pr.getDefaultNetworkDeviceFromAnnotations() + if err != nil { + log.Warnf("error encountered getting default network device: %s", err) + } + + podUnderTest.DefaultNetworkIPAddresses = pr.getDefaultPodIPAddresses() + + podUnderTest.MultusIPAddressesPerNet, err = pr.getPodIPsPerNet() + if err != nil { + log.Warnf("error encountered getting multus IPs: %s", err) + } + var tests []string + err = pr.GetAnnotationValue(podTestsAnnotationName, &tests) + if err != nil { + log.Warnf("unable to extract tests from annotation on '%s/%s' (error: %s). Attempting to fallback to all tests", podUnderTest.Namespace, podUnderTest.Name, err) + podUnderTest.Tests = testcases.GetConfiguredPodTests() + } else { + podUnderTest.Tests = tests + } + + if pr.Metadata.OwnerReferences != nil { + podUnderTest.IsManaged = true + } + + // Get a list of all the containers present in the pod + allContainersInPod := buildContainers(pr) + if len(allContainersInPod) > 0 { + // Pick the first container in the list to use as the network context + podUnderTest.ContainerList = allContainersInPod + } else { + log.Errorf("There are no containers in pod %s in namespace %s", podUnderTest.Name, podUnderTest.Namespace) + } + return podUnderTest +} + +// buildOperatorFromCSVResource builds a single `configsections.Operator` from a CSVResource +func buildOperatorFromCSVResource(csv *CSVResource, istest bool) (op *configsections.Operator) { + var err error + op = &configsections.Operator{} + op.Name = csv.Metadata.Name + op.Namespace = csv.Metadata.Namespace + + var tests []string + err = csv.GetAnnotationValue(operatorTestsAnnotationName, &tests) + if err != nil { + log.Warnf("unable to extract tests from annotation on '%s/%s' (error: %s). Attempting to fallback to all tests", op.Namespace, op.Name, err) + op.Tests = getConfiguredOperatorTests() + } else { + op.Tests = tests + } + + var subscriptionName []string + err = csv.GetAnnotationValue(subscriptionNameAnnotationName, &subscriptionName) + if err != nil { + log.Warnf("unable to get a subscription name annotation from CSV %s (error: %s).", csv.Metadata.Name, err) + } else { + op.SubscriptionName = subscriptionName[0] + } + if !istest { + op.InstallPlans, err = getCsvInstallPlans(op.Name, op.Namespace) + if err != nil { + log.Errorf("Failed to get operator bundle and index image for csv %s (ns %s), error: %s", op.Name, op.Namespace, err) + } + op.Packag, op.Org, op.Version = csv.PackOrgVersion(op.Name) + } + + return op +} + +// getConfiguredOperatorTests loads the `configuredTestFile` used by the `operator` specs and extracts +// the names of test groups from it. Returns slice of strings. +func getConfiguredOperatorTests() []string { + var opTests []string + configuredTests, err := testcases.LoadConfiguredTestFile(testcases.ConfiguredTestFile) + if err != nil { + log.Errorf("failed to load %s, continuing with no tests", testcases.ConfiguredTestFile) + return opTests + } + for _, configuredTest := range configuredTests.OperatorTest { + opTests = append(opTests, configuredTest.Name) + } + log.WithField("opTests", opTests).Infof("got all tests from %s.", testcases.ConfiguredTestFile) + return opTests +} + +var getCsvInstallPlanNames = func(csvName, csvNamespace string) ([]string, error) { + installPlanCmd := fmt.Sprintf("oc get installplan -n %s | grep %q | awk '{ print $1 }'", csvNamespace, csvName) + out := execCommandOutput(installPlanCmd) + if out == "" { + return []string{}, errors.New("installplan not found") + } + + return strings.Split(out, "\n"), nil +} + +var getInstallPlanData = func(installPlanName, namespace string) (bundleImagePath, catalogSource, catalogSourceNamespace string, err error) { + const installPlanNumFields = 3 + const bundleImageIndex = 0 + const catalogSourceIndex = 1 + const catalogSourceNamespaceIndex = 2 + + infoFromInstallPlanCmd := fmt.Sprintf("oc get installplan -n %s -o go-template="+ + "'{{range .items}}{{ if eq .metadata.name %q}}{{ range .status.bundleLookups }}"+ + "{{ .path }},{{ .catalogSourceRef.name }},{{ .catalogSourceRef.namespace }}{{end}}{{end}}{{end}}'", namespace, installPlanName) + + out := execCommandOutput(infoFromInstallPlanCmd) + installPlanFields := strings.Split(out, ",") + if len(installPlanFields) != installPlanNumFields { + return "", "", "", fmt.Errorf("invalid installplan info: %s", out) + } + + return installPlanFields[bundleImageIndex], installPlanFields[catalogSourceIndex], installPlanFields[catalogSourceNamespaceIndex], nil +} + +var getCatalogSourceImageIndex = func(catalogSourceName, catalogSourceNamespace string) (string, error) { + const nullOutput = "null" + indexImageCmd := fmt.Sprintf("oc get catalogsource -n %s %s -o json | jq -r .spec.image", catalogSourceNamespace, catalogSourceName) + indexImage := execCommandOutput(indexImageCmd) + if indexImage == "" { + return "", fmt.Errorf("failed to get index image for catalogsource %s (ns %s)", catalogSourceName, catalogSourceNamespace) + } + + // In case there wasn't a catalogsource for this installplan, jq will return null, so leave it empty. + if indexImage == nullOutput { + indexImage = "" + } + + return indexImage, nil +} + +// getCsvInstallPlans provides the bundle image and index image of each installplan for a given CSV. +// These variables are saved in the `configsections.Operator` in order to be used by DCI, +// which obtains them from the claim.json and provides them to preflight suite. +func getCsvInstallPlans(csvName, csvNamespace string) (installPlans []configsections.InstallPlan, err error) { + installPlanNames, err := getCsvInstallPlanNames(csvName, csvNamespace) + if err != nil { + return []configsections.InstallPlan{}, err + } + + for _, installPlanName := range installPlanNames { + bundleImage, catalogSourceName, catalogSourceNamespace, err := getInstallPlanData(installPlanName, csvNamespace) + if err != nil { + return []configsections.InstallPlan{}, err + } + + indexImage, err := getCatalogSourceImageIndex(catalogSourceName, catalogSourceNamespace) + if err != nil { + return []configsections.InstallPlan{}, err + } + + installPlans = append(installPlans, configsections.InstallPlan{Name: installPlanName, BundleImage: bundleImage, IndexImage: indexImage}) + } + + return installPlans, nil +} + +// getClusterCrdNames returns a list of crd names found in the cluster. +func getClusterCrdNames() ([]string, error) { + out := utils.ExecuteCommandAndValidate(ocGetClusterCrdNamesCommand, ocCommandTimeOut, interactive.GetContext(expectersVerboseModeEnabled), func() { + log.Error("can't run command: ", ocGetClusterCrdNamesCommand) + }) + + var crdNamesList []string + err := jsonUnmarshal([]byte(out), &crdNamesList) + if err != nil { + return nil, err + } + + return crdNamesList, nil +} + +// FindTestCrdNames gets a list of CRD names based on configured groups. +func FindTestCrdNames(crdFilters []configsections.CrdFilter) []string { + clusterCrdNames, err := getClusterCrdNames() + if err != nil { + log.Errorf("Unable to get cluster CRD.") + return []string{} + } + + var targetCrdNames []string + for _, crdName := range clusterCrdNames { + for _, crdFilter := range crdFilters { + if strings.HasSuffix(crdName, crdFilter.NameSuffix) { + targetCrdNames = append(targetCrdNames, crdName) + break + } + } + } + return targetCrdNames +} diff --git a/pkg/config/autodiscover/autodiscover_targets_test.go b/pkg/config/autodiscover/autodiscover_targets_test.go new file mode 100644 index 000000000..5fe221443 --- /dev/null +++ b/pkg/config/autodiscover/autodiscover_targets_test.go @@ -0,0 +1,518 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package autodiscover + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/config/configsections" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" + "github.com/test-network-function/test-network-function/pkg/utils" +) + +//nolint:funlen +func TestFindTestCrdNames(t *testing.T) { + // Simple function to check existence of a string in a slice + contains := func(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false + } + + testCases := []struct { + crdFilters []configsections.CrdFilter + badUnmarshal bool + expectedCRDs []string + }{ + { + crdFilters: []configsections.CrdFilter{ + { + NameSuffix: "metal3.io", + }, + }, + expectedCRDs: []string{ + "provisionings.metal3.io", + "baremetalhosts.metal3.io", + }, + badUnmarshal: false, + }, + { + crdFilters: []configsections.CrdFilter{ + { + NameSuffix: "k8s.io", + }, + }, + expectedCRDs: []string{ + "storagestates.migration.k8s.io", + "storageversionmigrations.migration.k8s.io", + }, + badUnmarshal: false, + }, + { // fail to unmarshal the JSON correctly + crdFilters: []configsections.CrdFilter{ + { + NameSuffix: "k8s.io", + }, + }, + expectedCRDs: []string{}, + badUnmarshal: true, + }, + } + + // Spoof the executeCommand func + origFunc := utils.ExecuteCommandAndValidate + utils.ExecuteCommandAndValidate = func(command string, timeout time.Duration, context *interactive.Context, failureCallbackFun func()) string { + fileContents, err := os.ReadFile("testdata/crd_output.json") + assert.Nil(t, err) + return string(fileContents) + } + + for _, tc := range testCases { + if tc.badUnmarshal { + jsonUnmarshal = func(data []byte, v interface{}) error { + return errors.New("this is an error") + } + } + + // Compare the expected to the actual + output := FindTestCrdNames(tc.crdFilters) + for _, i := range tc.expectedCRDs { + assert.True(t, contains(output, i)) + } + } + + utils.ExecuteCommandAndValidate = origFunc + jsonUnmarshal = json.Unmarshal +} + +func TestGetConfiguredOperatorTests(t *testing.T) { + // Note: Without testconfigure.yml being set, this only + // covers a subset of the code in the function. + opTests := getConfiguredOperatorTests() + assert.Nil(t, opTests) +} + +func TestAppendPodsets(t *testing.T) { + testCases := []struct { + podsets []configsections.PodSet + namespaces map[string]bool + expectedPodSets []configsections.PodSet + }{ + { + podsets: []configsections.PodSet{ + { + Name: "testpod1", + Namespace: "namespace1", + }, + }, + namespaces: map[string]bool{ + "namespace1": true, + }, + expectedPodSets: []configsections.PodSet{ + { + Name: "testpod1", + Namespace: "namespace1", + }, + }, + }, + { // 'namespace1' does not exist, no podset available. + podsets: []configsections.PodSet{ + { + Name: "testpod1", + Namespace: "namespace1", + }, + }, + namespaces: map[string]bool{ + "namespace2": true, + }, + expectedPodSets: nil, + }, + } + + for _, tc := range testCases { + assert.Equal(t, tc.expectedPodSets, appendPodsets(tc.podsets, tc.namespaces)) + } +} + +//nolint:funlen +func TestFindTestPodSetsByLabel(t *testing.T) { + testCases := []struct { + targetLabels []configsections.Label + resourceTypeDeployment string + filename string + expectedPodSets []configsections.PodSet + }{ + { // Test Case 1 - nothing found + targetLabels: []configsections.Label{ + { + Name: "label1", + Value: "value1", + }, + }, + resourceTypeDeployment: string(configsections.Deployment), + filename: "testdata/empty.json", + expectedPodSets: nil, + }, + { // Test Case 2 - Found one deployment matching labels + targetLabels: []configsections.Label{ + { + Name: "app", + Value: "mydeploy", + }, + }, + resourceTypeDeployment: string(configsections.Deployment), + filename: "testdata/test_deploy_matching_label.json", + expectedPodSets: []configsections.PodSet{ + { + Name: "mydeploy", + Namespace: "default", + Type: configsections.Deployment, + }, + }, + }, + } + + for _, tc := range testCases { + // spoof the output from execCommandOutput + origFunc := execCommandOutput + execCommandOutput = func(command string) string { + output, err := os.ReadFile(tc.filename) + assert.Nil(t, err) + return string(output) + } + + podsets := FindTestPodSetsByLabel(tc.targetLabels, tc.resourceTypeDeployment) + + if len(tc.expectedPodSets) > 0 { + // Note: We are assuming that [0] is populated with the data we need. + assert.Equal(t, tc.expectedPodSets[0].Name, podsets[0].Name) + assert.Equal(t, tc.expectedPodSets[0].Namespace, podsets[0].Namespace) + assert.Equal(t, tc.expectedPodSets[0].Type, podsets[0].Type) + } else { + assert.Nil(t, podsets) + } + + execCommandOutput = origFunc + } +} + +//nolint: funlen +func TestGetCsvInstallPlanNames(t *testing.T) { + originalExecCommandOutput := execCommandOutput + defer func() { + execCommandOutput = originalExecCommandOutput + }() + + testCases := []struct { + csvName string + csvNamespace string + expectedError string + expectedPlanNames []string + mockedExecCommandOutputFunc func(cmd string) string + }{ + { + csvName: "csvexample1", + csvNamespace: "csvns1", + expectedError: "", + expectedPlanNames: []string{"installPlan1"}, + mockedExecCommandOutputFunc: func(cmd string) string { + return "installPlan1" + }, + }, + { + csvName: "csvexample1", + csvNamespace: "csvns1", + expectedPlanNames: []string{"installPlan1", "installPlan2"}, + mockedExecCommandOutputFunc: func(cmd string) string { + return "installPlan1\ninstallPlan2" + }, + }, + { + csvName: "csvexample1", + csvNamespace: "csvns1", + expectedError: "installplan not found", + expectedPlanNames: []string{}, + mockedExecCommandOutputFunc: func(cmd string) string { + return "" + }, + }, + } + + for _, tc := range testCases { + execCommandOutput = tc.mockedExecCommandOutputFunc + planNames, err := getCsvInstallPlanNames(tc.csvName, tc.csvNamespace) + if tc.expectedError != "" { + assert.NotNil(t, err) + assert.Equal(t, err.Error(), tc.expectedError) + } else { + assert.Nil(t, err) + } + assert.Equal(t, planNames, tc.expectedPlanNames) + } +} + +//nolint:funlen +func TestGetInstallPlanData(t *testing.T) { + originalExecCommandOutput := execCommandOutput + defer func() { + execCommandOutput = originalExecCommandOutput + }() + + testCases := []struct { + installPlanName string + namespace string + expectedError string + expectedBundleImage string + expectedCatalogSourceName string + expectedCatalogSourceNamespace string + mockedExecCommandOutputFunc func(cmd string) string + }{ + { + installPlanName: "install-1", + namespace: "ns1", + expectedError: "", + expectedBundleImage: "http://bundle-csvexample1-in-csvns1:sha", + expectedCatalogSourceName: "catalogName1", + expectedCatalogSourceNamespace: "catalogNamespace1", + mockedExecCommandOutputFunc: func(cmd string) string { + return "http://bundle-csvexample1-in-csvns1:sha,catalogName1,catalogNamespace1" + }, + }, + { + installPlanName: "install-2", + namespace: "ns1", + expectedError: "invalid installplan info: invalid-output", + expectedBundleImage: "", + expectedCatalogSourceName: "", + expectedCatalogSourceNamespace: "", + mockedExecCommandOutputFunc: func(cmd string) string { + return "invalid-output" + }, + }, + } + + for _, tc := range testCases { + execCommandOutput = tc.mockedExecCommandOutputFunc + bundleImage, catalogName, catalogNamespace, err := getInstallPlanData(tc.installPlanName, tc.namespace) + if tc.expectedError != "" { + assert.NotNil(t, err) + assert.Equal(t, err.Error(), tc.expectedError) + } else { + assert.Nil(t, err) + } + + assert.Equal(t, bundleImage, tc.expectedBundleImage) + assert.Equal(t, catalogName, tc.expectedCatalogSourceName) + assert.Equal(t, catalogNamespace, tc.expectedCatalogSourceNamespace) + } +} + +//nolint:funlen +func TestGetCatalogSourceImageIndex(t *testing.T) { + originalExecCommandOutput := execCommandOutput + defer func() { + execCommandOutput = originalExecCommandOutput + }() + + testCases := []struct { + catalogName string + catalogNamespace string + expectedError string + expectedImageIndex string + mockedExecCommandOutputFunc func(cmd string) string + }{ + { + catalogName: "catalogName1", + catalogNamespace: "ns1", + expectedError: "", + expectedImageIndex: "http://index1-csvexample2-in-csvns2:sha", + mockedExecCommandOutputFunc: func(cmd string) string { + return "http://index1-csvexample2-in-csvns2:sha" + }, + }, + { + catalogName: "catalogName2", + catalogNamespace: "ns1", + expectedError: "", + expectedImageIndex: "", + mockedExecCommandOutputFunc: func(cmd string) string { + return "null" + }, + }, + { + catalogName: "catalogName3", + catalogNamespace: "ns3", + expectedError: "failed to get index image for catalogsource catalogName3 (ns ns3)", + expectedImageIndex: "", + mockedExecCommandOutputFunc: func(cmd string) string { + return "" + }, + }, + } + + for _, tc := range testCases { + execCommandOutput = tc.mockedExecCommandOutputFunc + imageIndex, err := getCatalogSourceImageIndex(tc.catalogName, tc.catalogNamespace) + if tc.expectedError != "" { + assert.NotNil(t, err) + assert.Equal(t, err.Error(), tc.expectedError) + } else { + assert.Nil(t, err) + } + + assert.Equal(t, imageIndex, tc.expectedImageIndex) + } +} + +//nolint:funlen +func TestGetCsvInstallPlans(t *testing.T) { + // Save the original functions and defer its restoration. + originalGetCsvInstallPlanNames := getCsvInstallPlanNames + defer func() { + getCsvInstallPlanNames = originalGetCsvInstallPlanNames + }() + + originalGetInstallPlanData := getInstallPlanData + defer func() { + getInstallPlanData = originalGetInstallPlanData + }() + + originalGetCatalogSourceImageIndex := getCatalogSourceImageIndex + defer func() { + getCatalogSourceImageIndex = originalGetCatalogSourceImageIndex + }() + + // installPlanIndex is a helper index for the mocking functions to allow + // the testing of several installPlans + installPlanIndex := 0 + testCases := []struct { + csvName string + csvNamespace string + mockedGetCsvInstallPlanNames func(csvName string, csvNamespace string) ([]string, error) + mockedGetInstallPlanData func(installPlanName string, namespace string) (bundleImagePath string, catalogSource string, catalogSourceNamespace string, err error) + mockedGetCatalogSourceImageIndex func(catalogSourceName string, catalogSourceNamespace string) (string, error) + expectedError string + expectedInstallPlans []configsections.InstallPlan + }{ + // Positive TCs: + { + csvName: "csvexample1", + csvNamespace: "csvns1", + mockedGetCsvInstallPlanNames: func(csvName string, csvNamespace string) ([]string, error) { + return []string{"install-1"}, nil + }, + mockedGetInstallPlanData: func(installPlanName string, namespace string) (string, string, string, error) { + return "http://bundle1-csvexample1-in-csvns1:sha", "catalogName1", "catalogNamespace1", nil + }, + mockedGetCatalogSourceImageIndex: func(catalogSourceName string, catalogSourceNamespace string) (string, error) { + return "http://index-csvexample1-in-csvns1:sha", nil + }, + expectedError: "", + expectedInstallPlans: []configsections.InstallPlan{{Name: "install-1", BundleImage: "http://bundle1-csvexample1-in-csvns1:sha", IndexImage: "http://index-csvexample1-in-csvns1:sha"}}, + }, + { + csvName: "csvexample2", + csvNamespace: "csvns2", + mockedGetCsvInstallPlanNames: func(csvName string, csvNamespace string) ([]string, error) { + return []string{"install-1", "install-2"}, nil + }, + mockedGetInstallPlanData: func(installPlanName string, namespace string) (string, string, string, error) { + if installPlanIndex == 0 { + return "http://bundle1-csvexample2-in-csvns2:sha", "catalogName1", "catalogNamespace1", nil + } + return "http://bundle2-csvexample2-in-csvns2:sha", "catalogName1", "catalogNamespace1", nil + }, + mockedGetCatalogSourceImageIndex: func(catalogSourceName string, catalogSourceNamespace string) (string, error) { + if installPlanIndex == 0 { + installPlanIndex++ + return "http://index1-csvexample2-in-csvns2:sha", nil + } + return "http://index2-csvexample2-in-csvns2:sha", nil + }, + expectedError: "", + expectedInstallPlans: []configsections.InstallPlan{ + {Name: "install-1", BundleImage: "http://bundle1-csvexample2-in-csvns2:sha", IndexImage: "http://index1-csvexample2-in-csvns2:sha"}, + {Name: "install-2", BundleImage: "http://bundle2-csvexample2-in-csvns2:sha", IndexImage: "http://index2-csvexample2-in-csvns2:sha"}, + }, + }, + // Error checking TCs: + { + // No installPlan found for given CSV. + csvName: "csvexample5", + csvNamespace: "csvns5", + mockedGetCsvInstallPlanNames: func(csvName string, csvNamespace string) ([]string, error) { + return []string{}, errors.New("installplan not found") + }, + expectedError: "installplan not found", + expectedInstallPlans: []configsections.InstallPlan{}, + }, + { + // Invalid output when getting installPlan data. + csvName: "csvexample4", + csvNamespace: "csvns4", + mockedGetCsvInstallPlanNames: func(csvName string, csvNamespace string) ([]string, error) { + return []string{"install-1"}, nil + }, + mockedGetInstallPlanData: func(installPlanName string, namespace string) (string, string, string, error) { + return "", "", "", errors.New("invalid installplan info: invalid-output") + }, + expectedError: "invalid installplan info: invalid-output", + expectedInstallPlans: []configsections.InstallPlan{}, + }, + { + // Empty output when retrieving image index from catalog source. + csvName: "csvexample3", + csvNamespace: "csvns3", + mockedGetCsvInstallPlanNames: func(csvName string, csvNamespace string) ([]string, error) { + return []string{"install-1"}, nil + }, + mockedGetInstallPlanData: func(installPlanName string, namespace string) (string, string, string, error) { + return "http://bundle1-csvexample3-in-csvns3:sha", "catalogName1", "catalogNamespace1", nil + }, + mockedGetCatalogSourceImageIndex: func(catalogSourceName string, catalogSourceNamespace string) (string, error) { + return "", fmt.Errorf("failed to get index image for catalogsource %s (ns %s)", catalogSourceName, catalogSourceNamespace) + }, + expectedError: "failed to get index image for catalogsource catalogName1 (ns catalogNamespace1)", + expectedInstallPlans: []configsections.InstallPlan{}, + }, + } + + for _, tc := range testCases { + getCsvInstallPlanNames = tc.mockedGetCsvInstallPlanNames + getInstallPlanData = tc.mockedGetInstallPlanData + getCatalogSourceImageIndex = tc.mockedGetCatalogSourceImageIndex + + installPlans, err := getCsvInstallPlans(tc.csvName, tc.csvNamespace) + if tc.expectedError != "" { + assert.NotNil(t, err) + assert.Equal(t, err.Error(), tc.expectedError) + } else { + assert.Nil(t, err) + } + + assert.Equal(t, installPlans, tc.expectedInstallPlans) + } +} diff --git a/pkg/config/autodiscover/autodiscover_test.go b/pkg/config/autodiscover/autodiscover_test.go new file mode 100644 index 000000000..39d8db098 --- /dev/null +++ b/pkg/config/autodiscover/autodiscover_test.go @@ -0,0 +1,236 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package autodiscover + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/config/configsections" +) + +func TestBuildLabelQuery(t *testing.T) { + testCases := []struct { + testLabel configsections.Label + expectedOutput string + }{ + { + testLabel: configsections.Label{ + Prefix: "testprefix", + Name: "testname", + Value: "testvalue", + }, + expectedOutput: "testprefix/testname=testvalue", + }, + { + testLabel: configsections.Label{ + Prefix: "testprefix", + Name: "testname", + Value: "", // empty value + }, + expectedOutput: "testprefix/testname", + }, + { + testLabel: configsections.Label{ + Prefix: "", // empty value + Name: "testname", + Value: "testvalue", + }, + expectedOutput: "testname=testvalue", + }, + } + + for _, tc := range testCases { + assert.Equal(t, tc.expectedOutput, buildLabelQuery(tc.testLabel)) + } +} + +//nolint:funlen +func TestGetContainersByLabel(t *testing.T) { + testCases := []struct { + expectedOutput []configsections.Container + prefix string + name string + value string + filename string + }{ + { + prefix: "testprefix", + name: "testname", + value: "testvalue", + filename: "testdata/testpods_withlabel.json", + expectedOutput: []configsections.Container{ + { + ContainerIdentifier: configsections.ContainerIdentifier{ + Namespace: "kube-system", + PodName: "coredns-78fcd69978-cc94v", + ContainerName: "coredns", + NodeName: "minikube", + ContainerUID: "cf794b9e8c2448815b8b5a47b354c9bf9414a04f6fa567ac3b059851ed6757ab", + ContainerRuntime: "docker", + }, + ImageSource: &configsections.ContainerImageSource{ + Registry: "k8s.gcr.io", + ContainerImageIdentifier: configsections.ContainerImageIdentifier{ + Repository: "coredns", + Name: "coredns", + Tag: "v1.8.4", + Digest: "", + }, + }, + }, + }, + }, + { + prefix: "test1", + name: "", + value: "", // no value + filename: "testdata/testpods_empty.json", + expectedOutput: []configsections.Container{}, + }, + } + origCommand := executeOcGetAllCommand + defer func() { + executeOcGetAllCommand = origCommand + }() + for _, tc := range testCases { + executeOcGetAllCommand = func(resourceType, labelQuery string) string { + file, _ := os.ReadFile(tc.filename) + return string(file) + } + containers, _ := getContainersByLabel(configsections.Label{ + Prefix: tc.prefix, + Name: tc.name, + Value: tc.value, + }) + assert.Equal(t, tc.expectedOutput, containers) + } +} + +func TestPerformAutoDiscovery(t *testing.T) { + defer os.Unsetenv(disableAutodiscoverEnvVar) + testCases := []struct { + autoDiscoverEnabled bool + }{ + {autoDiscoverEnabled: true}, + {autoDiscoverEnabled: false}, + } + + for _, tc := range testCases { + if !tc.autoDiscoverEnabled { + os.Setenv(disableAutodiscoverEnvVar, "true") + assert.False(t, PerformAutoDiscovery()) + } else { + os.Setenv(disableAutodiscoverEnvVar, "false") + assert.True(t, PerformAutoDiscovery()) + } + } +} + +//nolint:funlen +func TestGetContainerIdentifiersByLabel(t *testing.T) { + testCases := []struct { + expectedOutput []configsections.ContainerIdentifier + prefix string + name string + value string + filename string + }{ + { + expectedOutput: []configsections.ContainerIdentifier{ + { + Namespace: "kube-system", + PodName: "coredns-78fcd69978-cc94v", + ContainerName: "coredns", + NodeName: "minikube", + ContainerUID: "cf794b9e8c2448815b8b5a47b354c9bf9414a04f6fa567ac3b059851ed6757ab", + ContainerRuntime: "docker", + }, + }, + prefix: "testprefix", + name: "testname", + value: "testvalue", + filename: "testdata/testpods_withlabel.json", + }, + + { + prefix: "test1", + name: "", + value: "", // no value + filename: "testdata/testpods_empty.json", + expectedOutput: []configsections.ContainerIdentifier{}, + }, + } + + origCommand := executeOcGetAllCommand + defer func() { + executeOcGetAllCommand = origCommand + }() + + for _, tc := range testCases { + executeOcGetAllCommand = func(resourceType, labelQuery string) string { + file, _ := os.ReadFile(tc.filename) + return string(file) + } + + identifiers, err := getContainerIdentifiersByLabel(configsections.Label{ + Prefix: tc.prefix, + Name: tc.name, + Value: tc.value, + }) + + assert.Nil(t, err) + assert.Equal(t, tc.expectedOutput, identifiers) + } +} + +func TestBuildContainerImageSource(t *testing.T) { + testCases := []struct { + expectedOutput configsections.ContainerImageSource + url string + }{ + { + expectedOutput: configsections.ContainerImageSource{ + Registry: "k8s.gcr.io", + ContainerImageIdentifier: configsections.ContainerImageIdentifier{ + Repository: "coredns", + Name: "coredns", + Tag: "v1.8.0", + Digest: "", + }, + }, + url: "k8s.gcr.io/coredns/coredns:v1.8.0", + }, + { + expectedOutput: configsections.ContainerImageSource{ + Registry: "quay.io", + ContainerImageIdentifier: configsections.ContainerImageIdentifier{ + Repository: "rh-nfv-int", + Name: "testpmd-operator", + Tag: "", + Digest: "sha256:3e8fc703c71a7ccaca24b7312f8fcb3495370c46e7abc12975757b76430addf5", + }, + }, + url: "quay.io/rh-nfv-int/testpmd-operator@sha256:3e8fc703c71a7ccaca24b7312f8fcb3495370c46e7abc12975757b76430addf5", + }, + } + + for _, tc := range testCases { + assert.Equal(t, tc.expectedOutput, *(buildContainerImageSource(tc.url))) + } +} diff --git a/pkg/config/autodiscover/container_test.go b/pkg/config/autodiscover/container_test.go index d443a9455..5229fbb60 100644 --- a/pkg/config/autodiscover/container_test.go +++ b/pkg/config/autodiscover/container_test.go @@ -14,29 +14,20 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -package autodiscover_test +package autodiscover import ( "testing" "github.com/stretchr/testify/assert" - "github.com/test-network-function/test-network-function/pkg/config/autodiscover" ) -func TestBuildCnfFromPodResource(t *testing.T) { - orchestratorPodResource := loadPodResource(testOrchestratorFilePath) - orchestratorPod := autodiscover.BuildCnfFromPodResource(&orchestratorPodResource) +func TestBuildContainers(t *testing.T) { + subjectPod := loadPodResource(testSubjectFilePath) + subjectContainers := buildContainers(&subjectPod) + assert.Equal(t, 1, len(subjectContainers)) - subjectPodResource := loadPodResource(testSubjectFilePath) - subjectPod := autodiscover.BuildCnfFromPodResource(&subjectPodResource) - - assert.Equal(t, "tnf", orchestratorPod.Namespace) - assert.Equal(t, "I'mAPodName", orchestratorPod.Name) - assert.NotEqual(t, "I'mAContainer", orchestratorPod.Name) - // no tests set on pod and the config file will not be loaded from the unit test context: no tests should be set. - assert.Equal(t, []string{}, orchestratorPod.Tests) - - assert.Equal(t, "tnf", subjectPod.Namespace) - assert.Equal(t, "test", subjectPod.Name) - assert.Equal(t, []string{"OneTestName", "AnotherTestName"}, subjectPod.Tests) + assert.Equal(t, "tnf", subjectContainers[0].Namespace) + assert.Equal(t, "I'mAPodName", subjectContainers[0].PodName) + assert.Equal(t, "I'mAContainer", subjectContainers[0].ContainerName) } diff --git a/pkg/config/autodiscover/csv_info.go b/pkg/config/autodiscover/csv_info.go index cca9eb544..b43752daf 100644 --- a/pkg/config/autodiscover/csv_info.go +++ b/pkg/config/autodiscover/csv_info.go @@ -17,8 +17,10 @@ package autodiscover import ( - "encoding/json" "fmt" + "strings" + + log "github.com/sirupsen/logrus" "github.com/test-network-function/test-network-function/pkg/config/configsections" ) @@ -42,23 +44,34 @@ type CSVResource struct { } `json:"metadata"` } -func (csv *CSVResource) hasAnnotation(annotationKey string) (present bool) { - _, present = csv.Metadata.Annotations[annotationKey] - return +func (csv *CSVResource) hasAnnotation(annotationKey string) bool { + _, present := csv.Metadata.Annotations[annotationKey] + return present } // GetAnnotationValue will get the value stored in the given annotation and // Unmarshal it into the given var `v`. -func (csv *CSVResource) GetAnnotationValue(annotationKey string, v interface{}) (err error) { +func (csv *CSVResource) GetAnnotationValue(annotationKey string, v interface{}) error { if !csv.hasAnnotation(annotationKey) { return fmt.Errorf("failed to find annotation '%s' on CSV '%s/%s'", annotationKey, csv.Metadata.Namespace, csv.Metadata.Name) } val := csv.Metadata.Annotations[annotationKey] - err = json.Unmarshal([]byte(val), v) + err := jsonUnmarshal([]byte(val), v) if err != nil { return csv.annotationUnmarshalError(annotationKey, err) } - return + return err +} +func (csv *CSVResource) PackOrgVersion(subscription string) (org, packag, version string) { + ocCmd := fmt.Sprintf("oc get subscriptions.operators.coreos.com -A -o go-template='{{range .items}}{{if .status.installedCSV}}{{if eq .status.installedCSV %q}}{{.spec.source}} {{.status.currentCSV}}{{end}}{{end}}{{end}}'", subscription) + out := execCommandOutput(ocCmd) + orgNameVer := strings.Split(out, " ") + org = orgNameVer[0] + nameVersion := strings.SplitN(orgNameVer[1], ".", 2) //nolint:gomnd // ok + packag = orgNameVer[1] + version = nameVersion[1] + + return packag, org, version } func (csv *CSVResource) annotationUnmarshalError(annotationKey string, err error) error { @@ -66,18 +79,33 @@ func (csv *CSVResource) annotationUnmarshalError(annotationKey string, err error err, annotationKey, csv.Metadata.Namespace, csv.Metadata.Name) } -// GetCSVsByLabel will return all CSVs with a given label value. If `labelValue` is an empty string, all CSVs with that +// GetCSVsByLabelByNamespace will return all CSVs with a given label value. If `labelValue` is an empty string, all CSVs with that // label will be returned, regardless of the labels value. -func GetCSVsByLabel(labelName, labelValue string) (*CSVList, error) { - cmd := makeGetCommand(resourceTypeCSV, buildLabelQuery(configsections.Label{Namespace: tnfNamespace, Name: labelName, Value: labelValue})) +func GetCSVsByLabelByNamespace(labelName, labelValue, namespace string) (*CSVList, error) { + out := executeOcGetCommand(resourceTypeCSV, buildLabelQuery(configsections.Label{Prefix: tnfLabelPrefix, Name: labelName, Value: labelValue}), namespace) + + log.Debug("JSON output for all pods labeled with: ", labelName) + log.Debug("Command: ", out) - out, err := cmd.Output() + var csvList CSVList + err := jsonUnmarshal([]byte(out), &csvList) if err != nil { return nil, err } + return &csvList, nil +} + +// GetCSVsByLabel will return all CSVs with a given label value. If `labelValue` is an empty string, all CSVs with that +// label will be returned, regardless of the labels value. +func GetCSVsByLabel(labelName, labelValue string) (*CSVList, error) { + out := executeOcGetAllCommand(resourceTypeCSV, buildLabelQuery(configsections.Label{Prefix: tnfLabelPrefix, Name: labelName, Value: labelValue})) + + log.Debug("JSON output for all pods labeled with: ", labelName) + log.Debug("Command: ", out) + var csvList CSVList - err = json.Unmarshal(out, &csvList) + err := jsonUnmarshal([]byte(out), &csvList) if err != nil { return nil, err } diff --git a/pkg/config/autodiscover/csv_info_test.go b/pkg/config/autodiscover/csv_info_test.go index 0ac491260..5b873b7bb 100644 --- a/pkg/config/autodiscover/csv_info_test.go +++ b/pkg/config/autodiscover/csv_info_test.go @@ -14,17 +14,16 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -package autodiscover_test +package autodiscover import ( - "encoding/json" - "io/ioutil" + "errors" "log" + "os" "path" "testing" "github.com/stretchr/testify/assert" - "github.com/test-network-function/test-network-function/pkg/config/autodiscover" ) const ( @@ -35,12 +34,12 @@ var ( csvFilePath = path.Join(filePath, csvFile) ) -func loadCSVResource(filePath string) (csv autodiscover.CSVResource) { - contents, err := ioutil.ReadFile(filePath) +func loadCSVResource(filePath string) (csv CSVResource) { + contents, err := os.ReadFile(filePath) if err != nil { log.Fatalf("error (%s) loading CSVResource %s for testing", err, filePath) } - err = json.Unmarshal(contents, &csv) + err = jsonUnmarshal(contents, &csv) if err != nil { log.Fatalf("error (%s) loading CSVResource %s for testing", err, filePath) } @@ -59,3 +58,114 @@ func TestCSVGetAnnotationValue(t *testing.T) { assert.Equal(t, []string{"OPERATOR_STATUS", "ANOTHER_TEST"}, val) assert.Nil(t, err) } + +func TestAnnotationUnmarshalError(t *testing.T) { + testCases := []struct { + expectedString string + }{ + { + expectedString: "error (this is an error) attempting to unmarshal value of annotation 'testKey' on CSV 'testnamespace/testname'", + }, + } + + for _, tc := range testCases { + csvr := CSVResource{ + Metadata: struct { + Name string "json:\"name\"" + Namespace string "json:\"namespace\"" + Labels map[string]string "json:\"labels\"" + Annotations map[string]string "json:\"annotations\"" + }{ + Name: "testname", + Namespace: "testnamespace", + }, + } + assert.Equal(t, tc.expectedString, csvr.annotationUnmarshalError("testKey", errors.New("this is an error")).Error()) + } +} + +func TestGetCSVsByLabel(t *testing.T) { + testCases := []struct { + filename string + label string + value string + expectedCSV string + expectedItemCount int + }{ + { + filename: "csv_output.json", + expectedCSV: "etcdoperator.v0.9.4", + label: "testLabel", + value: "testValue", + expectedItemCount: 1, + }, + { + filename: "csv_output_nolabel.json", + expectedCSV: "", + label: "testLabel", + value: "testValue", + expectedItemCount: 0, + }, + } + + for _, tc := range testCases { + origFunc := executeOcGetAllCommand + executeOcGetAllCommand = func(resourceType, labelQuery string) string { + output, err := os.ReadFile(path.Join(filePath, tc.filename)) + assert.Nil(t, err) + return string(output) + } + + outputList, err := GetCSVsByLabel(tc.label, tc.value) + assert.Nil(t, err) + assert.Equal(t, tc.expectedItemCount, len(outputList.Items)) + if len(outputList.Items) > 0 { + assert.Equal(t, tc.expectedCSV, outputList.Items[0].Metadata.Name) + } + + executeOcGetAllCommand = origFunc + } +} + +func TestGetCSVsByNamespace(t *testing.T) { + testCases := []struct { + filename string + label string + value string + expectedCSV string + expectedItemCount int + }{ + { + filename: "csv_output.json", + expectedCSV: "etcdoperator.v0.9.4", + label: "testLabel", + value: "testValue", + expectedItemCount: 1, + }, + { + filename: "csv_output_nolabel.json", + expectedCSV: "", + label: "testLabel", + value: "testValue", + expectedItemCount: 0, + }, + } + + for _, tc := range testCases { + origFunc := executeOcGetAllCommand + executeOcGetCommand = func(resourceType, labelQuery, namespace string) string { + output, err := os.ReadFile(path.Join(filePath, tc.filename)) + assert.Nil(t, err) + return string(output) + } + + outputList, err := GetCSVsByLabelByNamespace(tc.label, tc.value, "testnamespace") + assert.Nil(t, err) + assert.Equal(t, tc.expectedItemCount, len(outputList.Items)) + if len(outputList.Items) > 0 { + assert.Equal(t, tc.expectedCSV, outputList.Items[0].Metadata.Name) + } + + executeOcGetAllCommand = origFunc + } +} diff --git a/pkg/config/autodiscover/generic_test.go b/pkg/config/autodiscover/generic_test.go deleted file mode 100644 index 568ace234..000000000 --- a/pkg/config/autodiscover/generic_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package autodiscover_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/test-network-function/test-network-function/pkg/config/autodiscover" -) - -func TestBuildContainersFromPodResource(t *testing.T) { - orchestratorPod := loadPodResource(testOrchestratorFilePath) - orchestratorContainers := autodiscover.BuildContainersFromPodResource(&orchestratorPod) - assert.Equal(t, 1, len(orchestratorContainers)) - - subjectPod := loadPodResource(testSubjectFilePath) - subjectContainers := autodiscover.BuildContainersFromPodResource(&subjectPod) - assert.Equal(t, 1, len(subjectContainers)) - - assert.Equal(t, "tnf", orchestratorContainers[0].Namespace) - assert.Equal(t, "I'mAPodName", orchestratorContainers[0].PodName) - assert.Equal(t, "I'mAContainer", orchestratorContainers[0].ContainerName) - - // Check correct order of precedence for network devices - assert.Equal(t, "eth0", orchestratorContainers[0].DefaultNetworkDevice) - assert.NotEqual(t, "LowerPriorityInterface", orchestratorContainers[0].DefaultNetworkDevice) - assert.Equal(t, "eth1", subjectContainers[0].DefaultNetworkDevice) - - // Check correct IPs are chosen - assert.Equal(t, 1, len(orchestratorContainers[0].MultusIPAddresses)) - assert.Equal(t, "1.1.1.1", orchestratorContainers[0].MultusIPAddresses[0]) - assert.NotEqual(t, "2.2.2.2", orchestratorContainers[0].MultusIPAddresses[0]) - // test-network-function.com/multusips should be used for the test subject container. - assert.Equal(t, 2, len(subjectContainers[0].MultusIPAddresses)) - assert.Equal(t, "3.3.3.3", subjectContainers[0].MultusIPAddresses[0]) - assert.Equal(t, "4.4.4.4", subjectContainers[0].MultusIPAddresses[1]) -} diff --git a/pkg/config/autodiscover/helm_info.go b/pkg/config/autodiscover/helm_info.go new file mode 100644 index 000000000..ec27a917c --- /dev/null +++ b/pkg/config/autodiscover/helm_info.go @@ -0,0 +1,32 @@ +package autodiscover + +import ( + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" + "github.com/test-network-function/test-network-function/pkg/utils" +) + +type HelmSetList struct { + Items []HelmChart `json:""` +} +type HelmChart struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Chart string `json:"chart"` +} + +func GetClusterHelmCharts() (*HelmSetList, error) { + var helmList HelmSetList + + out, err := utils.ExecuteCommand("helm list -A -o json", ocCommandTimeOut, interactive.GetContext(expectersVerboseModeEnabled)) + if err != nil { + return &helmList, err + } + + if out != "" { + err := jsonUnmarshal([]byte(out), &helmList.Items) + if err != nil { + return nil, err + } + } + return &helmList, nil +} diff --git a/pkg/config/autodiscover/helm_info_test.go b/pkg/config/autodiscover/helm_info_test.go new file mode 100644 index 000000000..754df92ee --- /dev/null +++ b/pkg/config/autodiscover/helm_info_test.go @@ -0,0 +1,82 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package autodiscover + +import ( + "encoding/json" + "errors" + "os" + "path" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" + "github.com/test-network-function/test-network-function/pkg/utils" +) + +const ( + testHelmChart = "testhelmchart.json" +) + +var ( + testHelmChartPath = path.Join(filePath, testHelmChart) +) + +func TestGetClusterHelmCharts(t *testing.T) { + testCases := []struct { + badJSONUnmarshal bool + jsonErr error + }{ + { // no failures + badJSONUnmarshal: false, + }, + { // failure to jsonUnmarshal + badJSONUnmarshal: true, + jsonErr: errors.New("this is an error"), + }, + } + + for _, tc := range testCases { + // Setup the mock functions + utils.ExecuteCommand = func(command string, timeout time.Duration, context *interactive.Context) (string, error) { + contents, err := os.ReadFile(testHelmChartPath) + assert.Nil(t, err) + return string(contents), nil + } + if tc.badJSONUnmarshal { + jsonUnmarshal = func(data []byte, v interface{}) error { + return tc.jsonErr + } + } else { + // use the "real" function + jsonUnmarshal = json.Unmarshal + } + + // Run the function and compare the list output + list, err := GetClusterHelmCharts() + if !tc.badJSONUnmarshal { + assert.NotNil(t, list) + assert.Equal(t, "my-test1", list.Items[0].Name) + } + + if tc.badJSONUnmarshal { + assert.NotNil(t, err) + jsonUnmarshal = json.Unmarshal + } + } +} diff --git a/pkg/config/autodiscover/operator_test.go b/pkg/config/autodiscover/operator_test.go index 166a961fd..f2f8eabbe 100644 --- a/pkg/config/autodiscover/operator_test.go +++ b/pkg/config/autodiscover/operator_test.go @@ -14,18 +14,17 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -package autodiscover_test +package autodiscover import ( "testing" "github.com/stretchr/testify/assert" - "github.com/test-network-function/test-network-function/pkg/config/autodiscover" ) func TestBuildOperatorFromCSVResource(t *testing.T) { csvResource := loadCSVResource(csvFilePath) - operator := autodiscover.BuildOperatorFromCSVResource(&csvResource) + operator := buildOperatorFromCSVResource(&csvResource, true) assert.Equal(t, "CSVNamespace", operator.Namespace) assert.Equal(t, "CSVName", operator.Name) diff --git a/pkg/config/autodiscover/pod_info.go b/pkg/config/autodiscover/pod_info.go index 30d7260cc..d80c3aa4d 100644 --- a/pkg/config/autodiscover/pod_info.go +++ b/pkg/config/autodiscover/pod_info.go @@ -17,7 +17,6 @@ package autodiscover import ( - "encoding/json" "fmt" log "github.com/sirupsen/logrus" @@ -26,36 +25,49 @@ import ( const ( cnfDefaultNetworkInterfaceKey = "defaultnetworkinterface" - cnfIPsKey = "multusips" cniNetworksStatusKey = "k8s.v1.cni.cncf.io/networks-status" resourceTypePods = "pods" + podPhaseRunning = "Running" ) var ( namespacedDefaultNetworkInterfaceKey = buildAnnotationName(cnfDefaultNetworkInterfaceKey) - namespacedIPsKey = buildAnnotationName(cnfIPsKey) ) // PodList holds the data from an `oc get pods -o json` command type PodList struct { - Items []PodResource `json:"items"` + Items []*PodResource `json:"items"` } // PodResource is a single Pod from an `oc get pods -o json` command type PodResource struct { Metadata struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Labels map[string]string `json:"labels"` - Annotations map[string]string `json:"annotations"` + Name string `json:"name"` + Namespace string `json:"namespace"` + DeletionTimestamp string `json:"deletionTimestamp"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` + OwnerReferences []map[string]interface{} `json:"ownerReferences"` } `json:"metadata"` Spec struct { - Containers []struct { - Name string `json:"name"` + ServiceAccount string `json:"serviceaccountname"` + Containers []struct { + Name string `json:"name"` + Image string `json:"image"` } `json:"containers"` + NodeName string `json:"nodeName"` } `json:"spec"` Status struct { - PodIPs []map[string]string `json:"podIPs"` + // PodIPs this is currently unused, but part of the oc get output + // The listof IPs is contained in the Metadata->Annotations section + // of this structure. This is a list of ips with the following format: + // [0]:map[string]string ["ip": "10.130.0.65", ] + PodIPs []map[string]string `json:"podIPs"` + Phase string `json:"phase"` + ContainerStatuses []struct { + Name string `json:"name"` + ContainerID string `json:"containerID"` + } `json:"containerStatuses"` } `json:"status"` } @@ -79,7 +91,7 @@ func (pr *PodResource) GetAnnotationValue(annotationKey string, v interface{}) ( return fmt.Errorf("failed to find annotation '%s' on pod '%s/%s'", annotationKey, pr.Metadata.Namespace, pr.Metadata.Name) } val := pr.Metadata.Annotations[annotationKey] - err = json.Unmarshal([]byte(val), v) + err = jsonUnmarshal([]byte(val), v) if err != nil { return pr.annotationUnmarshalError(annotationKey, err) } @@ -90,16 +102,17 @@ func (pr *PodResource) GetAnnotationValue(annotationKey string, v interface{}) ( // First, if the cnf-certification-specific annotation "test-network-function.com/defaultnetworkinterface" is present // then the value of that will be decoded and returned. It must be a single JSON-encoded string. // Next, if the "k8s.v1.cni.cncf.io/networks-status" annotation is present then the first entry where `default == true` -// will be used. Note that this annotation may not be present outside OpenShift. -func (pr *PodResource) getDefaultNetworkDeviceFromAnnotations() (iface string, err error) { +// will be used. Note that this annotation may not be present outside OpenShift. Returns (interface, error). +func (pr *PodResource) getDefaultNetworkDeviceFromAnnotations() (string, error) { // Note: The `GetAnnotationValue` method does not distinguish between bad encoding and a missing annotation, which is needed here. + var iface string if val, present := pr.Metadata.Annotations[namespacedDefaultNetworkInterfaceKey]; present { - err = json.Unmarshal([]byte(val), &iface) - return + err := jsonUnmarshal([]byte(val), &iface) + return iface, err } if val, present := pr.Metadata.Annotations[cniNetworksStatusKey]; present { var cniInfo []cniNetworkInterface - err = json.Unmarshal([]byte(val), &cniInfo) + err := jsonUnmarshal([]byte(val), &cniInfo) if err != nil { return "", pr.annotationUnmarshalError(cniNetworksStatusKey, err) } @@ -112,34 +125,42 @@ func (pr *PodResource) getDefaultNetworkDeviceFromAnnotations() (iface string, e return "", fmt.Errorf("unable to determine a default network interface for %s/%s", pr.Metadata.Namespace, pr.Metadata.Name) } -// getPodIPs gets the IPs of a pod. -// In precedence, it uses the cnf-certification specific annotation if it is present. If set, -// "test-network-function.com/multusips" must be a json-encoded list of string IPs. -// The fallback option is the -// CNI annotation "k8s.v1.cni.cncf.io/networks-status". If neither are available, then `pod.status.ips` is used, though -// this may not contain all IPs in all cases and will not be _only_ multus IPs. -func (pr *PodResource) getPodIPs() (ips []string, err error) { - // Note: The `GetAnnotationValue` method does not distinguish between bad encoding and a missing annotation, which is needed here. - if val, present := pr.Metadata.Annotations[namespacedIPsKey]; present { - err = json.Unmarshal([]byte(val), &ips) - return - } +// getPodIPsPerNet gets the IPs of a pod. +// CNI annotation "k8s.v1.cni.cncf.io/networks-status". +// Returns (ips, error). +func (pr *PodResource) getPodIPsPerNet() (map[string][]string, error) { + // This is a map indexed with the network name (network attachment) and + // listing all the IPs created in this subnet and belonging to the pod namespace + // The list of ips pr net is parsed from the content of the "k8s.v1.cni.cncf.io/networks-status" annotation. + // see file pkg/config/autodiscover/testdata/testtarget.json for an example of such annotation + ips := make(map[string][]string) + if val, present := pr.Metadata.Annotations[cniNetworksStatusKey]; present { var cniInfo []cniNetworkInterface - err = json.Unmarshal([]byte(val), &cniInfo) + err := jsonUnmarshal([]byte(val), &cniInfo) if err != nil { return nil, pr.annotationUnmarshalError(cniNetworksStatusKey, err) } + // If this is the default interface, skip it as it is tested separately + // Otherwise add all non default interfaces for _, cniInterface := range cniInfo { - ips = append(ips, cniInterface.IPs...) + if !cniInterface.Default { + ips[cniInterface.Name] = cniInterface.IPs + } } - return + return ips, nil } log.Warn("Could not establish pod IPs from annotations, please manually set the 'test-network-function.com/multusips' annotation for complete test coverage") - for _, ip := range pr.Status.PodIPs { - ips = append(ips, ip["ip"]) + + return ips, nil +} + +func (pr *PodResource) getDefaultPodIPAddresses() []string { + var allDefaultIPs []string + for _, defaultIP := range pr.Status.PodIPs { + allDefaultIPs = append(allDefaultIPs, defaultIP["ip"]) } - return + return allDefaultIPs } func (pr *PodResource) annotationUnmarshalError(annotationKey string, err error) error { @@ -147,21 +168,54 @@ func (pr *PodResource) annotationUnmarshalError(annotationKey string, err error) err, annotationKey, pr.Metadata.Namespace, pr.Metadata.Name) } -// GetPodsByLabel will return all pods with a given label value. If `labelValue` is an empty string, all pods with that +// GetPodsByLabelByNamespace will return all pods with a given label value in provided namespace. +// If `labelValue` is an empty string, all pods with that // label will be returned, regardless of the labels value. -func GetPodsByLabel(label configsections.Label) (*PodList, error) { - cmd := makeGetCommand(resourceTypePods, buildLabelQuery(label)) +func GetPodsByLabelByNamespace(label configsections.Label, namespace string) (*PodList, error) { + out := executeOcGetCommand(resourceTypePods, buildLabelQuery(label), namespace) - out, err := cmd.Output() + log.Debug("JSON output for all pods labeled with: ", label) + log.Debug("Command: ", out) + + var podList PodList + err := jsonUnmarshal([]byte(out), &podList) if err != nil { return nil, err } + // Filter out terminating pods and pending/unscheduled pods + var pods []*PodResource + for _, pod := range podList.Items { + if pod.Metadata.DeletionTimestamp == "" || pod.Status.Phase != podPhaseRunning { + pods = append(pods, pod) + } + } + podList.Items = pods + return &podList, nil +} + +// GetPodsByLabelByNamespace will return all pods with a given label value. +// If `labelValue` is an empty string, all pods with that +// label will be returned, regardless of the labels value. +func GetPodsByLabel(label configsections.Label) (*PodList, error) { + out := executeOcGetAllCommand(resourceTypePods, buildLabelQuery(label)) + + log.Debug("JSON output for all pods labeled with: ", label) + log.Debug("Command: ", out) + var podList PodList - err = json.Unmarshal(out, &podList) + err := jsonUnmarshal([]byte(out), &podList) if err != nil { return nil, err } + // Filter out terminating pods and pending/unscheduled pods + var pods []*PodResource + for _, pod := range podList.Items { + if pod.Metadata.DeletionTimestamp == "" || pod.Status.Phase != podPhaseRunning { + pods = append(pods, pod) + } + } + podList.Items = pods return &podList, nil } diff --git a/pkg/config/autodiscover/pod_info_test.go b/pkg/config/autodiscover/pod_info_test.go index e3d43a865..04b7d5de6 100644 --- a/pkg/config/autodiscover/pod_info_test.go +++ b/pkg/config/autodiscover/pod_info_test.go @@ -14,17 +14,16 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -package autodiscover_test +package autodiscover import ( - "encoding/json" - "io/ioutil" - "log" + "os" "path" + "reflect" "testing" + log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" - "github.com/test-network-function/test-network-function/pkg/config/autodiscover" ) const ( @@ -38,12 +37,12 @@ var ( testSubjectFilePath = path.Join(filePath, testSubjectFile) ) -func loadPodResource(filePath string) (pod autodiscover.PodResource) { - contents, err := ioutil.ReadFile(filePath) +func loadPodResource(filePath string) (pod PodResource) { + contents, err := os.ReadFile(filePath) if err != nil { log.Fatalf("error (%s) loading PodResource %s for testing", err, filePath) } - err = json.Unmarshal(contents, &pod) + err = jsonUnmarshal(contents, &pod) if err != nil { log.Fatalf("error (%s) loading PodResource %s for testing", err, filePath) } @@ -61,3 +60,54 @@ func TestPodGetAnnotationValue(t *testing.T) { assert.Equal(t, "eth0", val) assert.Nil(t, err) } + +func TestPodResource_getDefaultPodIPAddresses(t *testing.T) { + type test struct { + name string + testFile string + want []string + } + + // Positive tests + var testsExpectFail = []test{ + {name: "ipv4ipv6", + testFile: "testorchestrator.json", + want: []string{"2.2.2.3", "fd00:10:244:1::3"}, + }, + } + + // Negative tests + var testsExpectPass = []test{ + {name: "ipv4ipv6", + testFile: "ipv4ipv6pod.json", + want: []string{"2.2.2.2", "fd00:10:244:1::3"}, + }, + {name: "ipv4", + testFile: "ipv4pod.json", + want: []string{"2.2.2.2"}, + }, + {name: "ipv6", + testFile: "ipv6pod.json", + want: []string{"fd00:10:244:1::3"}, + }, + } + + // negative tests + for _, tt := range testsExpectFail { + t.Run(tt.name, func(t *testing.T) { + pr := loadPodResource(path.Join(filePath, tt.testFile)) + if got := pr.getDefaultPodIPAddresses(); reflect.DeepEqual(got, tt.want) { + t.Errorf("PodResource.getDefaultPodIPAddresses() = %v, want %v", got, tt.want) + } + }) + } + // positive tests + for _, tt := range testsExpectPass { + t.Run(tt.name, func(t *testing.T) { + pr := loadPodResource(path.Join(filePath, tt.testFile)) + if got := pr.getDefaultPodIPAddresses(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("PodResource.getDefaultPodIPAddresses() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/config/autodiscover/pod_test.go b/pkg/config/autodiscover/pod_test.go new file mode 100644 index 000000000..f88953d4c --- /dev/null +++ b/pkg/config/autodiscover/pod_test.go @@ -0,0 +1,41 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package autodiscover + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBuildPodUnderTest(t *testing.T) { + orchestratorPodResource := loadPodResource(testOrchestratorFilePath) + orchestratorPod := buildPodUnderTest(&orchestratorPodResource) + + subjectPodResource := loadPodResource(testSubjectFilePath) + subjectPod := buildPodUnderTest(&subjectPodResource) + + assert.Equal(t, "tnf", orchestratorPod.Namespace) + assert.Equal(t, "I'mAPodName", orchestratorPod.Name) + assert.NotEqual(t, "I'mAContainer", orchestratorPod.Name) + // no tests set on pod and the config file will not be loaded from the unit test context: no tests should be set. + assert.Equal(t, []string{}, orchestratorPod.Tests) + + assert.Equal(t, "tnf", subjectPod.Namespace) + assert.Equal(t, "I'mAPodName", subjectPod.Name) + assert.Equal(t, []string{"OneTestName", "AnotherTestName"}, subjectPod.Tests) +} diff --git a/pkg/config/autodiscover/podset_info.go b/pkg/config/autodiscover/podset_info.go new file mode 100644 index 000000000..9f49e834d --- /dev/null +++ b/pkg/config/autodiscover/podset_info.go @@ -0,0 +1,128 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package autodiscover + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/test-network-function/test-network-function/pkg/config/configsections" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" + "github.com/test-network-function/test-network-function/pkg/utils" +) + +var ( + jsonUnmarshal = json.Unmarshal + execCommandOutput = func(command string) string { + return utils.ExecuteCommandAndValidate(command, ocCommandTimeOut, interactive.GetContext(expectersVerboseModeEnabled), func() { + log.Error("can't run command: ", command) + }) + } +) + +// PodSetList holds the data from an `oc get deployment/statefulset -o json` command +type PodSetList struct { + Items []PodSetResource `json:"items"` +} + +// PodSetResource defines deployment/statefulset resources +type PodSetResource struct { + Metadata struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` + } `json:"metadata"` + + Spec struct { + Replicas int `json:"replicas"` + } +} + +// GetName returns podset's metadata section's name field. +func (podset *PodSetResource) GetName() string { + return podset.Metadata.Name +} + +// GetNamespace returns podset's metadata section's namespace field. +func (podset *PodSetResource) GetNamespace() string { + return podset.Metadata.Namespace +} + +// GetReplicas returns podset's spec section's replicas field. +func (podset *PodSetResource) GetReplicas() int { + return podset.Spec.Replicas +} + +// GetLabels returns a map with the podset's metadata section's labels. +func (podset *PodSetResource) GetLabels() map[string]string { + return podset.Metadata.Labels +} +func (podset *PodSetResource) GetHpa() configsections.Hpa { + template := fmt.Sprintf("go-template='{{ range .items }}{{ if eq .spec.scaleTargetRef.name %q }}{{.spec.minReplicas}},{{.spec.maxReplicas}},{{.metadata.name}}{{ end }}{{ end }}'", podset.GetName()) + ocCmd := fmt.Sprintf("oc get hpa -n %s -o %s", podset.GetNamespace(), template) + out := execCommandOutput(ocCmd) + if out != "" { + out := strings.Split(out, ",") + min, _ := strconv.Atoi(out[0]) + max, _ := strconv.Atoi(out[1]) + hpaNmae := out[2] + return configsections.Hpa{ + MinReplicas: min, + MaxReplicas: max, + HpaName: hpaNmae, + } + } + return configsections.Hpa{} +} + +// GetTargetPodSetsByNamespace will return all podsets(deployments/statefulset )that have pods with a given label. +func GetTargetPodSetsByNamespace(namespace string, targetLabel configsections.Label, resourceTypePodSet string) (*PodSetList, error) { + labelQuery := fmt.Sprintf("%q==%q", buildLabelName(targetLabel.Prefix, targetLabel.Name), targetLabel.Value) + jqArgs := fmt.Sprintf("'[.items[] | select(.spec.template.metadata.labels.%s)]'", labelQuery) + ocCmd := fmt.Sprintf("oc get %s -n %s -o json | jq %s", resourceTypePodSet, namespace, jqArgs) + + out := execCommandOutput(ocCmd) + + var podsetList PodSetList + err := jsonUnmarshal([]byte(out), &podsetList.Items) + if err != nil { + return nil, err + } + + return &podsetList, nil +} + +// GetTargetDeploymentsByLabel will return all deployments/statefulsets that have pods with a given label. +func GetTargetPodSetsByLabel(targetLabel configsections.Label, resourceTypePodSet string) (*PodSetList, error) { + labelQuery := fmt.Sprintf("%q==%q", buildLabelName(targetLabel.Prefix, targetLabel.Name), targetLabel.Value) + jqArgs := fmt.Sprintf("'[.items[] | select(.spec.template.metadata.labels.%s)]'", labelQuery) + ocCmd := fmt.Sprintf("oc get %s -A -o json | jq %s", resourceTypePodSet, jqArgs) + + out := execCommandOutput(ocCmd) + + var podsetList PodSetList + err := jsonUnmarshal([]byte(out), &podsetList.Items) + if err != nil { + return nil, err + } + + return &podsetList, nil +} diff --git a/pkg/config/autodiscover/podset_info_test.go b/pkg/config/autodiscover/podset_info_test.go new file mode 100644 index 000000000..52d372009 --- /dev/null +++ b/pkg/config/autodiscover/podset_info_test.go @@ -0,0 +1,137 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package autodiscover + +import ( + "encoding/json" + "errors" + "log" + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/config/configsections" +) + +const ( + testDeploymentFile = "testdeployment.json" + testJQFile = "testdeploy.json" +) + +var ( + testDeploymentFilePath = path.Join(filePath, testDeploymentFile) + testJQFilePath = path.Join(filePath, testJQFile) +) + +func loadDeployment(filePath string) (deployment PodSetResource) { + contents, err := os.ReadFile(filePath) + if err != nil { + log.Fatalf("error (%s) loading PodSetResource %s for testing", err, filePath) + } + err = jsonUnmarshal(contents, &deployment) + if err != nil { + log.Fatalf("error (%s) unmarshalling PodSetResource %s for testing", err, filePath) + } + return +} + +func TestPodGetAnnotationValue1(t *testing.T) { + deployment := loadDeployment(testDeploymentFilePath) + + assert.Equal(t, "test", deployment.GetName()) + assert.Equal(t, "tnf", deployment.GetNamespace()) + assert.Equal(t, 2, deployment.GetReplicas()) + + labels := deployment.GetLabels() + assert.Equal(t, 1, len(labels)) + assert.Equal(t, "test", labels["app"]) +} + +//nolint:funlen +func TestGetTargetDeploymentByNamespace(t *testing.T) { + testCases := []struct { + badExec bool + execErr error + badJSONUnmarshal bool + jsonErr error + }{ + { // no failures + badExec: false, + badJSONUnmarshal: false, + }, + { // failure to exec + badExec: true, + execErr: errors.New("this is an error"), + badJSONUnmarshal: false, + jsonErr: nil, + }, + { // failure to jsonUnmarshal + badExec: false, + execErr: nil, + badJSONUnmarshal: true, + jsonErr: errors.New("this is an error"), + }, + } + + origExecFunc := execCommandOutput + + for _, tc := range testCases { + // Setup the mock functions + if tc.badExec { + execCommandOutput = func(command string) string { + return "" + } + } else { + execCommandOutput = func(command string) string { + contents, err := os.ReadFile(testJQFilePath) + assert.Nil(t, err) + return string(contents) + } + } + if tc.badJSONUnmarshal { + jsonUnmarshal = func(data []byte, v interface{}) error { + return tc.jsonErr + } + } else { + // use the "real" function + jsonUnmarshal = json.Unmarshal + } + + // Run the function and compare the list output + list, err := GetTargetPodSetsByNamespace("test", configsections.Label{ + Prefix: "prefix1", + Name: "name1", + Value: "value1", + }, string(configsections.Deployment)) + if !tc.badExec && !tc.badJSONUnmarshal { + assert.NotNil(t, list) + assert.Equal(t, "my-test1", list.Items[0].Metadata.Name) + } + + // Assert the errors and cleanup + if tc.badExec { + assert.NotNil(t, err) + execCommandOutput = origExecFunc + } + + if tc.badJSONUnmarshal { + assert.NotNil(t, err) + jsonUnmarshal = json.Unmarshal + } + } +} diff --git a/pkg/config/autodiscover/testdata/crd_output.json b/pkg/config/autodiscover/testdata/crd_output.json new file mode 100644 index 000000000..e59227cde --- /dev/null +++ b/pkg/config/autodiscover/testdata/crd_output.json @@ -0,0 +1,97 @@ +[ + "alertmanagerconfigs.monitoring.coreos.com", + "alertmanagers.monitoring.coreos.com", + "apirequestcounts.apiserver.openshift.io", + "apiservers.config.openshift.io", + "authentications.config.openshift.io", + "authentications.operator.openshift.io", + "baremetalhosts.metal3.io", + "builds.config.openshift.io", + "catalogsources.operators.coreos.com", + "cloudcredentials.operator.openshift.io", + "clusterautoscalers.autoscaling.openshift.io", + "clustercsidrivers.operator.openshift.io", + "clusternetworks.network.openshift.io", + "clusteroperators.config.openshift.io", + "clusterresourcequotas.quota.openshift.io", + "clusterserviceversions.operators.coreos.com", + "clusterversions.config.openshift.io", + "configs.imageregistry.operator.openshift.io", + "configs.operator.openshift.io", + "configs.samples.operator.openshift.io", + "consoleclidownloads.console.openshift.io", + "consoleexternalloglinks.console.openshift.io", + "consolelinks.console.openshift.io", + "consolenotifications.console.openshift.io", + "consoleplugins.console.openshift.io", + "consolequickstarts.console.openshift.io", + "consoles.config.openshift.io", + "consoles.operator.openshift.io", + "consoleyamlsamples.console.openshift.io", + "containerruntimeconfigs.machineconfiguration.openshift.io", + "controllerconfigs.machineconfiguration.openshift.io", + "credentialsrequests.cloudcredential.openshift.io", + "csisnapshotcontrollers.operator.openshift.io", + "dnses.config.openshift.io", + "dnses.operator.openshift.io", + "dnsrecords.ingress.operator.openshift.io", + "egressnetworkpolicies.network.openshift.io", + "egressrouters.network.operator.openshift.io", + "etcds.operator.openshift.io", + "featuregates.config.openshift.io", + "helmchartrepositories.helm.openshift.io", + "hostsubnets.network.openshift.io", + "imagecontentsourcepolicies.operator.openshift.io", + "imagepruners.imageregistry.operator.openshift.io", + "images.config.openshift.io", + "infrastructures.config.openshift.io", + "ingresscontrollers.operator.openshift.io", + "ingresses.config.openshift.io", + "installplans.operators.coreos.com", + "ippools.whereabouts.cni.cncf.io", + "kubeapiservers.operator.openshift.io", + "kubecontrollermanagers.operator.openshift.io", + "kubeletconfigs.machineconfiguration.openshift.io", + "kubeschedulers.operator.openshift.io", + "kubestorageversionmigrators.operator.openshift.io", + "machineautoscalers.autoscaling.openshift.io", + "machineconfigpools.machineconfiguration.openshift.io", + "machineconfigs.machineconfiguration.openshift.io", + "machinehealthchecks.machine.openshift.io", + "machines.machine.openshift.io", + "machinesets.machine.openshift.io", + "netnamespaces.network.openshift.io", + "network-attachment-definitions.k8s.cni.cncf.io", + "networks.config.openshift.io", + "networks.operator.openshift.io", + "oauths.config.openshift.io", + "openshiftapiservers.operator.openshift.io", + "openshiftcontrollermanagers.operator.openshift.io", + "operatorconditions.operators.coreos.com", + "operatorgroups.operators.coreos.com", + "operatorhubs.config.openshift.io", + "operatorpkis.network.operator.openshift.io", + "operators.operators.coreos.com", + "overlappingrangeipreservations.whereabouts.cni.cncf.io", + "podmonitors.monitoring.coreos.com", + "podnetworkconnectivitychecks.controlplane.operator.openshift.io", + "probes.monitoring.coreos.com", + "profiles.tuned.openshift.io", + "projects.config.openshift.io", + "prometheuses.monitoring.coreos.com", + "prometheusrules.monitoring.coreos.com", + "provisionings.metal3.io", + "proxies.config.openshift.io", + "rangeallocations.security.internal.openshift.io", + "rolebindingrestrictions.authorization.openshift.io", + "schedulers.config.openshift.io", + "securitycontextconstraints.security.openshift.io", + "servicecas.operator.openshift.io", + "servicemonitors.monitoring.coreos.com", + "storages.operator.openshift.io", + "storagestates.migration.k8s.io", + "storageversionmigrations.migration.k8s.io", + "subscriptions.operators.coreos.com", + "thanosrulers.monitoring.coreos.com", + "tuneds.tuned.openshift.io" + ] diff --git a/pkg/config/autodiscover/testdata/csv.json b/pkg/config/autodiscover/testdata/csv.json index f9a029334..d16c71295 100644 --- a/pkg/config/autodiscover/testdata/csv.json +++ b/pkg/config/autodiscover/testdata/csv.json @@ -1,7 +1,8 @@ { "metadata": { "annotations": { - "test-network-function.com/operator_tests": "[\"OPERATOR_STATUS\", \"ANOTHER_TEST\"]" + "test-network-function.com/operator_tests": "[\"OPERATOR_STATUS\", \"ANOTHER_TEST\"]", + "test-network-function.com/subscription_name": "[\"nginx-operator-v0-0-1-sub\"]" }, "labels": { "test-network-function.com/operator": "target" diff --git a/pkg/config/autodiscover/testdata/csv_output.json b/pkg/config/autodiscover/testdata/csv_output.json new file mode 100644 index 000000000..b9196938a --- /dev/null +++ b/pkg/config/autodiscover/testdata/csv_output.json @@ -0,0 +1,711 @@ +{ + "apiVersion": "v1", + "items": [ + { + "apiVersion": "operators.coreos.com/v1alpha1", + "kind": "ClusterServiceVersion", + "metadata": { + "annotations": { + "alm-examples": "[\n {\n \"apiVersion\": \"etcd.database.coreos.com/v1beta2\",\n \"kind\": \"EtcdCluster\",\n \"metadata\": {\n \"name\": \"example\"\n },\n \"spec\": {\n \"size\": 3,\n \"version\": \"3.2.13\"\n }\n },\n {\n \"apiVersion\": \"etcd.database.coreos.com/v1beta2\",\n \"kind\": \"EtcdRestore\",\n \"metadata\": {\n \"name\": \"example-etcd-cluster-restore\"\n },\n \"spec\": {\n \"etcdCluster\": {\n \"name\": \"example-etcd-cluster\"\n },\n \"backupStorageType\": \"S3\",\n \"s3\": {\n \"path\": \"\u003cfull-s3-path\u003e\",\n \"awsSecret\": \"\u003caws-secret\u003e\"\n }\n }\n },\n {\n \"apiVersion\": \"etcd.database.coreos.com/v1beta2\",\n \"kind\": \"EtcdBackup\",\n \"metadata\": {\n \"name\": \"example-etcd-cluster-backup\"\n },\n \"spec\": {\n \"etcdEndpoints\": [\"\u003cetcd-cluster-endpoints\u003e\"],\n \"storageType\":\"S3\",\n \"s3\": {\n \"path\": \"\u003cfull-s3-path\u003e\",\n \"awsSecret\": \"\u003caws-secret\u003e\"\n }\n }\n }\n]\n", + "capabilities": "Full Lifecycle", + "categories": "Database", + "containerImage": "quay.io/coreos/etcd-operator@sha256:66a37fd61a06a43969854ee6d3e21087a98b93838e284a6086b13917f96b0d9b", + "createdAt": "2019-02-28 01:03:00", + "description": "Create and maintain highly-available etcd clusters on Kubernetes", + "olm.operatorGroup": "default-2lv6l", + "olm.operatorNamespace": "default", + "olm.targetNamespaces": "default", + "operatorframework.io/properties": "{\"properties\":[{\"type\":\"olm.gvk\",\"value\":{\"group\":\"etcd.database.coreos.com\",\"kind\":\"EtcdCluster\",\"version\":\"v1beta2\"}},{\"type\":\"olm.gvk\",\"value\":{\"group\":\"etcd.database.coreos.com\",\"kind\":\"EtcdRestore\",\"version\":\"v1beta2\"}},{\"type\":\"olm.package\",\"value\":{\"packageName\":\"etcd\",\"version\":\"0.9.4\"}},{\"type\":\"olm.gvk\",\"value\":{\"group\":\"etcd.database.coreos.com\",\"kind\":\"EtcdBackup\",\"version\":\"v1beta2\"}}]}", + "repository": "https://github.com/coreos/etcd-operator", + "tectonic-visibility": "ocs" + }, + "creationTimestamp": "2021-12-22T20:48:19Z", + "generation": 1, + "labels": { + "olm.api.2c1e6f7e17c07035": "provided", + "olm.api.2fdc3540750c4d2b": "provided", + "olm.api.c571d720f17289d3": "provided", + "operators.coreos.com/etcd.default": "", + "testLabel": "testValue" + }, + "managedFields": [ + { + "apiVersion": "operators.coreos.com/v1alpha1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:alm-examples": {}, + "f:capabilities": {}, + "f:categories": {}, + "f:containerImage": {}, + "f:createdAt": {}, + "f:description": {}, + "f:operatorframework.io/properties": {}, + "f:repository": {}, + "f:tectonic-visibility": {} + } + }, + "f:spec": { + ".": {}, + "f:apiservicedefinitions": {}, + "f:cleanup": { + ".": {}, + "f:enabled": {} + }, + "f:customresourcedefinitions": { + ".": {}, + "f:owned": {} + }, + "f:description": {}, + "f:displayName": {}, + "f:icon": {}, + "f:install": { + ".": {}, + "f:spec": { + ".": {}, + "f:deployments": {}, + "f:permissions": {} + }, + "f:strategy": {} + }, + "f:installModes": {}, + "f:keywords": {}, + "f:labels": { + ".": {}, + "f:alm-owner-etcd": {}, + "f:operated-by": {} + }, + "f:links": {}, + "f:maintainers": {}, + "f:maturity": {}, + "f:provider": { + ".": {}, + "f:name": {} + }, + "f:replaces": {}, + "f:selector": { + ".": {}, + "f:matchLabels": { + ".": {}, + "f:alm-owner-etcd": {}, + "f:operated-by": {} + } + }, + "f:version": {} + } + }, + "manager": "catalog", + "operation": "Update", + "time": "2021-12-22T20:48:19Z" + }, + { + "apiVersion": "operators.coreos.com/v1alpha1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + ".": {}, + "f:cleanup": {}, + "f:conditions": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:phase": {}, + "f:reason": {}, + "f:requirementStatus": {} + } + }, + "manager": "olm", + "operation": "Update", + "subresource": "status", + "time": "2021-12-22T20:48:20Z" + }, + { + "apiVersion": "operators.coreos.com/v1alpha1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:olm.operatorGroup": {}, + "f:olm.operatorNamespace": {}, + "f:olm.targetNamespaces": {} + }, + "f:labels": { + ".": {}, + "f:olm.api.2c1e6f7e17c07035": {}, + "f:olm.api.2fdc3540750c4d2b": {}, + "f:olm.api.c571d720f17289d3": {}, + "f:operators.coreos.com/etcd.default": {} + } + } + }, + "manager": "olm", + "operation": "Update", + "time": "2021-12-22T20:48:21Z" + } + ], + "name": "etcdoperator.v0.9.4", + "namespace": "default", + "resourceVersion": "61119", + "uid": "7ddefc17-8353-4aad-aa1d-2c8427df5a61" + }, + "spec": { + "apiservicedefinitions": {}, + "cleanup": { + "enabled": false + }, + "customresourcedefinitions": { + "owned": [ + { + "description": "Represents a cluster of etcd nodes.", + "displayName": "etcd Cluster", + "kind": "EtcdCluster", + "name": "etcdclusters.etcd.database.coreos.com", + "resources": [ + { + "kind": "Service", + "name": "", + "version": "v1" + }, + { + "kind": "Pod", + "name": "", + "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" + ] + }, + { + "description": "Limits describes the minimum/maximum amount of compute resources required/allowed", + "displayName": "Resource Requirements", + "path": "pod.resources", + "x-descriptors": [ + "urn:alm:descriptor:com.tectonic.ui:resourceRequirements" + ] + } + ], + "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": "serviceName", + "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": "Represents the intent to backup an etcd cluster.", + "displayName": "etcd Backup", + "kind": "EtcdBackup", + "name": "etcdbackups.etcd.database.coreos.com", + "specDescriptors": [ + { + "description": "Specifies the endpoints of an etcd cluster.", + "displayName": "etcd Endpoint(s)", + "path": "etcdEndpoints", + "x-descriptors": [ + "urn:alm:descriptor:etcd:endpoint" + ] + }, + { + "description": "The full AWS S3 path where the backup is saved.", + "displayName": "S3 Path", + "path": "s3.path", + "x-descriptors": [ + "urn:alm:descriptor:aws:s3:path" + ] + }, + { + "description": "The name of the secret object that stores the AWS credential and config files.", + "displayName": "AWS Secret", + "path": "s3.awsSecret", + "x-descriptors": [ + "urn:alm:descriptor:io.kubernetes:Secret" + ] + } + ], + "statusDescriptors": [ + { + "description": "Indicates if the backup was successful.", + "displayName": "Succeeded", + "path": "succeeded", + "x-descriptors": [ + "urn:alm:descriptor:text" + ] + }, + { + "description": "Indicates the reason for any backup related failures.", + "displayName": "Reason", + "path": "reason", + "x-descriptors": [ + "urn:alm:descriptor:io.kubernetes.phase:reason" + ] + } + ], + "version": "v1beta2" + }, + { + "description": "Represents the intent to restore an etcd cluster from a backup.", + "displayName": "etcd Restore", + "kind": "EtcdRestore", + "name": "etcdrestores.etcd.database.coreos.com", + "specDescriptors": [ + { + "description": "References the EtcdCluster which should be restored,", + "displayName": "etcd Cluster", + "path": "etcdCluster.name", + "x-descriptors": [ + "urn:alm:descriptor:io.kubernetes:EtcdCluster", + "urn:alm:descriptor:text" + ] + }, + { + "description": "The full AWS S3 path where the backup is saved.", + "displayName": "S3 Path", + "path": "s3.path", + "x-descriptors": [ + "urn:alm:descriptor:aws:s3:path" + ] + }, + { + "description": "The name of the secret object that stores the AWS credential and config files.", + "displayName": "AWS Secret", + "path": "s3.awsSecret", + "x-descriptors": [ + "urn:alm:descriptor:io.kubernetes:Secret" + ] + } + ], + "statusDescriptors": [ + { + "description": "Indicates if the restore was successful.", + "displayName": "Succeeded", + "path": "succeeded", + "x-descriptors": [ + "urn:alm:descriptor:text" + ] + }, + { + "description": "Indicates the reason for any restore related failures.", + "displayName": "Reason", + "path": "reason", + "x-descriptors": [ + "urn:alm:descriptor:io.kubernetes.phase:reason" + ] + } + ], + "version": "v1beta2" + } + ] + }, + "description": "The etcd Operater creates and maintains highly-available etcd clusters on Kubernetes, allowing engineers to easily deploy and manage etcd clusters for their applications.\n\netcd is a distributed key value store that provides a reliable way to store data across a cluster of machines. It’s open-source and available on GitHub. etcd gracefully handles leader elections during network partitions and will tolerate machine failure, including the leader.\n\n\n### Reading and writing to etcd\n\nCommunicate with etcd though its command line utility `etcdctl` via port forwarding:\n\n $ kubectl --namespace default port-forward service/example-client 2379:2379\n $ etcdctl --endpoints http://127.0.0.1:2379 get /\n\nOr directly to the API using the automatically generated Kubernetes Service:\n\n $ etcdctl --endpoints http://example-client.default.svc:2379 get /\n\nBe sure to secure your etcd cluster (see Common Configurations) before exposing it outside of the namespace or cluster.\n\n\n### Supported Features\n\n* **High availability** - Multiple instances of etcd are networked together and secured. Individual failures or networking issues are transparently handled to keep your cluster up and running.\n\n* **Automated updates** - 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\n* **Backups included** - Create etcd backups and restore them through the etcd Operator.\n\n### Common Configurations\n\n* **Configure TLS** - Specify [static TLS certs](https://github.com/coreos/etcd-operator/blob/master/doc/user/cluster_tls.md) as Kubernetes secrets.\n\n* **Set Node Selector and Affinity** - [Spread your etcd Pods](https://github.com/coreos/etcd-operator/blob/master/doc/user/spec_examples.md#three-member-cluster-with-node-selector-and-anti-affinity-across-nodes) across Nodes and availability zones.\n\n* **Set Resource Limits** - [Set the Kubernetes limit and request](https://github.com/coreos/etcd-operator/blob/master/doc/user/spec_examples.md#three-member-cluster-with-resource-requirement) values for your etcd Pods.\n\n* **Customize Storage** - [Set a custom StorageClass](https://github.com/coreos/etcd-operator/blob/master/doc/user/spec_examples.md#custom-persistentvolumeclaim-definition) that you would like to use.\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" + } + }, + "strategy": {}, + "template": { + "metadata": { + "creationTimestamp": null, + "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:66a37fd61a06a43969854ee6d3e21087a98b93838e284a6086b13917f96b0d9b", + "name": "etcd-operator", + "resources": {} + }, + { + "command": [ + "etcd-backup-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:66a37fd61a06a43969854ee6d3e21087a98b93838e284a6086b13917f96b0d9b", + "name": "etcd-backup-operator", + "resources": {} + }, + { + "command": [ + "etcd-restore-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:66a37fd61a06a43969854ee6d3e21087a98b93838e284a6086b13917f96b0d9b", + "name": "etcd-restore-operator", + "resources": {} + } + ], + "serviceAccountName": "etcd-operator" + } + } + } + } + ], + "permissions": [ + { + "rules": [ + { + "apiGroups": [ + "etcd.database.coreos.com" + ], + "resources": [ + "etcdclusters", + "etcdbackups", + "etcdrestores" + ], + "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": false, + "type": "AllNamespaces" + } + ], + "keywords": [ + "etcd", + "key value", + "database", + "coreos", + "open source" + ], + "labels": { + "alm-owner-etcd": "etcdoperator", + "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": "etcd-dev@googlegroups.com", + "name": "etcd Community" + } + ], + "maturity": "alpha", + "provider": { + "name": "CNCF" + }, + "replaces": "etcdoperator.v0.9.2", + "selector": { + "matchLabels": { + "alm-owner-etcd": "etcdoperator", + "operated-by": "etcdoperator" + } + }, + "version": "0.9.4" + }, + "status": { + "cleanup": {}, + "conditions": [ + { + "lastTransitionTime": "2021-12-22T20:48:20Z", + "lastUpdateTime": "2021-12-22T20:48:20Z", + "message": "requirements not yet checked", + "phase": "Pending", + "reason": "RequirementsUnknown" + }, + { + "lastTransitionTime": "2021-12-22T20:48:20Z", + "lastUpdateTime": "2021-12-22T20:48:20Z", + "message": "all requirements found, attempting install", + "phase": "InstallReady", + "reason": "AllRequirementsMet" + }, + { + "lastTransitionTime": "2021-12-22T20:48:21Z", + "lastUpdateTime": "2021-12-22T20:48:21Z", + "message": "waiting for install components to report healthy", + "phase": "Installing", + "reason": "InstallSucceeded" + }, + { + "lastTransitionTime": "2021-12-22T20:48:21Z", + "lastUpdateTime": "2021-12-22T20:48:23Z", + "message": "installing: waiting for deployment etcd-operator to become ready: deployment \"etcd-operator\" not available: Deployment does not have minimum availability.", + "phase": "Installing", + "reason": "InstallWaiting" + }, + { + "lastTransitionTime": "2021-12-22T20:48:42Z", + "lastUpdateTime": "2021-12-22T20:48:42Z", + "message": "install strategy completed with no errors", + "phase": "Succeeded", + "reason": "InstallSucceeded" + } + ], + "lastTransitionTime": "2021-12-22T20:48:42Z", + "lastUpdateTime": "2021-12-22T20:48:42Z", + "message": "install strategy completed with no errors", + "phase": "Succeeded", + "reason": "InstallSucceeded", + "requirementStatus": [ + { + "group": "apiextensions.k8s.io", + "kind": "CustomResourceDefinition", + "message": "CRD is present and Established condition is true", + "name": "etcdbackups.etcd.database.coreos.com", + "status": "Present", + "uuid": "569a3a7b-8f11-4cca-acc7-ce63a495f0c3", + "version": "v1" + }, + { + "group": "apiextensions.k8s.io", + "kind": "CustomResourceDefinition", + "message": "CRD is present and Established condition is true", + "name": "etcdclusters.etcd.database.coreos.com", + "status": "Present", + "uuid": "70c436f7-a277-490a-b156-63af8b01af2d", + "version": "v1" + }, + { + "group": "apiextensions.k8s.io", + "kind": "CustomResourceDefinition", + "message": "CRD is present and Established condition is true", + "name": "etcdrestores.etcd.database.coreos.com", + "status": "Present", + "uuid": "aa61a5ae-8ac1-4a74-a89d-778df41194a9", + "version": "v1" + }, + { + "dependents": [ + { + "group": "rbac.authorization.k8s.io", + "kind": "PolicyRule", + "message": "namespaced rule:{\"verbs\":[\"*\"],\"apiGroups\":[\"etcd.database.coreos.com\"],\"resources\":[\"etcdclusters\",\"etcdbackups\",\"etcdrestores\"]}", + "status": "Satisfied", + "version": "v1" + }, + { + "group": "rbac.authorization.k8s.io", + "kind": "PolicyRule", + "message": "namespaced rule:{\"verbs\":[\"*\"],\"apiGroups\":[\"\"],\"resources\":[\"pods\",\"services\",\"endpoints\",\"persistentvolumeclaims\",\"events\"]}", + "status": "Satisfied", + "version": "v1" + }, + { + "group": "rbac.authorization.k8s.io", + "kind": "PolicyRule", + "message": "namespaced rule:{\"verbs\":[\"*\"],\"apiGroups\":[\"apps\"],\"resources\":[\"deployments\"]}", + "status": "Satisfied", + "version": "v1" + }, + { + "group": "rbac.authorization.k8s.io", + "kind": "PolicyRule", + "message": "namespaced rule:{\"verbs\":[\"get\"],\"apiGroups\":[\"\"],\"resources\":[\"secrets\"]}", + "status": "Satisfied", + "version": "v1" + } + ], + "group": "", + "kind": "ServiceAccount", + "message": "", + "name": "etcd-operator", + "status": "Present", + "version": "v1" + } + ] + } + } + ], + "kind": "List", + "metadata": { + "resourceVersion": "", + "selfLink": "" + } +} diff --git a/pkg/config/autodiscover/testdata/csv_output_nolabel.json b/pkg/config/autodiscover/testdata/csv_output_nolabel.json new file mode 100644 index 000000000..1c318dacd --- /dev/null +++ b/pkg/config/autodiscover/testdata/csv_output_nolabel.json @@ -0,0 +1,9 @@ +{ + "apiVersion": "v1", + "items": [], + "kind": "List", + "metadata": { + "resourceVersion": "", + "selfLink": "" + } +} diff --git a/pkg/config/autodiscover/testdata/empty.json b/pkg/config/autodiscover/testdata/empty.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/pkg/config/autodiscover/testdata/empty.json @@ -0,0 +1 @@ +[] diff --git a/pkg/config/autodiscover/testdata/ipv4ipv6pod.json b/pkg/config/autodiscover/testdata/ipv4ipv6pod.json new file mode 100644 index 000000000..dcd14c709 --- /dev/null +++ b/pkg/config/autodiscover/testdata/ipv4ipv6pod.json @@ -0,0 +1,33 @@ + +{ + "metadata": { + "annotations": { + "k8s.v1.cni.cncf.io/networks-status": "[{\n \"name\": \"\",\n \"interface\": \"LowerPriorityInterface\",\n \"ips\": [\n \"1.1.1.1\"\n ],\n \"default\": true,\n \"dns\": {}\n}]", + "test-network-function.com/defaultnetworkinterface": "\"eth0\"" + }, + "labels": { + "app": "partner", + "test-network-function.com/generic": "orchestrator", + "test-network-function.com/container": "target" + }, + "name": "I'mAPodName", + "namespace": "tnf" + }, + "spec": { + "containers": [ + { + "name": "I'mAContainer" + } + ] + }, + "status": { + "podIPs": [ + { + "ip": "2.2.2.2" + }, + { + "ip": "fd00:10:244:1::3" + } + ] + } +} \ No newline at end of file diff --git a/pkg/config/autodiscover/testdata/ipv4pod.json b/pkg/config/autodiscover/testdata/ipv4pod.json new file mode 100644 index 000000000..5666d282a --- /dev/null +++ b/pkg/config/autodiscover/testdata/ipv4pod.json @@ -0,0 +1,30 @@ + +{ + "metadata": { + "annotations": { + "k8s.v1.cni.cncf.io/networks-status": "[{\n \"name\": \"\",\n \"interface\": \"LowerPriorityInterface\",\n \"ips\": [\n \"1.1.1.1\"\n ],\n \"default\": true,\n \"dns\": {}\n}]", + "test-network-function.com/defaultnetworkinterface": "\"eth0\"" + }, + "labels": { + "app": "partner", + "test-network-function.com/generic": "orchestrator", + "test-network-function.com/container": "target" + }, + "name": "I'mAPodName", + "namespace": "tnf" + }, + "spec": { + "containers": [ + { + "name": "I'mAContainer" + } + ] + }, + "status": { + "podIPs": [ + { + "ip": "2.2.2.2" + } + ] + } +} \ No newline at end of file diff --git a/pkg/config/autodiscover/testdata/ipv6pod.json b/pkg/config/autodiscover/testdata/ipv6pod.json new file mode 100644 index 000000000..b0727e151 --- /dev/null +++ b/pkg/config/autodiscover/testdata/ipv6pod.json @@ -0,0 +1,30 @@ + +{ + "metadata": { + "annotations": { + "k8s.v1.cni.cncf.io/networks-status": "[{\n \"name\": \"\",\n \"interface\": \"LowerPriorityInterface\",\n \"ips\": [\n \"1.1.1.1\"\n ],\n \"default\": true,\n \"dns\": {}\n}]", + "test-network-function.com/defaultnetworkinterface": "\"eth0\"" + }, + "labels": { + "app": "partner", + "test-network-function.com/generic": "orchestrator", + "test-network-function.com/container": "target" + }, + "name": "I'mAPodName", + "namespace": "tnf" + }, + "spec": { + "containers": [ + { + "name": "I'mAContainer" + } + ] + }, + "status": { + "podIPs": [ + { + "ip": "fd00:10:244:1::3" + } + ] + } +} \ No newline at end of file diff --git a/pkg/config/autodiscover/testdata/pods_with_debug_label.json b/pkg/config/autodiscover/testdata/pods_with_debug_label.json new file mode 100644 index 000000000..60ad3033e --- /dev/null +++ b/pkg/config/autodiscover/testdata/pods_with_debug_label.json @@ -0,0 +1,428 @@ +{ + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "annotations": { + "k8s.v1.cni.cncf.io/networks": "macvlan-conf", + "test-network-function.com/container_tests": "[\"PRIVILEGED_POD\",\"PRIVILEGED_ROLE\"]", + "test-network-function.com/defaultnetworkinterface": "\"eth0\"" + }, + "creationTimestamp": "2022-01-20T19:25:43Z", + "generateName": "test-7dc8cf6b5f-", + "labels": { + "app": "test", + "pod-template-hash": "7dc8cf6b5f", + "test-network-function.com/app": "debug", + "test-network-function.com/container": "target", + "test-network-function.com/generic": "target" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:k8s.v1.cni.cncf.io/networks": {}, + "f:test-network-function.com/container_tests": {}, + "f:test-network-function.com/defaultnetworkinterface": {} + }, + "f:generateName": {}, + "f:labels": { + ".": {}, + "f:app": {}, + "f:pod-template-hash": {}, + "f:test-network-function.com/container": {}, + "f:test-network-function.com/generic": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"cffc419b-44ae-4cbd-9a66-6ef34e1b3c18\"}": {} + } + }, + "f:spec": { + "f:affinity": { + ".": {}, + "f:podAntiAffinity": { + ".": {}, + "f:requiredDuringSchedulingIgnoredDuringExecution": {} + } + }, + "f:automountServiceAccountToken": {}, + "f:containers": { + "k:{\"name\":\"test\"}": { + ".": {}, + "f:command": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:lifecycle": { + ".": {}, + "f:preStop": { + ".": {}, + "f:exec": { + ".": {}, + "f:command": {} + } + } + }, + "f:livenessProbe": { + ".": {}, + "f:failureThreshold": {}, + "f:httpGet": { + ".": {}, + "f:httpHeaders": {}, + "f:path": {}, + "f:port": {}, + "f:scheme": {} + }, + "f:initialDelaySeconds": {}, + "f:periodSeconds": {}, + "f:successThreshold": {}, + "f:timeoutSeconds": {} + }, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":8080,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:name": {}, + "f:protocol": {} + } + }, + "f:readinessProbe": { + ".": {}, + "f:failureThreshold": {}, + "f:httpGet": { + ".": {}, + "f:httpHeaders": {}, + "f:path": {}, + "f:port": {}, + "f:scheme": {} + }, + "f:initialDelaySeconds": {}, + "f:periodSeconds": {}, + "f:successThreshold": {}, + "f:timeoutSeconds": {} + }, + "f:resources": { + ".": {}, + "f:limits": { + ".": {}, + "f:cpu": {}, + "f:memory": {} + }, + "f:requests": { + ".": {}, + "f:cpu": {}, + "f:memory": {} + } + }, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:enableServiceLinks": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2022-01-20T19:25:43Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:conditions": { + ".": {}, + "k:{\"type\":\"PodScheduled\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + } + } + } + }, + "manager": "kube-scheduler", + "operation": "Update", + "subresource": "status", + "time": "2022-01-20T19:25:43Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:conditions": { + "k:{\"type\":\"ContainersReady\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Initialized\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Ready\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:containerStatuses": {}, + "f:hostIP": {}, + "f:phase": {}, + "f:podIP": {}, + "f:podIPs": { + ".": {}, + "k:{\"ip\":\"10.244.2.3\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "Go-http-client", + "operation": "Update", + "subresource": "status", + "time": "2022-01-20T19:26:00Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + "f:test-network-function.com/app": {} + } + } + }, + "manager": "oc", + "operation": "Update", + "time": "2022-01-21T19:49:09Z" + } + ], + "name": "test-7dc8cf6b5f-2t4bn", + "namespace": "tnf", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "test-7dc8cf6b5f", + "uid": "cffc419b-44ae-4cbd-9a66-6ef34e1b3c18" + } + ], + "resourceVersion": "89137", + "uid": "479e0604-a05c-40af-bcda-4cad1dd3c6c6" + }, + "spec": { + "affinity": { + "podAntiAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": [ + { + "labelSelector": { + "matchExpressions": [ + { + "key": "app", + "operator": "In", + "values": [ + "test" + ] + } + ] + }, + "topologyKey": "kubernetes.io/hostname" + } + ] + } + }, + "automountServiceAccountToken": false, + "containers": [ + { + "command": [ + "./bin/app" + ], + "image": "quay.io/testnetworkfunction/cnf-test-partner:latest", + "imagePullPolicy": "IfNotPresent", + "lifecycle": { + "preStop": { + "exec": { + "command": [ + "/bin/sh", + "-c", + "killall -0 tail" + ] + } + } + }, + "livenessProbe": { + "failureThreshold": 3, + "httpGet": { + "httpHeaders": [ + { + "name": "health-check", + "value": "liveness" + } + ], + "path": "/health", + "port": 8080, + "scheme": "HTTP" + }, + "initialDelaySeconds": 10, + "periodSeconds": 5, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "name": "test", + "ports": [ + { + "containerPort": 8080, + "name": "http-probe", + "protocol": "TCP" + } + ], + "readinessProbe": { + "failureThreshold": 3, + "httpGet": { + "httpHeaders": [ + { + "name": "health-check", + "value": "readiness" + } + ], + "path": "/ready", + "port": 8080, + "scheme": "HTTP" + }, + "initialDelaySeconds": 10, + "periodSeconds": 5, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "resources": { + "limits": { + "cpu": "250m", + "memory": "512Mi" + }, + "requests": { + "cpu": "250m", + "memory": "512Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube-m03", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2022-01-20T19:25:45Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2022-01-20T19:26:00Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2022-01-20T19:26:00Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2022-01-20T19:25:45Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://8e11bf9a70177e31a46f3bcbad7d8b04b6d4545fbf9fc235004f3832f71d8497", + "image": "quay.io/testnetworkfunction/cnf-test-partner:latest", + "imageID": "docker-pullable://quay.io/testnetworkfunction/cnf-test-partner@sha256:f092f31e828a67d58e8cd1d2d5a9e60a14f56d131b022c8b74f943c93a7e1e08", + "lastState": {}, + "name": "test", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2022-01-20T19:25:47Z" + } + } + } + ], + "hostIP": "192.168.59.125", + "phase": "Running", + "podIP": "10.244.2.3", + "podIPs": [ + { + "ip": "10.244.2.3" + } + ], + "qosClass": "Guaranteed", + "startTime": "2022-01-20T19:25:45Z" + } + } + ], + "kind": "List", + "metadata": { + "resourceVersion": "", + "selfLink": "" + } +} diff --git a/pkg/config/autodiscover/testdata/test_deploy_matching_label.json b/pkg/config/autodiscover/testdata/test_deploy_matching_label.json new file mode 100644 index 000000000..eb7459d3a --- /dev/null +++ b/pkg/config/autodiscover/testdata/test_deploy_matching_label.json @@ -0,0 +1,190 @@ +[ + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "annotations": { + "deployment.kubernetes.io/revision": "1" + }, + "creationTimestamp": "2022-01-10T14:29:54Z", + "generation": 1, + "labels": { + "app": "mydeploy", + "key1": "value1" + }, + "managedFields": [ + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:deployment.kubernetes.io/revision": {} + } + }, + "f:status": { + "f:availableReplicas": {}, + "f:conditions": { + ".": {}, + "k:{\"type\":\"Available\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Progressing\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:observedGeneration": {}, + "f:readyReplicas": {}, + "f:replicas": {}, + "f:updatedReplicas": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "subresource": "status", + "time": "2022-01-10T14:30:12Z" + }, + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:app": {}, + "f:key1": {} + } + }, + "f:spec": { + "f:progressDeadlineSeconds": {}, + "f:replicas": {}, + "f:revisionHistoryLimit": {}, + "f:selector": {}, + "f:strategy": { + "f:rollingUpdate": { + ".": {}, + "f:maxSurge": {}, + "f:maxUnavailable": {} + }, + "f:type": {} + }, + "f:template": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:app": {} + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + } + } + }, + "manager": "oc", + "operation": "Update", + "time": "2022-01-10T14:30:35Z" + } + ], + "name": "mydeploy", + "namespace": "default", + "resourceVersion": "292373", + "uid": "2fa96952-c1f9-4e14-af2a-59889624b2be" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 1, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app": "mydeploy" + } + }, + "strategy": { + "rollingUpdate": { + "maxSurge": "25%", + "maxUnavailable": "25%" + }, + "type": "RollingUpdate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "mydeploy" + } + }, + "spec": { + "containers": [ + { + "image": "nginx", + "imagePullPolicy": "Always", + "name": "nginx", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "availableReplicas": 1, + "conditions": [ + { + "lastTransitionTime": "2022-01-10T14:30:12Z", + "lastUpdateTime": "2022-01-10T14:30:12Z", + "message": "Deployment has minimum availability.", + "reason": "MinimumReplicasAvailable", + "status": "True", + "type": "Available" + }, + { + "lastTransitionTime": "2022-01-10T14:29:54Z", + "lastUpdateTime": "2022-01-10T14:30:12Z", + "message": "ReplicaSet \"mydeploy-7dff945675\" has successfully progressed.", + "reason": "NewReplicaSetAvailable", + "status": "True", + "type": "Progressing" + } + ], + "observedGeneration": 1, + "readyReplicas": 1, + "replicas": 1, + "updatedReplicas": 1 + } + } +] diff --git a/pkg/config/autodiscover/testdata/testdeploy.json b/pkg/config/autodiscover/testdata/testdeploy.json new file mode 100644 index 000000000..afd58d3f4 --- /dev/null +++ b/pkg/config/autodiscover/testdata/testdeploy.json @@ -0,0 +1,184 @@ +[{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "annotations": { + "deployment.kubernetes.io/revision": "1" + }, + "creationTimestamp": "2021-10-11T15:22:41Z", + "generation": 1, + "labels": { + "app": "my-test1" + }, + "managedFields": [{ + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:app": {} + } + }, + "f:spec": { + "f:progressDeadlineSeconds": {}, + "f:replicas": {}, + "f:revisionHistoryLimit": {}, + "f:selector": {}, + "f:strategy": { + "f:rollingUpdate": { + ".": {}, + "f:maxSurge": {}, + "f:maxUnavailable": {} + }, + "f:type": {} + }, + "f:template": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:app": {}, + "f:prefix1/name1": {} + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + } + } + }, + "manager": "oc", + "operation": "Update", + "time": "2021-10-11T15:22:41Z" + }, + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:deployment.kubernetes.io/revision": {} + } + }, + "f:status": { + "f:availableReplicas": {}, + "f:conditions": { + ".": {}, + "k:{\"type\":\"Available\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Progressing\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:observedGeneration": {}, + "f:readyReplicas": {}, + "f:replicas": {}, + "f:updatedReplicas": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "subresource": "status", + "time": "2021-10-11T15:22:44Z" + } + ], + "name": "my-test1", + "namespace": "test", + "resourceVersion": "15460", + "uid": "bc6b1ac5-ee45-47a1-bc41-1258e0cf30a3" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 1, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app": "my-test1" + } + }, + "strategy": { + "rollingUpdate": { + "maxSurge": "25%", + "maxUnavailable": "25%" + }, + "type": "RollingUpdate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "my-test1", + "prefix1/name1": "value1" + } + }, + "spec": { + "containers": [{ + "image": "nginx", + "imagePullPolicy": "Always", + "name": "nginx", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + }], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "availableReplicas": 1, + "conditions": [{ + "lastTransitionTime": "2021-10-11T15:22:44Z", + "lastUpdateTime": "2021-10-11T15:22:44Z", + "message": "Deployment has minimum availability.", + "reason": "MinimumReplicasAvailable", + "status": "True", + "type": "Available" + }, + { + "lastTransitionTime": "2021-10-11T15:22:41Z", + "lastUpdateTime": "2021-10-11T15:22:44Z", + "message": "ReplicaSet \"my-test1-57dbcf59f6\" has successfully progressed.", + "reason": "NewReplicaSetAvailable", + "status": "True", + "type": "Progressing" + } + ], + "observedGeneration": 1, + "readyReplicas": 1, + "replicas": 1, + "updatedReplicas": 1 + } +}] diff --git a/pkg/config/autodiscover/testdata/testdeployment.json b/pkg/config/autodiscover/testdata/testdeployment.json new file mode 100644 index 000000000..63968b858 --- /dev/null +++ b/pkg/config/autodiscover/testdata/testdeployment.json @@ -0,0 +1,15 @@ + +{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "labels": { + "app": "test" + }, + "name": "test", + "namespace": "tnf" + }, + "spec": { + "replicas": 2 + } +} \ No newline at end of file diff --git a/pkg/config/autodiscover/testdata/testhelmchart.json b/pkg/config/autodiscover/testdata/testhelmchart.json new file mode 100644 index 000000000..8a7b7fa9a --- /dev/null +++ b/pkg/config/autodiscover/testdata/testhelmchart.json @@ -0,0 +1,11 @@ +[ +{ + "name":"my-test1", + "namespace":"test", + "revision":"1", + "updated":"test", + "status":"deployed", + "chart":"my-test1-1.13.8", + "app_version":"1.7.1" +} +] \ No newline at end of file diff --git a/pkg/config/autodiscover/testdata/testpods_empty.json b/pkg/config/autodiscover/testdata/testpods_empty.json new file mode 100644 index 000000000..1c318dacd --- /dev/null +++ b/pkg/config/autodiscover/testdata/testpods_empty.json @@ -0,0 +1,9 @@ +{ + "apiVersion": "v1", + "items": [], + "kind": "List", + "metadata": { + "resourceVersion": "", + "selfLink": "" + } +} diff --git a/pkg/config/autodiscover/testdata/testpods_withlabel.json b/pkg/config/autodiscover/testdata/testpods_withlabel.json new file mode 100644 index 000000000..308774a01 --- /dev/null +++ b/pkg/config/autodiscover/testdata/testpods_withlabel.json @@ -0,0 +1,274 @@ +{ + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "creationTimestamp": "2021-11-16T21:42:18Z", + "generateName": "coredns-78fcd69978-", + "labels": { + "k8s-app": "kube-dns", + "pod-template-hash": "78fcd69978", + "testprefix/testname": "testvalue" + }, + "name": "coredns-78fcd69978-cc94v", + "namespace": "kube-system", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "coredns-78fcd69978", + "uid": "527d93cd-fb69-4b85-9eff-a044f18b64fb" + } + ], + "resourceVersion": "1069", + "uid": "9365d337-041e-445c-a1e8-77e218bb96a1" + }, + "spec": { + "containers": [ + { + "args": [ + "-conf", + "/etc/coredns/Corefile" + ], + "image": "k8s.gcr.io/coredns/coredns:v1.8.4", + "imagePullPolicy": "IfNotPresent", + "livenessProbe": { + "failureThreshold": 5, + "httpGet": { + "path": "/health", + "port": 8080, + "scheme": "HTTP" + }, + "initialDelaySeconds": 60, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 5 + }, + "name": "coredns", + "ports": [ + { + "containerPort": 53, + "name": "dns", + "protocol": "UDP" + }, + { + "containerPort": 53, + "name": "dns-tcp", + "protocol": "TCP" + }, + { + "containerPort": 9153, + "name": "metrics", + "protocol": "TCP" + } + ], + "readinessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/ready", + "port": 8181, + "scheme": "HTTP" + }, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "resources": { + "limits": { + "memory": "170Mi" + }, + "requests": { + "cpu": "100m", + "memory": "70Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "add": [ + "NET_BIND_SERVICE" + ], + "drop": [ + "all" + ] + }, + "readOnlyRootFilesystem": true + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/etc/coredns", + "name": "config-volume", + "readOnly": true + }, + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "kube-api-access-cb6f4", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "Default", + "enableServiceLinks": true, + "nodeName": "minikube", + "nodeSelector": { + "kubernetes.io/os": "linux" + }, + "preemptionPolicy": "PreemptLowerPriority", + "priority": 2000000000, + "priorityClassName": "system-cluster-critical", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "coredns", + "serviceAccountName": "coredns", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "key": "CriticalAddonsOnly", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "node-role.kubernetes.io/master" + }, + { + "effect": "NoSchedule", + "key": "node-role.kubernetes.io/control-plane" + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "configMap": { + "defaultMode": 420, + "items": [ + { + "key": "Corefile", + "path": "Corefile" + } + ], + "name": "coredns" + }, + "name": "config-volume" + }, + { + "name": "kube-api-access-cb6f4", + "projected": { + "defaultMode": 420, + "sources": [ + { + "serviceAccountToken": { + "expirationSeconds": 3607, + "path": "token" + } + }, + { + "configMap": { + "items": [ + { + "key": "ca.crt", + "path": "ca.crt" + } + ], + "name": "kube-root-ca.crt" + } + }, + { + "downwardAPI": { + "items": [ + { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + }, + "path": "namespace" + } + ] + } + } + ] + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T21:42:36Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T21:42:39Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T21:42:39Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T21:42:36Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://cf794b9e8c2448815b8b5a47b354c9bf9414a04f6fa567ac3b059851ed6757ab", + "image": "k8s.gcr.io/coredns/coredns:v1.8.4", + "imageID": "docker-pullable://k8s.gcr.io/coredns/coredns@sha256:6e5a02c21641597998b4be7cb5eb1e7b02c0d8d23cce4dd09f4682d463798890", + "lastState": {}, + "name": "coredns", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2021-11-16T21:42:37Z" + } + } + } + ], + "hostIP": "192.168.49.2", + "phase": "Running", + "podIP": "10.244.0.2", + "podIPs": [ + { + "ip": "10.244.0.2" + } + ], + "qosClass": "Burstable", + "startTime": "2021-11-16T21:42:36Z" + } + } + ], + "kind": "List", + "metadata": { + "resourceVersion": "", + "selfLink": "" + } +} diff --git a/pkg/config/autodiscover/testdata/testtarget.json b/pkg/config/autodiscover/testdata/testtarget.json index 274d34b53..afa39c2f0 100644 --- a/pkg/config/autodiscover/testdata/testtarget.json +++ b/pkg/config/autodiscover/testdata/testtarget.json @@ -2,23 +2,23 @@ { "metadata": { "annotations": { - "k8s.v1.cni.cncf.io/networks-status": "[{\n \"name\": \"\",\n \"interface\": \"eth1\",\n \"ips\": [\n \"10.217.1.89\"\n ],\n \"default\": true,\n \"dns\": {}\n}]", - "test-network-function.com/multusips": "[\"3.3.3.3\",\"4.4.4.4\"]", - "test-network-function.com/container_tests": "[\"OneTestName\",\"AnotherTestName\"]" + "k8s.v1.cni.cncf.io/networks-status": "[{\n \"name\": \"k8s-pod-network\",\n \"ips\": [\n \"10.244.205.205\"\n ],\n \"default\": true,\n \"dns\": {}\n},{\n \"name\": \"default/macvlan-conf1\",\n \"interface\": \"net1\",\n \"ips\": [\n \"3.3.3.3\"\n ],\n \"mac\": \"62:a2:5a:1f:80:15\",\n \"dns\": {}\n},{\n \"name\": \"default/macvlan-conf2\",\n \"interface\": \"net2\",\n \"ips\": [\n \"4.4.4.4\"\n ],\n \"mac\": \"62:a2:5a:1f:80:16\",\n \"dns\": {}\n}]", + "test-network-function.com/host_resource_tests": "[\"OneTestName\",\"AnotherTestName\"]", + "test-network-function.com/defaultnetworkinterface": "\"eth0\"" }, "labels": { "app": "test", "test-network-function.com/generic": "target", "test-network-function.com/container": "target" }, - "name": "test", + "name": "I'mAPodName", "namespace": "tnf" }, "spec": { "containers": [ { "image": "quay.io/testnetworkfunction/cnf-test-partner:latest", - "name": "test" + "name": "I'mAContainer" } ] }, diff --git a/pkg/config/config.go b/pkg/config/config.go index 0dd616370..c5d1220ed 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,171 +18,354 @@ package config import ( "fmt" - "io/ioutil" "os" + "time" - ginkgoconfig "github.com/onsi/ginkgo/config" + "github.com/onsi/gomega" log "github.com/sirupsen/logrus" "github.com/test-network-function/test-network-function/pkg/config/autodiscover" "github.com/test-network-function/test-network-function/pkg/config/configsections" - "github.com/test-network-function/test-network-function/pkg/tnf/testcases" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" "gopkg.in/yaml.v2" ) const ( configurationFilePathEnvironmentVariableKey = "TNF_CONFIGURATION_PATH" defaultConfigurationFilePath = "tnf_config.yml" + defaultTimeoutSeconds = 10 ) -const ( - containerTestSpecName = "access-control" - operatorTestSpecName = "operator" +var ( + // testEnvironment is the singleton instance of `TestEnvironment`, accessed through `GetTestEnvironment` + testEnvironment TestEnvironment + expectersVerboseModeEnabled = false ) -// File is the top level of the config file. All new config sections must be added here -type File struct { - // Custom Pod labels for discovering containers under test for generic and container suites - TargetPodLabels []configsections.Label `yaml:"targetPodLabels,omitempty" json:"targetPodLabels,omitempty"` +// getConfigurationFilePathFromEnvironment returns the test configuration file. +func getConfigurationFilePathFromEnvironment() string { + environmentSourcedConfigurationFilePath := os.Getenv(configurationFilePathEnvironmentVariableKey) + if environmentSourcedConfigurationFilePath != "" { + return environmentSourcedConfigurationFilePath + } + return defaultConfigurationFilePath +} - Generic configsections.TestConfiguration `yaml:"generic,omitempty" json:"generic,omitempty"` +type NodeConfig struct { + // same Name as the one inside Node structure + Name string + Node configsections.Node + // pointer to the container of the debug pod running on the node + DebugContainer *configsections.Container + // podset indicates if the node has a podset,deployment/statefulset + podset bool + // debug indicates if the node should have a debug pod + debug bool +} - // Operator is the list of operator objects that needs to be tested. - Operators []configsections.Operator `yaml:"operators,omitempty" json:"operators,omitempty"` +func (n NodeConfig) IsMaster() bool { + return n.Node.IsMaster() +} - // CNFs is the list of the CNFs that needs to be tested. Each entry is a single pod to be tested. - CNFs []configsections.Cnf `yaml:"cnfs,omitempty" json:"cnfs,omitempty"` +func (n NodeConfig) IsWorker() bool { + return n.Node.IsWorker() +} - // CertifiedContainerInfo is the list of container images to be checked for certification status. - CertifiedContainerInfo []configsections.CertifiedContainerRequestInfo `yaml:"certifiedcontainerinfo,omitempty" json:"certifiedcontainerinfo,omitempty"` +func (n NodeConfig) HasPodset() bool { + return n.podset +} +func (n NodeConfig) HasDebugPod() bool { + return n.DebugContainer != nil +} - // CertifiedOperatorInfo is list of operator bundle names that are queried for certification status. - CertifiedOperatorInfo []configsections.CertifiedOperatorRequestInfo `yaml:"certifiedoperatorinfo,omitempty" json:"certifiedoperatorinfo,omitempty"` +// DefaultTimeout for creating new interactive sessions (oc, ssh, tty) +var DefaultTimeout = time.Duration(defaultTimeoutSeconds) * time.Second - // CnfAvailableTestCases list the available test cases for reference. - CnfAvailableTestCases []string `yaml:"cnfavailabletestcases,omitempty" json:"cnfavailabletestcases,omitempty"` -} +// TestEnvironment includes the representation of the current state of the test targets and partners as well as the test configuration +type TestEnvironment struct { + ContainersUnderTest map[configsections.ContainerIdentifier]*configsections.Container + PartnerContainers map[configsections.ContainerIdentifier]*configsections.Container + DebugContainers map[configsections.ContainerIdentifier]*configsections.Container + PodsUnderTest []*configsections.Pod + DeploymentsUnderTest []configsections.PodSet + StateFulSetUnderTest []configsections.PodSet + OperatorsUnderTest []*configsections.Operator + HelmchartsUnderTest []configsections.HelmChart + NameSpacesUnderTest []string + CrdNames []string + NodesUnderTest map[string]*NodeConfig -var ( - // configInstance is the singleton instance of loaded config, accessed through GetConfigInstance - configInstance File + // ContainersToExcludeFromConnectivityTests is a set used for storing the containers that should be excluded from + // connectivity testing. + ContainersToExcludeFromConnectivityTests map[configsections.ContainerIdentifier]interface{} + // ContainersToExcludeFromMultusConnectivityTests is a set used for storing the containers that should be excluded from + // Multus connectivity testing. + ContainersToExcludeFromMultusConnectivityTests map[configsections.ContainerIdentifier]interface{} + Config configsections.TestConfiguration // loaded tracks if the config has been loaded to prevent it being reloaded. - loaded = false + loaded bool // set when an intrusive test has done something that would cause Pod/Container to be recreated - needsRefresh = false -) + needsRefresh bool + // context for executing command in local shell + localShell *interactive.Context +} -// getConfigurationFilePathFromEnvironment returns the test configuration file. -func getConfigurationFilePathFromEnvironment() string { - environmentSourcedConfigurationFilePath := os.Getenv(configurationFilePathEnvironmentVariableKey) - if environmentSourcedConfigurationFilePath != "" { - return environmentSourcedConfigurationFilePath +func (env *TestEnvironment) GetLocalShellContext() *interactive.Context { + if env.localShell == nil { + context, err := interactive.SpawnShell(interactive.CreateGoExpectSpawner(), DefaultTimeout, interactive.Verbose(expectersVerboseModeEnabled), interactive.SendTimeout(DefaultTimeout)) + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(context).ToNot(gomega.BeNil()) + gomega.Expect(context.GetExpecter()).ToNot(gomega.BeNil()) + env.localShell = context + } + return env.localShell +} + +func (env *TestEnvironment) CloseLocalShellContext() { + if env.localShell != nil { + err := (*env.localShell.GetExpecter()).Close() + if err != nil { + log.Warnf("Failed to close local shell context due to %v", err) + } + env.localShell = nil } - return defaultConfigurationFilePath } // loadConfigFromFile loads a config file once. -func loadConfigFromFile(filePath string) error { - if loaded { +func (env *TestEnvironment) loadConfigFromFile(filePath string) error { + if env.loaded { return fmt.Errorf("cannot load config from file when a config is already loaded") } log.Info("Loading config from file: ", filePath) - contents, err := ioutil.ReadFile(filePath) + contents, err := os.ReadFile(filePath) if err != nil { return err } - err = yaml.Unmarshal(contents, &configInstance) + err = yaml.Unmarshal(contents, &env.Config) if err != nil { return err } - loaded = true + env.loaded = true return nil } -// doAutodiscovery will autodiscover config for any enabled test spec. Specs which are not selected will be skipped to -// avoid unnecessary noise in the logs. -func doAutodiscovery() { - if genericTestConfigRequired() { - configInstance.Generic = autodiscover.BuildGenericConfig() - } - if podTestConfigRequired() { - configInstance.CNFs = autodiscover.BuildCNFsConfig() - } - if testcases.IsInFocus(ginkgoconfig.GinkgoConfig.FocusStrings, operatorTestSpecName) { - configInstance.Operators = autodiscover.BuildOperatorConfig() - } -} - -// GetConfigInstance provides access to the singleton ConfigFile instance. -func GetConfigInstance() File { - if !loaded { +// LoadAndRefresh loads the config file if not loaded already and performs autodiscovery if needed +func (env *TestEnvironment) LoadAndRefresh() { + if !env.loaded { filePath := getConfigurationFilePathFromEnvironment() log.Debugf("GetConfigInstance before config loaded, loading from file: %s", filePath) - err := loadConfigFromFile(filePath) + err := env.loadConfigFromFile(filePath) if err != nil { log.Fatalf("unable to load configuration file: %s", err) } - - BuildConfig() - } else if needsRefresh { - BuildConfig() + env.doAutodiscover() + } else if env.needsRefresh { + env.reset() + env.doAutodiscover() } - return configInstance } -func findContainersByLabels(labels []configsections.Label) (containers []configsections.Container) { - for _, l := range labels { - list, err := autodiscover.GetContainersByLabel(l) - if err == nil { - containers = append(containers, list...) - } else { - log.Warnf("failed to query by label: %v %v", l, err) +// Resets the environment during the drain test since all the connections are affected +func (env *TestEnvironment) reset() { + log.Debug("clean up environment Test structure") + env.ResetOc() + env.Config.Partner = configsections.TestPartner{} + env.Config.TestTarget = configsections.TestTarget{} + // Delete Oc debug sessions before re-creating them + for name, node := range env.NodesUnderTest { + if node.debug { + autodiscover.DeleteDebugLabel(name) } } - return containers + env.NameSpacesUnderTest = nil + env.NodesUnderTest = nil + env.Config.Nodes = nil + env.DebugContainers = nil } -func findPodsByLabels(labels []configsections.Label) (cnfs []configsections.Cnf) { - for _, l := range labels { - pods, err := autodiscover.GetPodsByLabel(l) - if err == nil { - for i := range pods.Items { - cnfs = append(cnfs, autodiscover.BuildCnfFromPodResource(&pods.Items[i])) - } - } else { - log.Warnf("failed to query by label: %v %v", l, err) +// Resets the environment during the intrusive tests since all the connections are affected +func (env *TestEnvironment) ResetOc() { + log.Debug("Reset Oc sessions") + // Delete Oc debug sessions before re-creating them + for _, node := range env.NodesUnderTest { + if node.HasDebugPod() { + log.Infof("Closing session to node %s", node.Name) + node.DebugContainer.CloseOc() } } - return cnfs + // Delete all remaining sessions before re-creating them + for _, cut := range env.ContainersUnderTest { + cut.CloseOc() + } + + // Delete all remaining partner sessions before re-creating them + for _, cut := range env.PartnerContainers { + cut.CloseOc() + } } -// BuildConfig does auto discovery based on default labels if enabled and additional target pod/container -// discovery based on custom labels -func BuildConfig() { +func (env *TestEnvironment) doAutodiscover() { + log.Debug("start auto discovery") + for _, ns := range env.Config.TargetNameSpaces { + env.NameSpacesUnderTest = append(env.NameSpacesUnderTest, ns.Name) + } + if autodiscover.PerformAutoDiscovery() { - log.Warn("doing configuration autodiscovery. Currently this WILL override parts of the configuration file") - doAutodiscovery() + autodiscover.FindTestTarget(env.Config.TargetPodLabels, &env.Config.TestTarget, env.NameSpacesUnderTest, env.Config.SkipHelmChartList) + } + + env.ContainersToExcludeFromConnectivityTests = make(map[configsections.ContainerIdentifier]interface{}) + env.ContainersToExcludeFromMultusConnectivityTests = make(map[configsections.ContainerIdentifier]interface{}) + + for _, cid := range env.Config.ExcludeContainersFromConnectivityTests { + env.ContainersToExcludeFromConnectivityTests[cid] = "" + } + for _, cid := range env.Config.ExcludeContainersFromMultusConnectivityTests { + env.ContainersToExcludeFromMultusConnectivityTests[cid] = "" + } + env.ContainersUnderTest = env.createContainerMapWithOcSession(env.Config.ContainerList) + env.PodsUnderTest = env.Config.PodsUnderTest + + // Discover nodes early on since they might be used to run commands by discovery + // But after getting a node list in FindTestTarget() and a container under test list in env.ContainersUnderTest + env.discoverNodes() + + for _, cid := range env.Config.Partner.ContainersDebugList { + env.ContainersToExcludeFromConnectivityTests[cid.ContainerIdentifier] = "" + env.ContainersToExcludeFromMultusConnectivityTests[cid.ContainerIdentifier] = "" + } + env.DeploymentsUnderTest = env.Config.DeploymentsUnderTest + env.StateFulSetUnderTest = env.Config.StateFulSetUnderTest + env.OperatorsUnderTest = env.Config.Operators + env.HelmchartsUnderTest = env.Config.HelmChart + env.CrdNames = autodiscover.FindTestCrdNames(env.Config.CrdFilters) + + log.Infof("Test Configuration: %+v", *env) + + env.needsRefresh = false +} + +// labelNodes add label to specific nodes so that node selector in debug daemonset +// can be scheduled +func (env *TestEnvironment) labelNodes() { + var masterNode, workerNode string + // make sure at least one worker and one master has debug set to true + for name, node := range env.NodesUnderTest { + if node.IsMaster() && masterNode == "" { + masterNode = name + } + if node.IsMaster() && node.debug { + masterNode = "" + break + } + } + for name, node := range env.NodesUnderTest { + if node.IsWorker() && workerNode == "" { + workerNode = name + } + if node.IsWorker() && node.debug { + workerNode = "" + break + } + } + if masterNode != "" { + env.NodesUnderTest[masterNode].debug = true + } + if workerNode != "" { + env.NodesUnderTest[workerNode].debug = true + } + // label all nodes + for nodeName, node := range env.NodesUnderTest { + if node.debug { + autodiscover.AddDebugLabel(nodeName) + } } - if genericTestConfigRequired() { - configInstance.Generic.ContainersUnderTest = append(configInstance.Generic.ContainersUnderTest, findContainersByLabels(configInstance.TargetPodLabels)...) +} + +// create Nodes data from podset +func (env *TestEnvironment) createNodes(nodes map[string]configsections.Node) map[string]*NodeConfig { + log.Debug("autodiscovery: create nodes start") + defer log.Debug("autodiscovery: create nodes done") + nodesConfig := make(map[string]*NodeConfig) + for _, n := range nodes { + nodesConfig[n.Name] = &NodeConfig{Node: n, Name: n.Name} } - if podTestConfigRequired() { - configInstance.CNFs = append(configInstance.CNFs, findPodsByLabels(configInstance.TargetPodLabels)...) + for _, c := range env.ContainersUnderTest { + nodeName := c.NodeName + if _, ok := nodesConfig[nodeName]; ok { + nodesConfig[nodeName].podset = true + nodesConfig[nodeName].debug = true + } else { + log.Warn("node ", nodeName, " has podset, but not the right labels") + } } - needsRefresh = false + return nodesConfig } -func genericTestConfigRequired() bool { - // TODO clean up as part of config api refactoring task - return true +// attach debug pod session to node session +func (env *TestEnvironment) AttachDebugPodsToNodes() { + for _, c := range env.DebugContainers { + nodeName := c.NodeName + if _, ok := env.NodesUnderTest[nodeName]; ok { + env.NodesUnderTest[nodeName].DebugContainer = c + } + } } -func podTestConfigRequired() bool { - return testcases.IsInFocus(ginkgoconfig.GinkgoConfig.FocusStrings, containerTestSpecName) +// discoverNodes find all the nodes in the cluster +// label the ones with deployment +// attach them to debug pods +func (env *TestEnvironment) discoverNodes() { + env.NodesUnderTest = env.createNodes(env.Config.Nodes) + + expectedDebugPods := 0 + // Wait for the previous deployment's pod to fully terminate + autodiscover.CheckDebugDaemonset(expectedDebugPods) + env.labelNodes() + + for _, node := range env.NodesUnderTest { + if node.debug { + expectedDebugPods++ + } + } + autodiscover.CheckDebugDaemonset(expectedDebugPods) + autodiscover.FindDebugPods(&env.Config.Partner) + for _, debugPod := range env.Config.Partner.ContainersDebugList { + env.ContainersToExcludeFromConnectivityTests[debugPod.ContainerIdentifier] = "" + env.ContainersToExcludeFromMultusConnectivityTests[debugPod.ContainerIdentifier] = "" + } + env.DebugContainers = env.createContainerMapWithOcSession(env.Config.Partner.ContainersDebugList) + + env.AttachDebugPodsToNodes() +} + +// createContainerMapWithOcSession contains the general steps involved in creating "oc" sessions and other configuration. A map of the +// aggregate information is returned. +func (env *TestEnvironment) createContainerMapWithOcSession(containers []configsections.Container) map[configsections.ContainerIdentifier]*configsections.Container { + containerMap := make(map[configsections.ContainerIdentifier]*configsections.Container) + for i := range containers { + c := &containers[i] + log.Debugf("Creating shell session for pod %s - container %s (ns %s)", c.PodName, c.ContainerName, c.Namespace) + c.Oc = configsections.GetOcSession(c.PodName, c.ContainerName, c.Namespace, DefaultTimeout, interactive.Verbose(expectersVerboseModeEnabled), interactive.SendTimeout(DefaultTimeout)) + containerMap[c.ContainerIdentifier] = c + } + return containerMap } // SetNeedsRefresh marks the config stale so that the next getInstance call will redo discovery -func SetNeedsRefresh() { - needsRefresh = true +func (env *TestEnvironment) SetNeedsRefresh() { + env.needsRefresh = true +} + +// GetTestEnvironment provides the current state of test environment +func GetTestEnvironment() *TestEnvironment { + return &testEnvironment +} + +// EnableExpectersVerboseMode enables the verbose mode for expecters (Sent/Match output) +func EnableExpectersVerboseMode() { + expectersVerboseModeEnabled = true + + autodiscover.EnableExpectersVerboseMode() } diff --git a/pkg/config/config_instance_test.go b/pkg/config/config_instance_test.go new file mode 100644 index 000000000..2a9456ce2 --- /dev/null +++ b/pkg/config/config_instance_test.go @@ -0,0 +1,60 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/config/configsections" +) + +const ( + filePath = "testdata/tnf_test_config.yml" +) + +const ( + testDeploymentsNumber = 1 + testDeploymentName = "test" + testDeploymentNamespace = "default" + testDeploymentReplicas = 2 + + testCrdsNumber = 2 + testCrdNameSuffix1 = "group1.test1.com" + testCrdNameSuffix2 = "test2.com" +) + +func testLoadedDeployments(t *testing.T, deployments []configsections.PodSet) { + assert.Equal(t, len(deployments), testDeploymentsNumber) + assert.Equal(t, deployments[0].Name, testDeploymentName) + assert.Equal(t, deployments[0].Namespace, testDeploymentNamespace) + assert.Equal(t, deployments[0].Replicas, testDeploymentReplicas) +} + +func testLoadedCrds(t *testing.T, crds []configsections.CrdFilter) { + assert.Equal(t, len(crds), testCrdsNumber) + assert.Equal(t, crds[0].NameSuffix, testCrdNameSuffix1) + assert.Equal(t, crds[1].NameSuffix, testCrdNameSuffix2) +} + +func TestLoadConfigFromFile(t *testing.T) { + env := GetTestEnvironment() + assert.Nil(t, env.loadConfigFromFile(filePath)) + assert.NotNil(t, env.loadConfigFromFile(filePath)) // Loading when already loaded is an error case + testLoadedDeployments(t, env.Config.DeploymentsUnderTest) + testLoadedCrds(t, env.Config.CrdFilters) +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index ffacf749c..6898184d9 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,26 +17,293 @@ package config import ( + "os" "testing" + "time" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/config/configsections" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" + "github.com/test-network-function/test-network-function/pkg/utils" ) -const ( - filePath = "testdata/tnf_test_config.yml" -) +func TestGetConfigurationFilePathFromEnvironment(t *testing.T) { + defer os.Unsetenv(configurationFilePathEnvironmentVariableKey) + testCases := []struct { + envTestPath string + expectedPath string + }{ + { // Custom config + envTestPath: "testconfig.yml", + expectedPath: "testconfig.yml", + }, + { // Default config + envTestPath: "", + expectedPath: defaultConfigurationFilePath, + }, + } + + for _, tc := range testCases { + os.Setenv(configurationFilePathEnvironmentVariableKey, tc.envTestPath) + assert.Equal(t, tc.expectedPath, getConfigurationFilePathFromEnvironment()) + } +} + +func TestIsMaster(t *testing.T) { + testCases := []struct { + label []string + expectedMaster bool + }{ + { + label: []string{ + configsections.MasterLabel, + }, + expectedMaster: true, + }, + { + label: []string{ + configsections.WorkerLabel, + }, + expectedMaster: false, + }, + { // Check if a master is also labeled as a worker, still considered a master. + label: []string{ + configsections.MasterLabel, + configsections.WorkerLabel, + }, + expectedMaster: true, + }, + } + + for _, tc := range testCases { + n := NodeConfig{ + Node: configsections.Node{ + Labels: tc.label, + }, + } + assert.Equal(t, tc.expectedMaster, n.IsMaster()) + } +} + +func TestIsWorker(t *testing.T) { + testCases := []struct { + label []string + expectedWorker bool + }{ + { + label: []string{ + configsections.MasterLabel, + }, + expectedWorker: false, + }, + { + label: []string{ + configsections.WorkerLabel, + }, + expectedWorker: true, + }, + { // Check if a master labeled a worker is still considered a worker. + label: []string{ + configsections.WorkerLabel, + configsections.MasterLabel, + }, + expectedWorker: true, + }, + } + + for _, tc := range testCases { + n := NodeConfig{ + Node: configsections.Node{ + Labels: tc.label, + }, + } + assert.Equal(t, tc.expectedWorker, n.IsWorker()) + } +} + +func TestHasPodset(t *testing.T) { + testCases := []struct { + podset bool + }{ + { + podset: true, + }, + { + podset: false, + }, + } + + for _, tc := range testCases { + n := NodeConfig{ + podset: tc.podset, + } + assert.Equal(t, tc.podset, n.HasPodset()) + } +} + +func TestHasDebugPod(t *testing.T) { + testCases := []struct { + hasDebug bool + }{ + { + hasDebug: true, + }, + { + hasDebug: false, + }, + } -func TestLoadConfigFromFile(t *testing.T) { - assert.Nil(t, loadConfigFromFile(filePath)) - assert.NotNil(t, loadConfigFromFile(filePath)) // Loading when already loaded is an error case - conf := GetConfigInstance() - assert.Equal(t, conf.Generic.TestOrchestrator.Namespace, "default") - assert.Equal(t, conf.Generic.TestOrchestrator.ContainerName, "partner") - assert.Equal(t, conf.Generic.TestOrchestrator.PodName, "partner") + for _, tc := range testCases { + var n NodeConfig + if tc.hasDebug { + n.DebugContainer = &configsections.Container{} + assert.True(t, n.HasDebugPod()) + } else { + n.DebugContainer = nil + assert.False(t, n.HasDebugPod()) + } + } } -func TestGetConfigInstance(t *testing.T) { - _ = loadConfigFromFile(filePath) - assert.NotNil(t, GetConfigInstance()) - assert.Equal(t, GetConfigInstance(), GetConfigInstance()) +func TestCreateNodes(t *testing.T) { + nodes := map[string]configsections.Node{ + "master1": { + Name: "master1", + Labels: []string{ + configsections.MasterLabel, + }, + }, + } + expectedNodeConfig := map[string]*NodeConfig{ + "master1": { + podset: true, + Node: configsections.Node{ + Name: "master1", + Labels: []string{ + configsections.MasterLabel, + }, + }, + Name: "master1", + DebugContainer: nil, + }, + } + + env := &TestEnvironment{} + env.ContainersUnderTest = map[configsections.ContainerIdentifier]*configsections.Container{ + { + NodeName: "mynode1", + }: { + ContainerIdentifier: configsections.ContainerIdentifier{ + NodeName: "mynode1", + }, + }, + } + createdNodes := env.createNodes(nodes) + assert.Equal(t, expectedNodeConfig["master1"].Name, createdNodes["master1"].Name) + assert.Equal(t, expectedNodeConfig["master1"].Node, createdNodes["master1"].Node) + assert.Equal(t, expectedNodeConfig["master1"].Node.Labels, createdNodes["master1"].Node.Labels) + assert.Equal(t, expectedNodeConfig["master1"].DebugContainer, createdNodes["master1"].DebugContainer) + assert.False(t, createdNodes["master1"].HasPodset()) +} + +func TestSetNeedsRefresh(t *testing.T) { + testEnv := &TestEnvironment{} + testEnv.SetNeedsRefresh() + assert.True(t, testEnv.needsRefresh) +} + +func TestAttachDebugPodsToNodes(t *testing.T) { + testEnv := &TestEnvironment{ + DebugContainers: map[configsections.ContainerIdentifier]*configsections.Container{ + { + PodName: "debug1", + NodeName: "node1", + }: { + ContainerIdentifier: configsections.ContainerIdentifier{ + PodName: "debug1", + NodeName: "node1", + }, + }, + }, + NodesUnderTest: map[string]*NodeConfig{ + "node1": { + Name: "node1", + }, + }, + } + + testEnv.AttachDebugPodsToNodes() + assert.Equal(t, "debug1", testEnv.NodesUnderTest["node1"].DebugContainer.PodName) +} + +func TestReset(t *testing.T) { + gomega.RegisterFailHandler(ginkgo.Fail) + defer ginkgo.GinkgoRecover() + testEnv := &TestEnvironment{} + testEnv.NodesUnderTest = map[string]*NodeConfig{ + "node1": { + Name: "node1", + Node: configsections.Node{ + Labels: []string{ + configsections.WorkerLabel, + }, + }, + debug: true, + }, + } + origFunc := utils.ExecuteCommandAndValidate + utils.ExecuteCommandAndValidate = func(command string, timeout time.Duration, context *interactive.Context, failureCallbackFun func()) string { + return "" + } + defer func() { + utils.ExecuteCommandAndValidate = origFunc + }() + testEnv.reset() + assert.Equal(t, testEnv.Config.Partner, configsections.TestPartner{}) + assert.Equal(t, testEnv.Config.TestTarget, configsections.TestTarget{}) + assert.Nil(t, testEnv.NodesUnderTest["node1"].Node.Labels) + assert.Nil(t, testEnv.NameSpacesUnderTest) + assert.Nil(t, testEnv.NodesUnderTest) + assert.Nil(t, testEnv.Config.Nodes) + assert.Nil(t, testEnv.DebugContainers) +} + +func TestLabelNodes(t *testing.T) { + gomega.RegisterFailHandler(ginkgo.Fail) + defer ginkgo.GinkgoRecover() + testEnv := &TestEnvironment{} + testEnv.NodesUnderTest = map[string]*NodeConfig{ + "node1": { + Name: "node1", + Node: configsections.Node{ + Labels: []string{ + configsections.WorkerLabel, + }, + }, + // debug: true, + }, + "node2": { + Name: "node2", + Node: configsections.Node{ + Labels: []string{ + configsections.MasterLabel, + }, + }, + // debug: true, + }, + } + + origFunc := utils.ExecuteCommandAndValidate + utils.ExecuteCommandAndValidate = func(command string, timeout time.Duration, context *interactive.Context, failureCallbackFun func()) string { + return "" + } + defer func() { + utils.ExecuteCommandAndValidate = origFunc + }() + + testEnv.labelNodes() + assert.True(t, testEnv.NodesUnderTest["node1"].debug) + assert.True(t, testEnv.NodesUnderTest["node2"].debug) } diff --git a/pkg/config/configsections/certified_request_test.go b/pkg/config/configsections/certified_request_test.go index c54bd9c0c..df93dfd4f 100644 --- a/pkg/config/configsections/certified_request_test.go +++ b/pkg/config/configsections/certified_request_test.go @@ -14,18 +14,15 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -package configsections_test +package configsections import ( "encoding/json" - "io/ioutil" "log" "os" "testing" "github.com/stretchr/testify/assert" - "github.com/test-network-function/test-network-function/pkg/config" - "github.com/test-network-function/test-network-function/pkg/config/configsections" "gopkg.in/yaml.v2" ) @@ -40,25 +37,29 @@ type unmarshalFunc func([]byte, interface{}) error // test data var ( // bananas go in the fruit bowl. - fruitbowlRequestInfo = configsections.CertifiedContainerRequestInfo{ + fruitbowlRequestInfo = ContainerImageIdentifier{ Name: "banana", Repository: "fruitbowl", } // apples go in the fridge. - fridgeRequestInfo = configsections.CertifiedContainerRequestInfo{ + fridgeRequestInfo = ContainerImageIdentifier{ Name: "apple", Repository: "fridge", } - jenkinsOperatorRequestInfo = configsections.CertifiedOperatorRequestInfo{ + jenkinsOperatorRequestInfo = CertifiedOperatorRequestInfo{ Name: "jenkins", Organization: "Red Hat", } - etcdOperatorRequestInfo = configsections.CertifiedOperatorRequestInfo{ + etcdOperatorRequestInfo = CertifiedOperatorRequestInfo{ Name: "etcd", Organization: "Core OS", } + + acceptedKernelTaintsInfo = AcceptedKernelTaintsInfo{ + Module: "taint1", + } ) var ( @@ -68,7 +69,7 @@ var ( // setupRequestTest writes the result of `populateRequestConfig` to a temporary file for loading in a test. func setupRequestTest(marshalFun marshalFunc) (tempfileName string) { - tempfile, err := ioutil.TempFile(".", tmpfileNameBase) + tempfile, err := os.CreateTemp(".", tmpfileNameBase) if err != nil { log.Fatal(err) } @@ -78,14 +79,14 @@ func setupRequestTest(marshalFun marshalFunc) (tempfileName string) { return tempfile.Name() } -// loadRequestConfig reads `tmpPath`, unmarshals it using `unmarshalFun`, and returns the resulting `config.File`. -func loadRequestConfig(tmpPath string, unmarshalFun unmarshalFunc) (conf *config.File) { - contents, err := ioutil.ReadFile(tmpPath) +// loadRequestConfig reads `tmpPath`, unmarshals it using `unmarshalFun`, and returns the resulting `TestConfiguration`. +func loadRequestConfig(tmpPath string, unmarshalFun unmarshalFunc) (conf *TestConfiguration) { + contents, err := os.ReadFile(tmpPath) if err != nil { log.Fatal(err) } - conf = &config.File{} + conf = &TestConfiguration{} err = unmarshalFun(contents, conf) if err != nil { log.Fatal(err) @@ -95,12 +96,12 @@ func loadRequestConfig(tmpPath string, unmarshalFun unmarshalFunc) (conf *config } // saveRequestConfig calls `marshalFun` on `c`, then writes the result to `configPath`. -func saveRequestConfig(marshalFun marshalFunc, c *config.File, configPath string) { +func saveRequestConfig(marshalFun marshalFunc, c *TestConfiguration, configPath string) { bytes, err := marshalFun(c) if err != nil { log.Fatal(err) } - err = ioutil.WriteFile(configPath, bytes, filePerm) + err = os.WriteFile(configPath, bytes, filePerm) if err != nil { log.Fatal(err) } @@ -113,16 +114,19 @@ func cleanupTempfiles() { tempFiles = make([]*os.File, 0) } -func buildRequestConfig() *config.File { - conf := &config.File{} - conf.CertifiedContainerInfo = []configsections.CertifiedContainerRequestInfo{ +func buildRequestConfig() *TestConfiguration { + conf := &TestConfiguration{} + conf.CertifiedContainerInfo = []ContainerImageIdentifier{ fruitbowlRequestInfo, fridgeRequestInfo, } - conf.CertifiedOperatorInfo = []configsections.CertifiedOperatorRequestInfo{ + conf.CertifiedOperatorInfo = []CertifiedOperatorRequestInfo{ jenkinsOperatorRequestInfo, etcdOperatorRequestInfo, } + conf.AcceptedKernelTaints = []AcceptedKernelTaintsInfo{ + acceptedKernelTaintsInfo, + } return conf } @@ -135,6 +139,7 @@ func RequestTest(t *testing.T, marshalFun marshalFunc, unmarshalFun unmarshalFun assert.Equal(t, len(cfg.CertifiedOperatorInfo), 2) assert.Equal(t, cfg.CertifiedOperatorInfo[0], jenkinsOperatorRequestInfo) assert.Equal(t, cfg.CertifiedOperatorInfo[1], etcdOperatorRequestInfo) + assert.Equal(t, cfg.AcceptedKernelTaints[0], acceptedKernelTaintsInfo) } func TestRequestInfos(t *testing.T) { diff --git a/pkg/config/configsections/common.go b/pkg/config/configsections/common.go index 16dc2fcd3..b004edbec 100644 --- a/pkg/config/configsections/common.go +++ b/pkg/config/configsections/common.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,7 +18,106 @@ package configsections // Label ns/name/value for resource lookup type Label struct { + Prefix string `yaml:"prefix" json:"prefix"` + Name string `yaml:"name" json:"name"` + Value string `yaml:"value" json:"value"` +} + +type InstallPlan struct { + // Operator's installPlan name + Name string `yaml:"name" json:"name"` + + // BundleImage is the URL referencing the bundle image + BundleImage string `yaml:"bundleImage" json:"bundleImage"` + + // IndexImage is the URL referencing the index image + IndexImage string `yaml:"indexImage" json:"indexImage"` +} + +// Operator struct defines operator manifest for testing +type Operator struct { + + // Name is a required field, Name of the csv . + Name string `yaml:"name" json:"name"` + + // Namespace is a required field , namespace is where the csv is installed. + // If its all namespace then you can replace it with ALL_NAMESPACE TODO: add check for ALL_NAMESPACE Namespace string `yaml:"namespace" json:"namespace"` - Name string `yaml:"name" json:"name"` - Value string `yaml:"value" json:"value"` + + // Tests this is list of test that need to run against the operator. + Tests []string `yaml:"tests" json:"tests"` + + // Subscription name is required field, Name of used subscription. + SubscriptionName string `yaml:"subscriptionName" json:"subscriptionName"` + + InstallPlans []InstallPlan `yaml:"installPlans,omitempty" json:"installPlans,omitempty"` + + Packag string `yaml:"packag" json:"packag"` + + Org string `yaml:"Org" json:"Org"` + + Version string `yaml:"Version" json:"Version"` +} + +// Namespace struct defines namespace properties +type Namespace struct { + Name string `yaml:"name" json:"name"` +} +type SkipHelmChartList struct { + Name string `yaml:"name" json:"name"` +} + +// TestConfiguration provides test related configuration +type TestConfiguration struct { + // Custom Pod labels for discovering containers/pods under test + TargetPodLabels []Label `yaml:"targetPodLabels,omitempty" json:"targetPodLabels,omitempty"` + // targetNameSpaces to be used in + TargetNameSpaces []Namespace `yaml:"targetNameSpaces" json:"targetNameSpaces"` + + // TestTarget contains k8s resources that can be targeted by tests + TestTarget `yaml:"testTarget" json:"testTarget"` + // TestPartner contains the helper containers that can be used to facilitate tests + Partner TestPartner `yaml:"testPartner" json:"testPartner"` + // CertifiedContainerInfo is the list of container images to be checked for certification status. + CertifiedContainerInfo []ContainerImageIdentifier `yaml:"certifiedcontainerinfo,omitempty" json:"certifiedcontainerinfo,omitempty"` + // CheckDiscoveredContainerCertificationStatus controls whether the container certification test will validate images used by autodiscovered containers, in addition to the configured image list + CheckDiscoveredContainerCertificationStatus bool `yaml:"checkDiscoveredContainerCertificationStatus" json:"checkDiscoveredContainerCertificationStatus"` + // CertifiedOperatorInfo is list of operator bundle names that are queried for certification status. + CertifiedOperatorInfo []CertifiedOperatorRequestInfo `yaml:"certifiedoperatorinfo,omitempty" json:"certifiedoperatorinfo,omitempty"` + // CRDs section. + CrdFilters []CrdFilter `yaml:"targetCrdFilters" json:"targetCrdFilters"` + // AcceptedKernelTaints + AcceptedKernelTaints []AcceptedKernelTaintsInfo `yaml:"acceptedKernelTaints,omitempty" json:"acceptedKernelTaints,omitempty"` + SkipHelmChartList []SkipHelmChartList `yaml:"skipHelmChartList,omitempty" json:"skipHelmChartList,omitempty"` +} + +// TestPartner contains the helper containers that can be used to facilitate tests +type TestPartner struct { + // DebugPods + ContainersDebugList []Container `yaml:"debugContainers,omitempty" json:"debugContainers,omitempty"` +} + +// TestTarget is a collection of resources under test +type TestTarget struct { + // DeploymentsUnderTest is the list of deployments that contain pods under test. + DeploymentsUnderTest []PodSet `yaml:"deploymentsUnderTest" json:"deploymentsUnderTest"` + // StateFulSetUnderTest is the list of statefulset that contain pods under test. + StateFulSetUnderTest []PodSet `yaml:"stateFulSetUnderTest" json:"stateFulSetUnderTest"` + // PodsUnderTest is the list of the pods that needs to be tested. Each entry is a single pod to be tested. + PodsUnderTest []*Pod `yaml:"podsUnderTest,omitempty" json:"podsUnderTest,omitempty"` + // NonValidPods contains a list of pods that share the same labels with Pods Under Test + // without belonging to namespaces under test + NonValidPods []*Pod + // ContainerConfigList is the list of containers that needs to be tested. + ContainerList []Container `yaml:"containersUnderTest" json:"containersUnderTest"` + // ExcludeContainersFromConnectivityTests excludes specific containers from network connectivity tests. This is particularly useful for containers that don't have ping available. + ExcludeContainersFromConnectivityTests []ContainerIdentifier `yaml:"ExcludeContainersFromConnectivityTests" json:"ExcludeContainersFromConnectivityTests"` + // ExcludeContainersFromMultusConnectivityTests excludes specific containers from network connectivity tests. This is particularly useful for containers that don't have ping available. + ExcludeContainersFromMultusConnectivityTests []ContainerIdentifier `yaml:"excludeContainersFromMultusConnectivityTests" json:"excludeContainersFromMultusConnectivityTests"` + // Operator is the list of operator objects that needs to be tested. + Operators []*Operator `yaml:"operators,omitempty" json:"operators,omitempty"` + HelmChart []HelmChart `yaml:"helm" json:"helm"` + // + // Node list + Nodes map[string]Node `yaml:"Nodes" json:"Nodes"` } diff --git a/pkg/config/configsections/config_section_test.go b/pkg/config/configsections/config_section_test.go new file mode 100644 index 000000000..d7020d977 --- /dev/null +++ b/pkg/config/configsections/config_section_test.go @@ -0,0 +1,273 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package configsections + +import ( + "encoding/json" + "log" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf/testcases" + "gopkg.in/yaml.v2" +) + +var ( + file *os.File + jsonFile *os.File + err error + test TestConfiguration +) + +const ( + // cnfConfig represents CNF configuration only + cnfConfig = "cnf_only_config" + // cnfName name of the cnf + cnfName = "cnf-test-one" + // crdNameOne name of the crd + crdNameSuffix1 = "group1.test.com" + // crdNameTwo name of the crd + crdNameSuffix2 = "group2.test.com" + // deploymentName is the name of the deployment + deploymentName = "deployment-one" + // deploymentReplicas no of replicas + deploymentReplicas = 1 + // fullConfig represents full configuration, including Operator and CNF + fullConfig = "full_config" + // operatorConfig represents operators configuration only + operatorConfig = "operator_only_config" + // operatorName name of the operator + operatorName = "etcdoperator.v0.9.4" + // operatorNameSpace is test namespace for an operator + operatorNameSpace = "my-etcd" + // testNameSpace k8s namespace + testNameSpace = "default" +) + +const ( + // filePerm is the permissions these tests will use when creating config files in test setup + filePerm = 0644 +) + +func saveConfig(c *TestConfiguration, configPath string) (err error) { + bytes, _ := yaml.Marshal(c) + if err != nil { + return + } + err = os.WriteFile(configPath, bytes, filePerm) + return +} + +func saveConfigAsJSON(c *TestConfiguration, configPath string) (err error) { + bytes, err := json.Marshal(c) + if err != nil { + return + } + err = os.WriteFile(configPath, bytes, filePerm) + return +} + +// newConfig returns a new decoded TnfContainerOperatorTestConfig struct +func newConfig(configPath string) (*TestConfiguration, error) { + // Create config structure + conf := &TestConfiguration{} + // Open config file + if file, err = os.Open(configPath); err != nil { + return nil, err + } + defer file.Close() + // Init new YAML decode + d := yaml.NewDecoder(file) + // Start YAML decoding from file + if err = d.Decode(&conf); err != nil { + return nil, err + } + return conf, nil +} + +func loadDeploymentsConfig() { + test.DeploymentsUnderTest = []PodSet{ + { + Name: deploymentName, + Namespace: testNameSpace, + Replicas: deploymentReplicas, + }, + } +} + +func loadPodConfig() { + test.PodsUnderTest = []*Pod{ + { + Name: cnfName, + Namespace: testNameSpace, + Tests: []string{testcases.PrivilegedPod}, + }, + } +} + +func loadOperatorConfig() { + operator := Operator{} + operator.Name = operatorName + operator.Namespace = operatorNameSpace + operator.Tests = []string{testcases.OperatorStatus} + test.Operators = append(test.Operators, &operator) + loadPodConfig() +} + +func loadCrds() { + test.CrdFilters = []CrdFilter{ + {NameSuffix: crdNameSuffix1}, + {NameSuffix: crdNameSuffix2}, + } +} + +func loadFullConfig() { + loadOperatorConfig() + loadPodConfig() + loadDeploymentsConfig() + loadCrds() +} + +func setup(configType string) { + file, err = os.CreateTemp(".", "test-config.yml") + if err != nil { + log.Fatal(err) + } + test = TestConfiguration{} + switch configType { + case fullConfig: + loadFullConfig() + case cnfConfig: + loadPodConfig() + loadDeploymentsConfig() + case operatorConfig: + loadOperatorConfig() + } + err = saveConfig(&test, file.Name()) + if err != nil { + log.Fatal(err) + } +} + +func setupJSON(configType string) { + jsonFile, err = os.CreateTemp(".", "test-json-config.json") + if err != nil { + log.Fatal(err) + } + test = TestConfiguration{} + switch configType { + case fullConfig: + loadFullConfig() + case cnfConfig: + loadPodConfig() + case operatorConfig: + loadOperatorConfig() + } + err = saveConfigAsJSON(&test, jsonFile.Name()) + if err != nil { + log.Fatal(err) + } +} + +func teardown() { + if file != nil { + os.Remove(file.Name()) + } + if jsonFile != nil { + os.Remove(jsonFile.Name()) + } +} + +func TestFullConfigLoad(t *testing.T) { + setup(fullConfig) + defer (teardown)() + cfg, err := newConfig(file.Name()) + assert.NotNil(t, cfg) + assert.Equal(t, len(cfg.Operators), 1) + assert.Equal(t, cfg.PodsUnderTest[0].Name, cnfName) + + assert.Equal(t, cfg.CrdFilters[0].NameSuffix, crdNameSuffix1) + assert.Equal(t, cfg.CrdFilters[1].NameSuffix, crdNameSuffix2) + + assert.Nil(t, err) +} + +func TestPodConfigLoad(t *testing.T) { + setup(cnfConfig) + defer (teardown)() + cfg, err := newConfig(file.Name()) + assert.NotNil(t, cfg) + assert.Equal(t, cfg.PodsUnderTest[0].Name, cnfName) + assert.Nil(t, err) +} + +func TestOperatorConfigLoad(t *testing.T) { + setup(operatorConfig) + defer (teardown)() + cfg, err := newConfig(file.Name()) + assert.NotNil(t, cfg) + assert.Equal(t, len(cfg.Operators), 1) + assert.Nil(t, err) +} + +func TestFullJsonConfig(t *testing.T) { + defer (teardown)() + // json + setupJSON(fullConfig) + jsonCfg, err := newConfig(jsonFile.Name()) + assert.NotNil(t, jsonCfg) + assert.Nil(t, err) + // yaml + setup(fullConfig) + yamlCfg, err := newConfig(file.Name()) + assert.Nil(t, err) + assert.NotNil(t, yamlCfg) + assert.Equal(t, yamlCfg.Operators, jsonCfg.Operators) + assert.Equal(t, yamlCfg.PodsUnderTest, jsonCfg.PodsUnderTest) + assert.Equal(t, yamlCfg.CrdFilters[0].NameSuffix, crdNameSuffix1) + assert.Equal(t, yamlCfg.CrdFilters[1].NameSuffix, crdNameSuffix2) +} + +func TestCnfJsonConfig(t *testing.T) { + defer (teardown)() + // json + setupJSON(cnfConfig) + jsonCfg, err := newConfig(jsonFile.Name()) + assert.NotNil(t, jsonCfg) + assert.Nil(t, err) + // yaml + setup(cnfConfig) + yamlCfg, err := newConfig(file.Name()) + assert.Nil(t, err) + assert.NotNil(t, yamlCfg) + assert.Equal(t, yamlCfg.PodsUnderTest, jsonCfg.PodsUnderTest) +} + +func TestOperatorJsonConfig(t *testing.T) { + defer (teardown)() + // json + setupJSON(operatorConfig) + jsonCfg, err := newConfig(jsonFile.Name()) + assert.NotNil(t, jsonCfg) + assert.Nil(t, err) + // yaml + setup(operatorConfig) + yamlCfg, err := newConfig(file.Name()) + assert.Nil(t, err) + assert.Equal(t, yamlCfg.Operators, jsonCfg.Operators) +} diff --git a/pkg/config/configsections/container.go b/pkg/config/configsections/container.go new file mode 100644 index 000000000..c4aeb3439 --- /dev/null +++ b/pkg/config/configsections/container.go @@ -0,0 +1,146 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package configsections + +import ( + "fmt" + "time" + + "github.com/onsi/gomega" + log "github.com/sirupsen/logrus" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" +) + +const ( + defaultTimeoutSeconds = 10 +) + +var ( + expectersVerboseModeEnabled = false + // DefaultTimeout for creating new interactive sessions (oc, ssh, tty) + DefaultTimeout = time.Duration(defaultTimeoutSeconds) * time.Second +) + +// Container is a construct which follows the Container design pattern. Essentially, a Container holds the +// pertinent information to perform a test against or using an Operating System Container. This includes facets such +// as the reference to the interactive.Oc instance, the reference to the test configuration, and the default network +// IP address. +type Container struct { + ContainerIdentifier `yaml:"ContainerIdentifier" json:"ContainerIdentifier"` + Oc *interactive.Oc `yaml:"-" json:"-"` + ImageSource *ContainerImageSource `yaml:"ImageSource" json:"ImageSource"` +} + +type ContainerImageSource struct { + Registry string `yaml:"Registry" json:"Registry"` + ContainerImageIdentifier `yaml:"ContainerImageIdentifier" json:"ContainerImageIdentifier"` +} + +// Tag and Digest should not be populated at the same time. Digest takes precedence if both are populated +type ContainerImageIdentifier struct { + // Name is the name of the image that you want to check if exists in the RedHat catalog + Name string `yaml:"name" json:"name"` + + // Repository is the name of the repository `rhel8` of the container + // This is valid for container only and required field + Repository string `yaml:"repository" json:"repository"` + + // Tag is the optional image tag. "latest" is implied if not specified + Tag string `yaml:"tag" json:"tag"` + + // Digest is the image digest following the "@" in a URL, e.g. image@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2 + Digest string `yaml:"digest" json:"digest"` +} + +// Helper used to instantiate an OpenShift Client Session. +func GetOcSession(pod, container, namespace string, timeout time.Duration, options ...interactive.Option) *interactive.Oc { + // Spawn an interactive OC shell using a goroutine (needed to avoid cross expect.Expecter interaction). Extract the + // Oc reference from the goroutine through a channel. Performs basic sanity checking that the Oc session is set up + // correctly. + var containerOc *interactive.Oc + ocChan := make(chan *interactive.Oc) + + goExpectSpawner := interactive.NewGoExpectSpawner() + var spawner interactive.Spawner = goExpectSpawner + + go func() { + oc, outCh, err := interactive.SpawnOc(&spawner, pod, container, namespace, timeout, options...) + gomega.Expect(outCh).ToNot(gomega.BeNil()) + gomega.Expect(err).To(gomega.BeNil()) + // Set up a go routine which reads from the error channel + go func() { + log.Debugf("start watching the session with container %s/%s", oc.GetPodName(), oc.GetPodContainerName()) + select { + case err := <-outCh: + log.Fatalf("OC session to container %s/%s is broken due to: %v, aborting the test run", oc.GetPodName(), oc.GetPodContainerName(), err) + case <-oc.GetDoneChannel(): + log.Debugf("stop watching the session with container %s/%s", oc.GetPodName(), oc.GetPodContainerName()) + } + }() + ocChan <- oc + }() + + containerOc = <-ocChan + + gomega.Expect(containerOc).ToNot(gomega.BeNil()) + + return containerOc +} + +func (c *Container) GetOc() *interactive.Oc { + if c.Oc == nil { + c.Oc = GetOcSession(c.PodName, c.ContainerName, c.Namespace, DefaultTimeout, interactive.Verbose(expectersVerboseModeEnabled), interactive.SendTimeout(DefaultTimeout)) + } + return c.Oc +} + +func (c *Container) CloseOc() { + if c.Oc != nil { + c.Oc.Close() + c.Oc = nil + } +} + +// ContainerIdentifier is a complex key representing a unique container. +type ContainerIdentifier struct { + Namespace string `yaml:"namespace" json:"namespace"` + PodName string `yaml:"podName" json:"podName"` + ContainerName string `yaml:"containerName" json:"containerName"` + NodeName string `yaml:"nodeName" json:"nodeName"` + ContainerUID string `yaml:"containerUID" json:"containerUID"` + ContainerRuntime string `yaml:"containerRuntime" json:"containerRuntime"` +} + +func (cid ContainerIdentifier) MarshalText() (text []byte, err error) { //nolint:gocritic // This is the type for a key using pointer won't work + return []byte(cid.Namespace + "_" + + cid.PodName + "_" + + cid.ContainerName + "_" + + cid.NodeName + "_" + + cid.ContainerUID + "_" + + cid.ContainerRuntime), nil +} + +func (cid *ContainerIdentifier) String() string { + return fmt.Sprintf("node:%s ns:%s podName:%s containerName:%s containerUID:%s containerRuntime:%s", + cid.NodeName, + cid.Namespace, + cid.PodName, + cid.ContainerName, + cid.ContainerUID, + cid.ContainerRuntime, + ) +} diff --git a/pkg/config/configsections/container_config.go b/pkg/config/configsections/container_config.go deleted file mode 100644 index fda54b767..000000000 --- a/pkg/config/configsections/container_config.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package configsections - -// CNFType defines a type to be either Operator or Container -type CNFType string - -// CertifiedContainerRequestInfo contains all certified images request info -type CertifiedContainerRequestInfo struct { - // Name is the name of the `operator bundle package name` or `image-version` that you want to check if exists in the RedHat catalog - Name string `yaml:"name" json:"name"` - - // Repository is the name of the repository `rhel8` of the container - // This is valid for container only and required field - Repository string `yaml:"repository" json:"repository"` -} - -// CertifiedOperatorRequestInfo contains all certified operator request info -type CertifiedOperatorRequestInfo struct { - - // Name is the name of the `operator bundle package name` that you want to check if exists in the RedHat catalog - Name string `yaml:"name" json:"name"` - - // Organization as understood by the operator publisher , e.g. `redhat-marketplace` - Organization string `yaml:"organization" json:"organization"` -} - -// Operator struct defines operator manifest for testing -type Operator struct { - - // Name is a required field, Name of the csv . - Name string `yaml:"name" json:"name"` - - // Namespace is a required field , namespace is where the csv is installed. - // If its all namespace then you can replace it with ALL_NAMESPACE TODO: add check for ALL_NAMESPACE - Namespace string `yaml:"namespace" json:"namespace"` - - // Tests this is list of test that need to run against the operator. - Tests []string `yaml:"tests" json:"tests"` - - // Subscription name is required field, Name of used subscription. - SubscriptionName string `yaml:"subscriptionName" json:"subscriptionName"` -} - -// Crd struct defines Custom Resource Definition of the operator -type Crd struct { - // Name is the name of the CRD populated by the operator config generator - Name string `yaml:"name" json:"name"` - - // Namespace is the namespace where above CRD is installed(For all namespace this will be ALL_NAMESPACE) - Namespace string `yaml:"namespace" json:"namespace"` - - // Instances is the instance of CR matching for the above CRD KIND - Instances []Instance `yaml:"instances" json:"instances"` -} - -// Deployment defines deployment resources -type Deployment struct { - // Name is the name of the deployment specified in the CSV - Name string `yaml:"name" json:"name"` - - // Replicas is no of replicas that are expected for this deployment as specified in the CSV - Replicas string `yaml:"replicas" json:"replicas"` -} - -// Permission defines roles and cluster roles resources -type Permission struct { - // Name is the name of Roles and Cluster Roles that is specified in the CSV - Name string `yaml:"name" json:"name"` - - // Role is the role type either CLUSTER_ROLE or ROLE - Role string `yaml:"role" json:"role"` -} - -// Cnf defines cloud network function in the cluster -type Cnf struct { - // Name is the name of a single Pod to test - Name string `yaml:"name" json:"name"` - - // Namespace where the Pod is deployed - Namespace string `yaml:"namespace" json:"namespace"` - - // Tests this is list of test that need to run against the Pod. - Tests []string `yaml:"tests" json:"tests"` -} - -// Instance defines crd instances in the cluster -type Instance struct { - // Name is the name of the instance of custom resource (Auto populated) - Name string `yaml:"name" json:"name"` -} diff --git a/pkg/config/configsections/container_test.go b/pkg/config/configsections/container_test.go index 29cd7c31f..3e2da4741 100644 --- a/pkg/config/configsections/container_test.go +++ b/pkg/config/configsections/container_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -14,266 +14,22 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -package configsections_test +package configsections import ( - "encoding/json" - "io/ioutil" - "log" - "os" "testing" "github.com/stretchr/testify/assert" - "github.com/test-network-function/test-network-function/pkg/config" - "github.com/test-network-function/test-network-function/pkg/config/configsections" - "github.com/test-network-function/test-network-function/pkg/tnf/testcases" - "gopkg.in/yaml.v2" ) -var ( - file *os.File - jsonFile *os.File - err error - test config.File -) - -const ( - // cnfConfig represents CNF configuration only - cnfConfig = "cnf_only_config" - // cnfName name of the cnf - cnfName = "cnf-test-one" - // crdNameOne name of the crd - crdNameOne = "crd-test-one" - // crdNameTwo name of the crd - crdNameTwo = "crd-test-two" - // deploymentName is the name of the deployment - deploymentName = "deployment-one" - // deploymentReplicas no of replicas - deploymentReplicas = "1" - // fullConfig represents full configuration, including Operator and CNF - fullConfig = "full_config" - // instanceNameOne name of the instance - instanceNameOne = "instance-one" - // instanceNameTwo name of the instance - instanceNameTwo = "instance-two" - // operatorConfig represents operators configuration only - operatorConfig = "operator_only_config" - // operatorName name of the operator - operatorName = "etcdoperator.v0.9.4" - // operatorNameSpace is test namespace for an operator - operatorNameSpace = "my-etcd" - // testNameSpace k8s namespace - testNameSpace = "default" -) - -const ( - // filePerm is the permissions these tests will use when creating config files in test setup - filePerm = 0644 -) - -func saveConfig(c *config.File, configPath string) (err error) { - bytes, _ := yaml.Marshal(c) - if err != nil { - return - } - err = ioutil.WriteFile(configPath, bytes, filePerm) - return -} - -func saveConfigAsJSON(c *config.File, configPath string) (err error) { - bytes, err := json.Marshal(c) - if err != nil { - return - } - err = ioutil.WriteFile(configPath, bytes, filePerm) - return -} - -// newConfig returns a new decoded TnfContainerOperatorTestConfig struct -func newConfig(configPath string) (*config.File, error) { - // Create config structure - conf := &config.File{} - // Open config file - if file, err = os.Open(configPath); err != nil { - return nil, err - } - defer file.Close() - // Init new YAML decode - d := yaml.NewDecoder(file) - // Start YAML decoding from file - if err = d.Decode(&conf); err != nil { - return nil, err +func TestString(t *testing.T) { + cID := ContainerIdentifier{ + NodeName: "node1", + Namespace: "namespace1", + PodName: "pod1", + ContainerName: "container1", + ContainerUID: "uid1", + ContainerRuntime: "runtime1", } - return conf, nil -} - -func loadCnfConfig() { - // CNF only - test.CNFs = []configsections.Cnf{ - { - Name: cnfName, - Namespace: testNameSpace, - Tests: []string{testcases.PrivilegedPod}, - }, - } - test.CnfAvailableTestCases = nil - for key := range testcases.CnfTestTemplateFileMap { - test.CnfAvailableTestCases = append(test.CnfAvailableTestCases, key) - } -} - -func loadOperatorConfig() { - operator := configsections.Operator{} - operator.Name = operatorName - operator.Namespace = operatorNameSpace - setCrdsAndInstances() - dep := configsections.Deployment{} - dep.Name = deploymentName - dep.Replicas = deploymentReplicas - operator.Tests = []string{testcases.OperatorStatus} - test.Operators = append(test.Operators, operator) - // CNF only - loadCnfConfig() -} - -func setCrdsAndInstances() { - crd := configsections.Crd{} - crd.Name = crdNameOne - crd.Namespace = testNameSpace - instance := configsections.Instance{} - instance.Name = instanceNameOne - crd.Instances = append(crd.Instances, instance) - crd2 := configsections.Crd{} - crd2.Name = crdNameTwo - crd2.Namespace = testNameSpace - instance2 := configsections.Instance{} - instance2.Name = instanceNameTwo - crd2.Instances = append(crd2.Instances, instance2) -} - -func loadFullConfig() { - loadOperatorConfig() - loadCnfConfig() -} - -func setup(configType string) { - file, err = ioutil.TempFile(".", "test-config.yml") - if err != nil { - log.Fatal(err) - } - test = config.File{} - switch configType { - case fullConfig: - loadFullConfig() - case cnfConfig: - loadCnfConfig() - case operatorConfig: - loadOperatorConfig() - } - err = saveConfig(&test, file.Name()) - if err != nil { - log.Fatal(err) - } -} - -func setupJSON(configType string) { - jsonFile, err = ioutil.TempFile(".", "test-json-config.json") - if err != nil { - log.Fatal(err) - } - test = config.File{} - switch configType { - case fullConfig: - loadFullConfig() - case cnfConfig: - loadCnfConfig() - case operatorConfig: - loadOperatorConfig() - } - err = saveConfigAsJSON(&test, jsonFile.Name()) - if err != nil { - log.Fatal(err) - } -} - -func teardown() { - if file != nil { - os.Remove(file.Name()) - } - if jsonFile != nil { - os.Remove(jsonFile.Name()) - } -} - -func TestFullConfigLoad(t *testing.T) { - setup(fullConfig) - defer (teardown)() - cfg, err := newConfig(file.Name()) - assert.NotNil(t, cfg) - assert.Equal(t, len(cfg.Operators), 1) - assert.Equal(t, cfg.CNFs[0].Name, cnfName) - assert.Nil(t, err) -} - -func TestCnfConfigLoad(t *testing.T) { - setup(cnfConfig) - defer (teardown)() - cfg, err := newConfig(file.Name()) - assert.NotNil(t, cfg) - assert.Equal(t, cfg.CNFs[0].Name, cnfName) - assert.Nil(t, err) -} - -func TestOperatorConfigLoad(t *testing.T) { - setup(operatorConfig) - defer (teardown)() - cfg, err := newConfig(file.Name()) - assert.NotNil(t, cfg) - assert.Equal(t, len(cfg.Operators), 1) - assert.Nil(t, err) -} - -func TestFullJsonConfig(t *testing.T) { - defer (teardown)() - // json - setupJSON(fullConfig) - jsonCfg, err := newConfig(jsonFile.Name()) - assert.NotNil(t, jsonCfg) - assert.Nil(t, err) - // yaml - setup(fullConfig) - yamlCfg, err := newConfig(file.Name()) - assert.Nil(t, err) - assert.NotNil(t, yamlCfg) - assert.Equal(t, yamlCfg.Operators, jsonCfg.Operators) - assert.Equal(t, yamlCfg.CNFs, jsonCfg.CNFs) -} - -func TestCnfJsonConfig(t *testing.T) { - defer (teardown)() - // json - setupJSON(cnfConfig) - jsonCfg, err := newConfig(jsonFile.Name()) - assert.NotNil(t, jsonCfg) - assert.Nil(t, err) - // yaml - setup(cnfConfig) - yamlCfg, err := newConfig(file.Name()) - assert.Nil(t, err) - assert.NotNil(t, yamlCfg) - assert.Equal(t, yamlCfg.CNFs, jsonCfg.CNFs) -} - -func TestOperatorJsonConfig(t *testing.T) { - defer (teardown)() - // json - setupJSON(operatorConfig) - jsonCfg, err := newConfig(jsonFile.Name()) - assert.NotNil(t, jsonCfg) - assert.Nil(t, err) - // yaml - setup(operatorConfig) - yamlCfg, err := newConfig(file.Name()) - assert.Nil(t, err) - assert.Equal(t, yamlCfg.Operators, jsonCfg.Operators) + assert.Equal(t, "node:node1 ns:namespace1 podName:pod1 containerName:container1 containerUID:uid1 containerRuntime:runtime1", cID.String()) } diff --git a/pkg/tnf/handlers/containerid/doc.go b/pkg/config/configsections/crdfilter.go similarity index 80% rename from pkg/tnf/handlers/containerid/doc.go rename to pkg/config/configsections/crdfilter.go index 8c545280b..9459b7a02 100644 --- a/pkg/tnf/handlers/containerid/doc.go +++ b/pkg/config/configsections/crdfilter.go @@ -14,6 +14,10 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// Package containerid provides a test that returns the container id of the container running the test. -// Only works for containers created by crio -package containerid +package configsections + +// CrdFilter defines a CustomResourceDefinition config filter. +type CrdFilter struct { + NameSuffix string `yaml:"nameSuffix" json:"nameSuffix"` + // labels []Label +} diff --git a/pkg/config/configsections/generic_config.go b/pkg/config/configsections/generic_config.go deleted file mode 100644 index 151f1dba2..000000000 --- a/pkg/config/configsections/generic_config.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package configsections - -// ContainerIdentifier is a complex key representing a unique container. -type ContainerIdentifier struct { - Namespace string `yaml:"namespace" json:"namespace"` - PodName string `yaml:"podName" json:"podName"` - ContainerName string `yaml:"containerName" json:"containerName"` -} - -// Container contains the payload of container facets. -type Container struct { - ContainerIdentifier `yaml:",inline"` - // OpenShift Default network interface name (i.e., eth0) - DefaultNetworkDevice string `yaml:"defaultNetworkDevice" json:"defaultNetworkDevice"` - // MultusIPAddresses are the overlay IPs. - MultusIPAddresses []string `yaml:"multusIpAddresses" json:"multusIpAddresses"` -} - -// TestConfiguration provides generic test related configuration -type TestConfiguration struct { - ContainersUnderTest []Container `yaml:"containersUnderTest" json:"containersUnderTest"` - PartnerContainers []Container `yaml:"partnerContainers" json:"partnerContainers"` - TestOrchestrator ContainerIdentifier `yaml:"testOrchestrator" json:"testOrchestrator"` - FsDiffMasterContainer ContainerIdentifier `yaml:"fsDiffMasterContainer" json:"fsDiffMasterContainer"` - // ExcludeContainersFromConnectivityTests excludes specific containers from network connectivity tests. This is particularly useful for containers that don't have ping available. - ExcludeContainersFromConnectivityTests []ContainerIdentifier `yaml:"excludeContainersFromConnectivityTests" json:"excludeContainersFromConnectivityTests"` -} diff --git a/pkg/tnf/handlers/serviceaccount/doc.go b/pkg/config/configsections/helm.go similarity index 87% rename from pkg/tnf/handlers/serviceaccount/doc.go rename to pkg/config/configsections/helm.go index a48c232ee..b1ed29914 100644 --- a/pkg/tnf/handlers/serviceaccount/doc.go +++ b/pkg/config/configsections/helm.go @@ -14,5 +14,9 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// Package serviceaccount provides a test for reading the CNF pod's serviceaccount -package serviceaccount +package configsections + +type HelmChart struct { + Version string + Name string +} diff --git a/pkg/config/configsections/hpa.go b/pkg/config/configsections/hpa.go new file mode 100644 index 000000000..6d1e3dd92 --- /dev/null +++ b/pkg/config/configsections/hpa.go @@ -0,0 +1,23 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package configsections + +type Hpa struct { + MinReplicas int + MaxReplicas int + HpaName string +} diff --git a/cmd/catalog/main.go b/pkg/config/configsections/misc.go similarity index 55% rename from cmd/catalog/main.go rename to pkg/config/configsections/misc.go index 1da5b9b28..b5c11d209 100644 --- a/cmd/catalog/main.go +++ b/pkg/config/configsections/misc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -14,17 +14,19 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -package main +package configsections -import ( - "log" +// Types defined in this file are not currently in use. Move them out when starting to use. +// May remove this altogether in the future - "github.com/test-network-function/test-network-function/cmd/catalog/cmd" -) +// CNFType defines a type to be either Operator or Container +type CNFType string -// main generates a JSON formatted version of the test catalog. -func main() { - if err := cmd.Execute(); err != nil { - log.Fatalf("Could not generate the test catalog: %s", err) - } +// Permission defines roles and cluster roles resources +type Permission struct { + // Name is the name of Roles and Cluster Roles that is specified in the CSV + Name string `yaml:"name" json:"name"` + + // Role is the role type either CLUSTER_ROLE or ROLE + Role string `yaml:"role" json:"role"` } diff --git a/pkg/config/configsections/node.go b/pkg/config/configsections/node.go new file mode 100644 index 000000000..29b36c872 --- /dev/null +++ b/pkg/config/configsections/node.go @@ -0,0 +1,49 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package configsections + +// WorkerLabel const for k8s worker +const WorkerLabel = "node-role.kubernetes.io/worker" + +// MasterLabel const for k8s for master +const MasterLabel = "node-role.kubernetes.io/master" + +// Node defines in the cluster. with name of the node and the type of this node master/worker,,,,. +type Node struct { + Name string + Labels []string +} + +// IsMaster Function that return if the node is master +func (node Node) IsMaster() bool { + for _, t := range node.Labels { + if t == MasterLabel { + return true + } + } + return false +} + +// IsWorker Function that return if the node is worker +func (node Node) IsWorker() bool { + for _, t := range node.Labels { + if t == WorkerLabel { + return true + } + } + return false +} diff --git a/pkg/config/configsections/node_test.go b/pkg/config/configsections/node_test.go new file mode 100644 index 000000000..56908f534 --- /dev/null +++ b/pkg/config/configsections/node_test.go @@ -0,0 +1,79 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package configsections + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsMaster(t *testing.T) { + testCases := []struct { + nodeLabel string + expectedOutput bool + }{ + { + expectedOutput: true, + nodeLabel: MasterLabel, + }, + { + expectedOutput: false, + nodeLabel: WorkerLabel, + }, + { + expectedOutput: false, + nodeLabel: "", + }, + } + + for _, tc := range testCases { + testNode := Node{ + Name: "mastertest", + Labels: []string{tc.nodeLabel}, + } + assert.Equal(t, tc.expectedOutput, testNode.IsMaster()) + } +} + +func TestIsWorker(t *testing.T) { + testCases := []struct { + nodeLabel string + expectedOutput bool + }{ + { + expectedOutput: true, + nodeLabel: WorkerLabel, + }, + { + expectedOutput: false, + nodeLabel: MasterLabel, + }, + { + expectedOutput: false, + nodeLabel: "", + }, + } + + for _, tc := range testCases { + testNode := Node{ + Name: "workertest", + Labels: []string{tc.nodeLabel}, + } + assert.Equal(t, tc.expectedOutput, testNode.IsWorker()) + } +} diff --git a/pkg/config/configsections/pod.go b/pkg/config/configsections/pod.go new file mode 100644 index 000000000..f4eff10a1 --- /dev/null +++ b/pkg/config/configsections/pod.go @@ -0,0 +1,49 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package configsections + +// Pod defines cloud network function in the cluster +type Pod struct { + // Name is the name of a single Pod to test + Name string `yaml:"name" json:"name"` + + // Namespace where the Pod is deployed + Namespace string `yaml:"namespace" json:"namespace"` + + // ServiceAccount name used by the pod + ServiceAccount string `yaml:"serviceaccount" json:"serviceaccount"` + + // ContainerCount is the count of containers inside the pod + ContainerCount int `yaml:"containercount" json:"containercount"` + + // Tests this is list of test that need to run against the Pod. + Tests []string `yaml:"tests" json:"tests"` + + DefaultNetworkIPAddresses []string `yaml:"defaultnetworkipaddresses,omitempty" json:"defaultnetworkipaddresses,omitempty"` + + // OpenShift Default network interface name (i.e., eth0) + DefaultNetworkDevice string `yaml:"defaultNetworkDevice" json:"defaultNetworkDevice"` + + // MultusIPAddressesPerNet are the overlay IPs. + MultusIPAddressesPerNet map[string][]string `yaml:"multusIpAddressesPerNet,omitempty" json:"multusIpAddressesPerNet,omitempty"` + + // Representation of the container in this pod used to run networing tests + ContainerList []Container `yaml:"containerfornettests,omitempty" json:"containerfornettests,omitempty"` + + // IsManaged indicates whether this pod belongs to any other resource (deployment/statefulset). + IsManaged bool +} diff --git a/pkg/config/configsections/podset.go b/pkg/config/configsections/podset.go new file mode 100644 index 000000000..8d77612b0 --- /dev/null +++ b/pkg/config/configsections/podset.go @@ -0,0 +1,33 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package configsections + +// PodSet defines a podset (deployment/Statefulset) in the cluster. +type PodSet struct { + Name string + Namespace string + Replicas int + Hpa Hpa + Type PodSetType +} + +type PodSetType string + +const ( + Deployment PodSetType = "deployment" + StateFulSet PodSetType = "statefulset" +) diff --git a/cmd/generic/main.go b/pkg/config/configsections/request.go similarity index 50% rename from cmd/generic/main.go rename to pkg/config/configsections/request.go index 91b7adc69..1043b373f 100644 --- a/cmd/generic/main.go +++ b/pkg/config/configsections/request.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -14,17 +14,21 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -package main +package configsections -import ( - "log" +// CertifiedOperatorRequestInfo contains all certified operator request info +type CertifiedOperatorRequestInfo struct { - "github.com/test-network-function/test-network-function/cmd/generic/cmd" -) + // Name is the name of the `operator bundle package name` that you want to check if exists in the RedHat catalog + Name string `yaml:"name" json:"name"` -func main() { - err := cmd.Execute() - if err != nil { - log.Fatalf("Fatal Error: %s", err) - } + // Organization as understood by the operator publisher , e.g. `redhat-marketplace` + Organization string `yaml:"organization" json:"organization"` +} + +// AcceptedKernelTaintsInfo contains all certified operator request info +type AcceptedKernelTaintsInfo struct { + + // Accepted modules that cause taints that we want to supply to the test suite + Module string `yaml:"module" json:"module"` } diff --git a/pkg/config/configsections/test-json-config.json883666995 b/pkg/config/configsections/test-json-config.json883666995 new file mode 100644 index 000000000..88e2bd5c3 --- /dev/null +++ b/pkg/config/configsections/test-json-config.json883666995 @@ -0,0 +1 @@ +{"targetNameSpaces":null,"testTarget":{"deploymentsUnderTest":[{"Name":"deployment-one","Namespace":"default","Replicas":1,"Hpa":{"MinReplicas":0,"MaxReplicas":0,"HpaName":""},"Type":""}],"stateFulSetUnderTest":null,"podsUnderTest":[{"name":"cnf-test-one","namespace":"default","serviceaccount":"","containercount":0,"tests":["PRIVILEGED_POD"],"defaultnetworkipaddresses":null,"defaultNetworkDevice":"","IsManaged":false}],"NonValidPods":null,"containersUnderTest":null,"excludeContainersFromConnectivityTests":null,"operators":[{"name":"etcdoperator.v0.9.4","namespace":"my-etcd","tests":["OPERATOR_STATUS"],"subscriptionName":""}],"Nodes":null},"testPartner":{},"checkDiscoveredContainerCertificationStatus":false,"targetCrdFilters":[{"nameSuffix":"group1.test.com"},{"nameSuffix":"group2.test.com"}]} \ No newline at end of file diff --git a/pkg/config/doc.go b/pkg/config/doc.go index 944c71ddc..72508abae 100644 --- a/pkg/config/doc.go +++ b/pkg/config/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/config/testdata/tnf_test_config.yml b/pkg/config/testdata/tnf_test_config.yml index 2833911b2..339fe18f4 100644 --- a/pkg/config/testdata/tnf_test_config.yml +++ b/pkg/config/testdata/tnf_test_config.yml @@ -1,4 +1,4 @@ -generic: +testTarget: containersUnderTest: - namespace: default podName: test @@ -6,38 +6,32 @@ generic: defaultNetworkDevice: eth0 multusIpAddresses: - 10.217.0.8 - - namespace: default - podName: partner - containerName: partner - defaultNetworkDevice: eth0 - multusIpAddresses: - - 10.217.0.29 - partnerContainers: - - namespace: default - podName: partner - containerName: partner - defaultNetworkDevice: eth0 - multusIpAddresses: - - 10.217.0.29 - testOrchestrator: - namespace: default - podName: partner - containerName: partner -operators: - - name: etcdoperator.v0.9.4 - namespace: default - autogenerate: false - tests: - - OPERATOR_STATUS -cnfs: - - name: ubuntu - namespace: default - tests: - - PRIVILEGED_POD - - PRIVILEGED_ROLE + operators: + - name: etcdoperator.v0.9.4 + namespace: default + autogenerate: false + tests: + - OPERATOR_STATUS + podsUnderTest: # FKA cnfs + - name: ubuntu + namespace: default + tests: + - PRIVILEGED_POD + - PRIVILEGED_ROLE + deploymentsUnderTest: + - name: test + namespace: default + replicas: 2 + certifiedcontainerinfo: - name: nginx-116 # working example repository: rhel8 certifiedoperatorinfo: - name: etcd-operator - organization: redhat-marketplace \ No newline at end of file + organization: redhat-marketplace +acceptedKernelTaints: + - module: "taint1" + - module: "taint2" +targetCrdFilters: + - nameSuffix: "group1.test1.com" + - nameSuffix: "test2.com" diff --git a/pkg/gradetool/gradetool.go b/pkg/gradetool/gradetool.go index e3f98a272..286e5302b 100644 --- a/pkg/gradetool/gradetool.go +++ b/pkg/gradetool/gradetool.go @@ -19,7 +19,7 @@ package gradetool import ( "encoding/json" "fmt" - "io/ioutil" + "os" "path" "github.com/test-network-function/test-network-function-claim/pkg/claim" @@ -102,7 +102,7 @@ func NewGradeResult(gradeName string) GradeResult { } func generateTestResultsKey(id identifier.Identifier) string { - return fmt.Sprintf("{\"url\":\"%s\",\"version\":\"%s\"}", id.URL, id.SemanticVersion) + return fmt.Sprintf("{\"url\":%q,\"version\":%q}", id.URL, id.SemanticVersion) } func doGrading(policy Policy, results map[string]interface{}) (interface{}, error) { @@ -171,7 +171,7 @@ func generateOutput(outputObj interface{}, outputPath string) error { if err != nil { return err } - err = ioutil.WriteFile(outputPath, outputBytes, outputFilePermissions) + err = os.WriteFile(outputPath, outputBytes, outputFilePermissions) if err != nil { return err } @@ -206,7 +206,7 @@ func validatePolicySchema(policyPath string) error { } func unmarshalFromFile(jsonPath string, obj interface{}) error { - jsonBytes, err := ioutil.ReadFile(jsonPath) + jsonBytes, err := os.ReadFile(jsonPath) if err != nil { return err } diff --git a/pkg/jsonschema/utils.go b/pkg/jsonschema/utils.go index 985ba4ab9..3aa2f62e3 100644 --- a/pkg/jsonschema/utils.go +++ b/pkg/jsonschema/utils.go @@ -17,14 +17,14 @@ package jsonschema import ( - "io/ioutil" + "os" "github.com/xeipuuv/gojsonschema" ) // ValidateJSONFileAgainstSchema validates a given file against the supplied JSON schema. func ValidateJSONFileAgainstSchema(filename, schemaPath string) (*gojsonschema.Result, error) { - inputBytes, err := ioutil.ReadFile(filename) + inputBytes, err := os.ReadFile(filename) if err != nil { return nil, err } @@ -33,7 +33,7 @@ func ValidateJSONFileAgainstSchema(filename, schemaPath string) (*gojsonschema.R // ValidateJSONAgainstSchema validates a given byte array against the supplied JSON schema. func ValidateJSONAgainstSchema(inputBytes []byte, schemaPath string) (*gojsonschema.Result, error) { - schemaBytes, err := ioutil.ReadFile(schemaPath) + schemaBytes, err := os.ReadFile(schemaPath) if err != nil { return nil, err } diff --git a/pkg/junit/convert.go b/pkg/junit/convert.go index 94a2ebeea..7391b6ed0 100644 --- a/pkg/junit/convert.go +++ b/pkg/junit/convert.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -29,6 +29,9 @@ const ( // junitContentKey is the "#content" key in the JSON serialized JUnit file. junitContentKey = "#content" + // junitMessageKey is the "-message" key in the JSON serialized JUnit file. + junitMessageKey = "-message" + // junitFailureKey is the "failure" key in the JSON serialized Junit file. junitFailureKey = "failure" @@ -38,9 +41,12 @@ const ( // junitTestNameKey is the "-name" key in the JSON serialized JSON file. junitTestNameKey = "-name" - // junitTestSuiteKey =s the "testsuite" ke in the JSON serialized JSON file. + // junitTestSuiteKey is the "testsuite" key in the JSON serialized JSON file. junitTestSuiteKey = "testsuite" + // junitTestSuitesKey is the "testsuites" key in the JSON serialized JSON file. + junitTestSuitesKey = "testsuites" + // CouldNotDeriveFailureReason is the sentinel message emitted when JUnit failure reason cannot be determined. CouldNotDeriveFailureReason = "could not derive a reason for the failure from the output JSON" ) @@ -88,12 +94,15 @@ func determineFailureReason(failure interface{}) string { var failureReason string if failureReasonObject, ok := failure.(map[string]interface{}); ok { if derivedFailureReason, ok := failureReasonObject[junitContentKey]; ok { - failureReason = derivedFailureReason.(string) + failureReason = "Failed due to line: " + derivedFailureReason.(string) + } else { + failureReason = "Failed due to line: No error line found in JUnit" //nolint:goconst // only instance + } + if derivedFailureReason, ok := failureReasonObject[junitMessageKey]; ok { + failureReason = failureReason + "\n" + "Error message: " + derivedFailureReason.(string) } else { - failureReason = CouldNotDeriveFailureReason + failureReason = failureReason + "\n" + "Error message: No JUinit message found" } - } else { - failureReason = CouldNotDeriveFailureReason } return failureReason } @@ -141,21 +150,25 @@ func toInterfaceArray(object interface{}) []interface{} { func ExtractTestSuiteResults(junitMap map[string]interface{}, reportKeyName string) (map[string]TestResult, error) { // Note: All of the follow checks are paranoia; assuming a well formed JUnit output file, most of these checks // will never fail. As such, individual error reporting per case is ignored in favor of a blanket error statement. - if suite, ok := junitMap[reportKeyName].(map[string]interface{}); ok { - if testSuiteResults, ok := suite[junitTestSuiteKey]; ok { - if testCaseResultsMap, ok := testSuiteResults.(map[string]interface{}); ok { - if testCaseResults, ok := testCaseResultsMap[junitTestCaseKey]; ok { - // Note: order is important since an []interface{} can still cast as interface{}, but an - // interface{} cannot be cast as []interface{} - if resultsObjects, ok := testCaseResults.([]interface{}); ok { - resultsMap := parseResults(resultsObjects) - parseResults(resultsObjects) - return resultsMap, nil + if suites, ok := junitMap[reportKeyName].(map[string]interface{}); ok { + if testSuitesResults, ok := suites[junitTestSuitesKey]; ok { + if testSuitesResultsMap, ok := testSuitesResults.(map[string]interface{}); ok { + if testSuiteResults, ok := testSuitesResultsMap[junitTestSuiteKey]; ok { + if testCaseResultsMap, ok := testSuiteResults.(map[string]interface{}); ok { + if testCaseResults, ok := testCaseResultsMap[junitTestCaseKey]; ok { + // Note: order is important since an []interface{} can still cast as interface{}, but an + // interface{} cannot be cast as []interface{} + if resultsObjects, ok := testCaseResults.([]interface{}); ok { + resultsMap := parseResults(resultsObjects) + parseResults(resultsObjects) + return resultsMap, nil + } + resultsObjects := toInterfaceArray(testCaseResults) + resultsMap := parseResults(resultsObjects) + parseResults(resultsObjects) + return resultsMap, nil + } } - resultsObjects := toInterfaceArray(testCaseResults) - resultsMap := parseResults(resultsObjects) - parseResults(resultsObjects) - return resultsMap, nil } } } diff --git a/pkg/junit/convert_test.go b/pkg/junit/convert_test.go index 3d59892a9..b35e1f207 100644 --- a/pkg/junit/convert_test.go +++ b/pkg/junit/convert_test.go @@ -38,7 +38,7 @@ func TestExtractTestSuiteResults(t *testing.T) { results, err := junit.ExtractTestSuiteResults(claim, testKey) assert.Nil(t, err) // positive test - assert.Equal(t, true, results["generic when Reading namespace of test/test Should not be 'default' and should not begin with 'openshift-'"].Passed) + assert.Equal(t, true, results["[It] operator Runs test on operators operator-install-status-CSV_INSTALLED"].Passed) // negative test - assert.Equal(t, false, results["generic when Testing owners of CNF pod Should contain at least one of kind DaemonSet/ReplicaSet"].Passed, false) + assert.Equal(t, false, results["[It] platform-alteration platform-alteration-boot-params"].Passed) } diff --git a/pkg/junit/doc.go b/pkg/junit/doc.go index 70cb2f88f..f9b7b7c7a 100644 --- a/pkg/junit/doc.go +++ b/pkg/junit/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/junit/testdata/success.junit.xml b/pkg/junit/testdata/success.junit.xml index d37ade7a4..ae3552f8a 100644 --- a/pkg/junit/testdata/success.junit.xml +++ b/pkg/junit/testdata/success.junit.xml @@ -1,21 +1,162 @@ - - - - - - - - - - - - - - /Users/ryangoulding/workspace/t/test-network-function/test-network-function/generic/suite.go:801 Expected <int>: 0 to equal <int>: 1 /Users/ryangoulding/workspace/t/test-network-function/test-network-function/generic/suite.go:808 - - - /Users/ryangoulding/workspace/t/test-network-function/test-network-function/generic/suite.go:500 Expected <int>: 0 to equal <int>: 1 /Users/ryangoulding/workspace/t/test-network-function/test-network-function/generic/suite.go:423 - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/operator/suite.go:86 2021-10-15T11:35:29.914992242-05:00 &{Text:nginx-operator-v0-0-1-sub in namespace tnf Should have a valid subscription Duration:0s} + �[1mSTEP:�[0m nginx-operator-v0-0-1-sub in namespace tnf Should have a valid subscription �[38;5;243m10/15/21 11:35:29.914�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/certification/suite.go:63 2021-10-15T11:35:29.98917388-05:00 &{Text:Getting certification status. Number of containers to check: 1 Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/certification/suite.go:70 2021-10-15T11:35:29.989228997-05:00 &{Text:container rhel8/nginx-116 should eventually be verified as certified Duration:0s} + �[1mSTEP:�[0m Getting certification status. Number of containers to check: 1 �[38;5;243m10/15/21 11:35:29.989�[0m �[1mSTEP:�[0m container rhel8/nginx-116 should eventually be verified as certified �[38;5;243m10/15/21 11:35:29.989�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/certification/suite.go:84 2021-10-15T11:35:30.354416591-05:00 &{Text:Verify operator as certified. Number of operators to check: 1 Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/certification/suite.go:88 2021-10-15T11:35:30.354554715-05:00 &{Text:should eventually be verified as certified (operator community-operators/etcd) Duration:0s} + �[1mSTEP:�[0m Verify operator as certified. Number of operators to check: 1 �[38;5;243m10/15/21 11:35:30.354�[0m �[1mSTEP:�[0m should eventually be verified as certified (operator community-operators/etcd) �[38;5;243m10/15/21 11:35:30.354�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:212 2021-10-15T11:35:30.471068176-05:00 &{Text:Testing pod nodeSelector Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:217 2021-10-15T11:35:30.472079593-05:00 &{Text:Testing pod nodeSelector tnf/test-697ff58f87-89gmx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:217 2021-10-15T11:35:30.56378219-05:00 &{Text:Testing pod nodeSelector tnf/test-697ff58f87-hfcm6 Duration:0s} + �[1mSTEP:�[0m Testing pod nodeSelector �[38;5;243m10/15/21 11:35:30.471�[0m �[1mSTEP:�[0m Testing pod nodeSelector tnf/test-697ff58f87-89gmx �[38;5;243m10/15/21 11:35:30.472�[0m �[1mSTEP:�[0m Testing pod nodeSelector tnf/test-697ff58f87-hfcm6 �[38;5;243m10/15/21 11:35:30.563�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:234 2021-10-15T11:35:30.634110734-05:00 &{Text:Test terminationGracePeriod Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:239 2021-10-15T11:35:30.634562725-05:00 &{Text:Testing pod terminationGracePeriod tnf test-697ff58f87-89gmx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:239 2021-10-15T11:35:30.704516596-05:00 &{Text:Testing pod terminationGracePeriod tnf test-697ff58f87-hfcm6 Duration:0s} + �[1mSTEP:�[0m Test terminationGracePeriod �[38;5;243m10/15/21 11:35:30.634�[0m �[1mSTEP:�[0m Testing pod terminationGracePeriod tnf test-697ff58f87-89gmx �[38;5;243m10/15/21 11:35:30.634�[0m �[1mSTEP:�[0m Testing pod terminationGracePeriod tnf test-697ff58f87-hfcm6 �[38;5;243m10/15/21 11:35:30.704�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:258 2021-10-15T11:35:30.775579992-05:00 &{Text:Testing PUTs are configured with pre-stop lifecycle Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:262 2021-10-15T11:35:30.775641879-05:00 &{Text:should have pre-stop configured tnf/test-697ff58f87-89gmx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:262 2021-10-15T11:35:30.848140776-05:00 &{Text:should have pre-stop configured tnf/test-697ff58f87-hfcm6 Duration:0s} + �[1mSTEP:�[0m Testing PUTs are configured with pre-stop lifecycle �[38;5;243m10/15/21 11:35:30.775�[0m �[1mSTEP:�[0m should have pre-stop configured tnf/test-697ff58f87-89gmx �[38;5;243m10/15/21 11:35:30.775�[0m �[1mSTEP:�[0m should have pre-stop configured tnf/test-697ff58f87-hfcm6 �[38;5;243m10/15/21 11:35:30.848�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:403 2021-10-15T11:35:30.918743322-05:00 &{Text:Should set pod replica number greater than 1 and corresponding pod anti-affinity rules in deployment Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:409 2021-10-15T11:35:30.918802421-05:00 &{Text:Testing Pod AntiAffinity on Deployment=test, Replicas=2 (ns=tnf) Duration:0s} + �[1mSTEP:�[0m Should set pod replica number greater than 1 and corresponding pod anti-affinity rules in deployment �[38;5;243m10/15/21 11:35:30.918�[0m �[1mSTEP:�[0m Testing Pod AntiAffinity on Deployment=test, Replicas=2 (ns=tnf) �[38;5;243m10/15/21 11:35:30.918�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:295 2021-10-15T11:35:30.993327182-05:00 &{Text:Testing node draining effect of deployment Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:297 2021-10-15T11:35:30.993391098-05:00 &{Text:test deployment in namespace tnf Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:307 2021-10-15T11:35:31.065255957-05:00 &{Text:Should return map of nodes to deployments Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:310 2021-10-15T11:35:31.139364913-05:00 &{Text:should create new replicas when node is drained Duration:0s} + �[1mSTEP:�[0m Testing node draining effect of deployment �[38;5;243m10/15/21 11:35:30.993�[0m �[1mSTEP:�[0m test deployment in namespace tnf �[38;5;243m10/15/21 11:35:30.993�[0m �[1mSTEP:�[0m Should return map of nodes to deployments �[38;5;243m10/15/21 11:35:31.065�[0m �[1mSTEP:�[0m should create new replicas when node is drained �[38;5;243m10/15/21 11:35:31.139�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:180 2021-10-15T11:36:57.660075018-05:00 &{Text:Testing deployment scaling Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:188 2021-10-15T11:36:57.660233946-05:00 &{Text:Scaling Deployment=test, Replicas=2 (ns=tnf) Duration:0s} + �[1mSTEP:�[0m Testing deployment scaling �[38;5;243m10/15/21 11:36:57.66�[0m �[1mSTEP:�[0m Scaling Deployment=test, Replicas=2 (ns=tnf) �[38;5;243m10/15/21 11:36:57.66�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:454 2021-10-15T11:37:06.475755503-05:00 &{Text:Testing owners of CNF pod, should be replicas Set Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:459 2021-10-15T11:37:06.476181458-05:00 &{Text:Should be ReplicaSet tnf test-697ff58f87-hsqvx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/lifecycle/suite.go:459 2021-10-15T11:37:06.545811327-05:00 &{Text:Should be ReplicaSet tnf test-697ff58f87-tdwtn Duration:0s} + �[1mSTEP:�[0m Testing owners of CNF pod, should be replicas Set �[38;5;243m10/15/21 11:37:06.475�[0m �[1mSTEP:�[0m Should be ReplicaSet tnf test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:06.476�[0m �[1mSTEP:�[0m Should be ReplicaSet tnf test-697ff58f87-tdwtn �[38;5;243m10/15/21 11:37:06.545�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:140 2021-10-15T11:37:06.614709622-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx, should not be 'default' or begin with openshift- Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:140 2021-10-15T11:37:06.614764466-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn, should not be 'default' or begin with openshift- Duration:0s} + �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx, should not be 'default' or begin with openshift- �[38;5;243m10/15/21 11:37:06.614�[0m �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn, should not be 'default' or begin with openshift- �[38;5;243m10/15/21 11:37:06.614�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:157 2021-10-15T11:37:06.614925364-05:00 &{Text:Should have a valid ServiceAccount name Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:162 2021-10-15T11:37:06.61532637-05:00 &{Text:Testing pod service account tnf test-697ff58f87-hsqvx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:162 2021-10-15T11:37:06.688288844-05:00 &{Text:Testing pod service account tnf test-697ff58f87-tdwtn Duration:0s} + �[1mSTEP:�[0m Should have a valid ServiceAccount name �[38;5;243m10/15/21 11:37:06.614�[0m �[1mSTEP:�[0m Testing pod service account tnf test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:06.615�[0m �[1mSTEP:�[0m Testing pod service account tnf test-697ff58f87-tdwtn �[38;5;243m10/15/21 11:37:06.688�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:177 2021-10-15T11:37:06.787931993-05:00 &{Text:Should not have RoleBinding in other namespaces Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:183 2021-10-15T11:37:06.788452475-05:00 &{Text:Testing role bidning tnf test-697ff58f87-hsqvx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:183 2021-10-15T11:37:06.879475679-05:00 &{Text:Testing role bidning tnf test-697ff58f87-tdwtn Duration:0s} + �[1mSTEP:�[0m Should not have RoleBinding in other namespaces �[38;5;243m10/15/21 11:37:06.787�[0m �[1mSTEP:�[0m Testing role bidning tnf test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:06.788�[0m �[1mSTEP:�[0m Testing role bidning tnf test-697ff58f87-tdwtn �[38;5;243m10/15/21 11:37:06.879�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:199 2021-10-15T11:37:06.966513448-05:00 &{Text:Should not have ClusterRoleBindings Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:205 2021-10-15T11:37:06.967203987-05:00 &{Text:Testing cluster role bidning tnf test-697ff58f87-hsqvx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:205 2021-10-15T11:37:07.087865841-05:00 &{Text:Testing cluster role bidning tnf test-697ff58f87-tdwtn Duration:0s} + �[1mSTEP:�[0m Should not have ClusterRoleBindings �[38;5;243m10/15/21 11:37:06.966�[0m �[1mSTEP:�[0m Testing cluster role bidning tnf test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:06.967�[0m �[1mSTEP:�[0m Testing cluster role bidning tnf test-697ff58f87-tdwtn �[38;5;243m10/15/21 11:37:07.087�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:07.214216335-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:07.296939624-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn Duration:0s} + �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:07.214�[0m �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn �[38;5;243m10/15/21 11:37:07.296�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:07.404501757-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:07.498824485-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn Duration:0s} + �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:07.404�[0m �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn �[38;5;243m10/15/21 11:37:07.498�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:07.582929697-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:07.655495208-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn Duration:0s} + �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:07.582�[0m �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn �[38;5;243m10/15/21 11:37:07.655�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:07.734309905-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:07.812369266-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn Duration:0s} + �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:07.734�[0m �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn �[38;5;243m10/15/21 11:37:07.812�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:07.89551254-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:07.97660132-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn Duration:0s} + �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:07.895�[0m �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn �[38;5;243m10/15/21 11:37:07.976�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:08.060410972-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:08.143722032-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn Duration:0s} + �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:08.06�[0m �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn �[38;5;243m10/15/21 11:37:08.143�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:08.229702179-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:08.312237822-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn Duration:0s} + �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:08.229�[0m �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn �[38;5;243m10/15/21 11:37:08.312�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:08.397342461-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:08.467327258-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn Duration:0s} + �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:08.397�[0m �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn �[38;5;243m10/15/21 11:37:08.467�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:08.544270833-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/accesscontrol/suite.go:86 2021-10-15T11:37:08.642174686-05:00 &{Text:Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn Duration:0s} + �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:08.544�[0m �[1mSTEP:�[0m Reading namespace of podnamespace= tnf podname= test-697ff58f87-tdwtn �[38;5;243m10/15/21 11:37:08.642�[0m + + + /home/deliedit/redhat/github/david/cnftest/pkg/tnf/test.go:149 github.com/test-network-function/test-network-function/pkg/tnf.(*Test).RunAndValidateWithFailureCallback(0xc000923230, 0x0) /home/deliedit/redhat/github/david/cnftest/pkg/tnf/test.go:149 +0xfa github.com/test-network-function/test-network-function/pkg/tnf.(*Test).RunAndValidate(...) /home/deliedit/redhat/github/david/cnftest/pkg/tnf/test.go:140 github.com/test-network-function/test-network-function/test-network-function/platform.newContainerFsDiffTest(0xc0003831f0, 0xc, 0xc00047e240, 0x0) /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:129 +0x225 github.com/test-network-function/test-network-function/test-network-function/platform.testContainersFsDiff.func1.1() /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:112 +0x1d8 github.com/onsi/ginkgo/internal.(*Suite).runNode.func2(0xc000134000, 0xc0009c2ba0, 0xc0009c2c00, 0xc000341080) /home/deliedit/go/pkg/mod/github.com/onsi/ginkgo@v1.16.6-0.20211014152641-f228134fe057/internal/suite.go:724 +0x84 created by github.com/onsi/ginkgo/internal.(*Suite).runNode /home/deliedit/go/pkg/mod/github.com/onsi/ginkgo@v1.16.6-0.20211014152641-f228134fe057/internal/suite.go:712 +0x505 + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:111 2021-10-15T11:37:08.739945107-05:00 &{Text:test-697ff58f87-hsqvx(test) should not install new packages after starting Duration:0s} + �[1mSTEP:�[0m test-697ff58f87-hsqvx(test) should not install new packages after starting �[38;5;243m10/15/21 11:37:08.739�[0m + + + /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:299 github.com/test-network-function/test-network-function/test-network-function/platform.testTainted.func1() /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:299 +0x598 github.com/onsi/ginkgo/internal.(*Suite).runNode.func2(0xc000134000, 0xc000609e00, 0xc000609e60, 0xc000e48900) /home/deliedit/go/pkg/mod/github.com/onsi/ginkgo@v1.16.6-0.20211014152641-f228134fe057/internal/suite.go:724 +0x84 created by github.com/onsi/ginkgo/internal.(*Suite).runNode /home/deliedit/go/pkg/mod/github.com/onsi/ginkgo@v1.16.6-0.20211014152641-f228134fe057/internal/suite.go:712 +0x505 + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:287 2021-10-15T11:37:08.754431552-05:00 &{Text:Testing tainted nodes in cluster Duration:0s} + �[1mSTEP:�[0m Testing tainted nodes in cluster �[38;5;243m10/15/21 11:37:08.754�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:324 2021-10-15T11:37:23.89384141-05:00 &{Text:Should return machineconfig hugepages configuration of node minikube-m03 Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:327 2021-10-15T11:37:24.240867623-05:00 &{Text:Node's machine config hugepages=0/hugepagesz=2048 values should match the actual ones in the node. Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:324 2021-10-15T11:37:29.104091042-05:00 &{Text:Should return machineconfig hugepages configuration of node minikube Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:327 2021-10-15T11:37:29.441875893-05:00 &{Text:Node's machine config hugepages=0/hugepagesz=2048 values should match the actual ones in the node. Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:324 2021-10-15T11:37:32.539900108-05:00 &{Text:Should return machineconfig hugepages configuration of node minikube-m02 Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:327 2021-10-15T11:37:32.868660725-05:00 &{Text:Node's machine config hugepages=0/hugepagesz=2048 values should match the actual ones in the node. Duration:0s} + �[1mSTEP:�[0m Should return machineconfig hugepages configuration of node minikube-m03 �[38;5;243m10/15/21 11:37:23.893�[0m �[1mSTEP:�[0m Node's machine config hugepages=0/hugepagesz=2048 values should match the actual ones in the node. �[38;5;243m10/15/21 11:37:24.24�[0m �[1mSTEP:�[0m Should return machineconfig hugepages configuration of node minikube �[38;5;243m10/15/21 11:37:29.104�[0m �[1mSTEP:�[0m Node's machine config hugepages=0/hugepagesz=2048 values should match the actual ones in the node. �[38;5;243m10/15/21 11:37:29.441�[0m �[1mSTEP:�[0m Should return machineconfig hugepages configuration of node minikube-m02 �[38;5;243m10/15/21 11:37:32.539�[0m �[1mSTEP:�[0m Node's machine config hugepages=0/hugepagesz=2048 values should match the actual ones in the node. �[38;5;243m10/15/21 11:37:32.868�[0m + + + /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:146 github.com/test-network-function/test-network-function/test-network-function/platform.getMcKernelArguments(0xc00067d190, 0xc00002afc0, 0x4, 0xc00002afc0) /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:146 +0x374 github.com/test-network-function/test-network-function/test-network-function/platform.testBootParamsHelper(0xc00067d190, 0xc0002e7a58, 0x15, 0xc0003831e6, 0x3, 0xc00047e240) /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:246 +0x1b9 github.com/test-network-function/test-network-function/test-network-function/platform.testBootParams.func1() /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:238 +0xd7 github.com/onsi/ginkgo/internal.(*Suite).runNode.func2(0xc000134000, 0xc000ec0ba0, 0xc000ec0c00, 0xc000eea480) /home/deliedit/go/pkg/mod/github.com/onsi/ginkgo@v1.16.6-0.20211014152641-f228134fe057/internal/suite.go:724 +0x84 created by github.com/onsi/ginkgo/internal.(*Suite).runNode /home/deliedit/go/pkg/mod/github.com/onsi/ginkgo@v1.16.6-0.20211014152641-f228134fe057/internal/suite.go:712 +0x505 + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:243 2021-10-15T11:37:37.276527244-05:00 &{Text:Testing boot params for the pod's node tnf/test-697ff58f87-hsqvx Duration:0s} + �[1mSTEP:�[0m Testing boot params for the pod's node tnf/test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:37.276�[0m + + + /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:146 github.com/test-network-function/test-network-function/test-network-function/platform.getMcKernelArguments(0xc00073c820, 0xc0003fa030, 0x4, 0xc0003fa030) /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:146 +0x374 github.com/test-network-function/test-network-function/test-network-function/platform.testSysctlConfigsHelper(0xc0002e7a58, 0x15, 0xc0003831e6, 0x3) /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:276 +0x1e5 github.com/test-network-function/test-network-function/test-network-function/platform.testSysctlConfigs.func1() /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:266 +0xbf github.com/onsi/ginkgo/internal.(*Suite).runNode.func2(0xc000134000, 0xc000e604e0, 0xc000e60540, 0xc000254780) /home/deliedit/go/pkg/mod/github.com/onsi/ginkgo@v1.16.6-0.20211014152641-f228134fe057/internal/suite.go:724 +0x84 created by github.com/onsi/ginkgo/internal.(*Suite).runNode /home/deliedit/go/pkg/mod/github.com/onsi/ginkgo@v1.16.6-0.20211014152641-f228134fe057/internal/suite.go:712 +0x505 + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:271 2021-10-15T11:37:37.753637946-05:00 &{Text:Testing sysctl config files for the pod's node tnf/test-697ff58f87-hsqvx Duration:0s} + �[1mSTEP:�[0m Testing sysctl config files for the pod's node tnf/test-697ff58f87-hsqvx �[38;5;243m10/15/21 11:37:37.753�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:81 2021-10-15T11:37:40.726721736-05:00 &{Text:should report a proper Red Hat version Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:93 2021-10-15T11:37:40.726784282-05:00 &{Text:test-697ff58f87-hsqvx(test) is checked for Red Hat version Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/platform/suite.go:93 2021-10-15T11:37:40.734002808-05:00 &{Text:test-697ff58f87-tdwtn(test) is checked for Red Hat version Duration:0s} + �[1mSTEP:�[0m should report a proper Red Hat version �[38;5;243m10/15/21 11:37:40.726�[0m �[1mSTEP:�[0m test-697ff58f87-hsqvx(test) is checked for Red Hat version �[38;5;243m10/15/21 11:37:40.726�[0m �[1mSTEP:�[0m test-697ff58f87-tdwtn(test) is checked for Red Hat version �[38;5;243m10/15/21 11:37:40.734�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/networking/suite.go:88 2021-10-15T11:37:40.738043989-05:00 &{Text:a Ping is issued from partner-68cf756959-sczv6(partner) to test-697ff58f87-hsqvx(test) 10.244.2.179 Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/networking/suite.go:92 2021-10-15T11:37:44.854881652-05:00 &{Text:a Ping is issued from test-697ff58f87-hsqvx(test) to partner-68cf756959-sczv6(partner) 10.244.2.183 Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/networking/suite.go:88 2021-10-15T11:37:48.998931451-05:00 &{Text:a Ping is issued from partner-68cf756959-sczv6(partner) to test-697ff58f87-tdwtn(test) 10.244.0.79 Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/networking/suite.go:92 2021-10-15T11:37:53.054891245-05:00 &{Text:a Ping is issued from test-697ff58f87-tdwtn(test) to partner-68cf756959-sczv6(partner) 10.244.2.183 Duration:0s} + �[1mSTEP:�[0m a Ping is issued from partner-68cf756959-sczv6(partner) to test-697ff58f87-hsqvx(test) 10.244.2.179 �[38;5;243m10/15/21 11:37:40.738�[0m �[1mSTEP:�[0m a Ping is issued from test-697ff58f87-hsqvx(test) to partner-68cf756959-sczv6(partner) 10.244.2.183 �[38;5;243m10/15/21 11:37:44.854�[0m �[1mSTEP:�[0m a Ping is issued from partner-68cf756959-sczv6(partner) to test-697ff58f87-tdwtn(test) 10.244.0.79 �[38;5;243m10/15/21 11:37:48.998�[0m �[1mSTEP:�[0m a Ping is issued from test-697ff58f87-tdwtn(test) to partner-68cf756959-sczv6(partner) 10.244.2.183 �[38;5;243m10/15/21 11:37:53.054�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/networking/suite.go:114 2021-10-15T11:37:57.155974016-05:00 &{Text:a Ping is issued from partner-68cf756959-sczv6(partner) to test-697ff58f87-hsqvx(test) 10.244.2.179 Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/networking/suite.go:114 2021-10-15T11:38:01.295628767-05:00 &{Text:a Ping is issued from partner-68cf756959-sczv6(partner) to test-697ff58f87-tdwtn(test) 10.244.0.79 Duration:0s} + �[1mSTEP:�[0m a Ping is issued from partner-68cf756959-sczv6(partner) to test-697ff58f87-hsqvx(test) 10.244.2.179 �[38;5;243m10/15/21 11:37:57.155�[0m �[1mSTEP:�[0m a Ping is issued from partner-68cf756959-sczv6(partner) to test-697ff58f87-tdwtn(test) 10.244.0.79 �[38;5;243m10/15/21 11:38:01.295�[0m + + + Report Entries: By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/networking/suite.go:142 2021-10-15T11:38:05.328059396-05:00 &{Text:Testing services in namespace tnf Duration:0s} -- By Step /home/deliedit/redhat/github/david/cnftest/test-network-function/networking/suite.go:142 2021-10-15T11:38:05.411991173-05:00 &{Text:Testing services in namespace tnf Duration:0s} + �[1mSTEP:�[0m Testing services in namespace tnf �[38;5;243m10/15/21 11:38:05.328�[0m �[1mSTEP:�[0m Testing services in namespace tnf �[38;5;243m10/15/21 11:38:05.411�[0m + + + + + + + + \ No newline at end of file diff --git a/pkg/tnf/dependencies/binaries.go b/pkg/tnf/dependencies/binaries.go index 4fdd5d044..6aaa408f3 100644 --- a/pkg/tnf/dependencies/binaries.go +++ b/pkg/tnf/dependencies/binaries.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -50,6 +50,9 @@ const ( // OcBinaryName is the name of the OpenShift CLI client command. OcBinaryName = "oc" + // SysctlBinaryName is the name of the Sysctl command. + SysctlBinaryName = "sysctl" + // PodmanBinaryName is the name of the podman tool. PodmanBinaryName = "podman" diff --git a/pkg/tnf/dependencies/doc.go b/pkg/tnf/dependencies/doc.go index 21d62a92b..d466c9896 100644 --- a/pkg/tnf/dependencies/doc.go +++ b/pkg/tnf/dependencies/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/doc.go b/pkg/tnf/doc.go index 77611e5fe..7d300ecdf 100644 --- a/pkg/tnf/doc.go +++ b/pkg/tnf/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/automountservice/automountservice.go b/pkg/tnf/handlers/automountservice/automountservice.go new file mode 100644 index 000000000..d0f7c2135 --- /dev/null +++ b/pkg/tnf/handlers/automountservice/automountservice.go @@ -0,0 +1,169 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package automountservice + +import ( + "regexp" + "time" + + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/identifier" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" + "github.com/test-network-function/test-network-function/test-network-function/common" +) + +const ( + TokenIsTrue int = 1 + TokenIsFalse int = 2 + TokenNotSet int = 3 +) + +// automountservice is the reel handler struct. +type AutomountService struct { + result int + namespace string + isNamespaceSet bool + serviceaccount string + isServiceAccountSet bool + podname string + isPodnameSet bool + timeout time.Duration + args []string + token int +} + +const ( + allRegex = `(?m).+` + SaRegex = `(?m)"automountServiceAccountToken": (.+)` + False = "false," + True = "true," +) + +// NewAutomountService returns a new automountservice handler struct. +func NewAutomountService(options ...func(*AutomountService)) *AutomountService { + as := &AutomountService{ + timeout: common.DefaultTimeout, + result: tnf.ERROR, + token: TokenNotSet, + } + for _, o := range options { + o(as) + } + // to have a valid constructor we need to define + // namespace and podname Or namespace and serviceaccount + if !as.isNamespaceSet && (as.isPodnameSet == !as.isServiceAccountSet) { + return nil + } + + if as.isPodnameSet { + as.args = []string{"oc", "-n", as.namespace, "get", "pods", as.podname, "-o", "json", "|", "jq", "-r", ".spec"} + } else { + as.args = []string{"oc", "-n", as.namespace, "get", "serviceaccounts", as.serviceaccount, "-o", "json"} + } + return as +} + +// WithNamespace specify the namespace +func WithNamespace(ns string) func(*AutomountService) { + return func(as *AutomountService) { + as.namespace = ns + as.isNamespaceSet = true + } +} + +// WithTimeout specify the timeout of the test +func WithTimeout(t time.Duration) func(*AutomountService) { + return func(as *AutomountService) { + as.timeout = t + } +} + +// WithPodname specify the podname to test +func WithPodname(ns string) func(*AutomountService) { + return func(as *AutomountService) { + as.podname = ns + as.isPodnameSet = true + } +} + +// WithServiceAccount specify the serviceaccount to check +func WithServiceAccount(sa string) func(*AutomountService) { + return func(as *AutomountService) { + as.serviceaccount = sa + as.isServiceAccountSet = true + } +} + +// Args returns the initial execution/send command strings for handler automountservice. +func (as *AutomountService) Args() []string { + return as.args +} + +// GetIdentifier returns the tnf.Test specific identifier. +func (as *AutomountService) GetIdentifier() identifier.Identifier { + return identifier.AutomountServiceIdentifier +} + +// Timeout returns the timeout for the test. +func (as *AutomountService) Timeout() time.Duration { + return as.timeout +} + +// Result returns the test result. +func (as *AutomountService) Result() int { + return as.result +} + +// ReelFirst returns a reel step for handler automountservice. +func (as *AutomountService) ReelFirst() *reel.Step { + return &reel.Step{ + Expect: []string{allRegex}, + Timeout: as.timeout, + } +} + +// ReelMatch parses the automountservice output and set the test result on match. +func (as *AutomountService) ReelMatch(_, _, match string) *reel.Step { + numExpectedMatches := 2 + saMatchIdx := 1 + as.result = tnf.SUCCESS + re := regexp.MustCompile(SaRegex) + matched := re.FindStringSubmatch(match) + if len(matched) < numExpectedMatches { + return nil + } + if matched[saMatchIdx] == False { + as.token = TokenIsFalse + } else if matched[saMatchIdx] == True { + as.token = TokenIsTrue + } + return nil +} + +// ReelTimeout function for automountservice will be called by the reel FSM when a expect timeout occurs. +func (as *AutomountService) ReelTimeout() *reel.Step { + return nil +} + +// ReelEOF function for automountservice will be called by the reel FSM when a EOF is read. +func (as *AutomountService) ReelEOF() { +} + +// Token return the value of automountServiceAccountToken for this test +func (as *AutomountService) Token() int { + return as.token +} diff --git a/pkg/tnf/handlers/automountservice/automountservice_test.go b/pkg/tnf/handlers/automountservice/automountservice_test.go new file mode 100644 index 000000000..e8395e7ca --- /dev/null +++ b/pkg/tnf/handlers/automountservice/automountservice_test.go @@ -0,0 +1,134 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package automountservice_test + +import ( + "fmt" + "os" + "path" + "regexp" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf" + as "github.com/test-network-function/test-network-function/pkg/tnf/handlers/automountservice" + "github.com/test-network-function/test-network-function/pkg/tnf/identifier" +) + +const ( + // adding special variable + testTimeoutDuration = time.Second * 2 + testServiceAccount = "default" + testNamespace = "tnf" + testPodname = "test" + testDataDirectory = "testData" + testDataFileSuffix = ".yaml" +) + +type testCase struct { + token int + status int + count int +} + +var testCases = map[string]testCase{ + "podFalse": {as.TokenIsFalse, tnf.SUCCESS, 2}, + "podTrue": {as.TokenIsTrue, tnf.SUCCESS, 2}, + "podNotSet": {as.TokenNotSet, tnf.SUCCESS, 0}, + "saFalse": {as.TokenIsFalse, tnf.SUCCESS, 2}, + "saTrue": {as.TokenIsTrue, tnf.SUCCESS, 2}, + "saNotSet": {as.TokenNotSet, tnf.SUCCESS, 0}, +} + +func getMockOutputFilename(testName string) string { + return path.Join(testDataDirectory, fmt.Sprintf("%s%s", testName, testDataFileSuffix)) +} + +func getMockOutput(t *testing.T, testName string) string { + fileName := getMockOutputFilename(testName) + b, err := os.ReadFile(fileName) + assert.Nil(t, err) + return string(b) +} + +// Test_NewAutomountService is the unit test for NewAutomountService(). +func Test_NewAutomountService(t *testing.T) { + automount := as.NewAutomountService(as.WithNamespace(testNamespace), as.WithServiceAccount(testServiceAccount)) + assert.NotNil(t, automount) + assert.Equal(t, automount.Result(), tnf.ERROR) + // test creating with pod + automount = as.NewAutomountService(as.WithNamespace(testNamespace), as.WithPodname(testPodname)) + assert.NotNil(t, automount) + assert.Equal(t, automount.Result(), tnf.ERROR) +} + +// Test_Automountservice_GetIdentifier is the unit test for Automountservice_GetIdentifier(). +func TestAutomountservice_GetIdentifier(t *testing.T) { + test := as.NewAutomountService() + assert.Equal(t, identifier.AutomountServiceIdentifier, test.GetIdentifier()) +} + +// Test_Automountservice_ReelEOF is the unit test for Automountservice_ReelEOF(). +func TestAutomountservice_ReelEOF(t *testing.T) { + test := as.NewAutomountService() + assert.NotNil(t, test) + test.ReelEOF() +} + +func Test_Automountservice_Args(t *testing.T) { + test := as.NewAutomountService(as.WithNamespace(testNamespace), as.WithServiceAccount(testServiceAccount)) + args := []string{"oc", "-n", testNamespace, "get", "serviceaccounts", testServiceAccount, "-o", "json"} + assert.ElementsMatch(t, args, test.Args()) + + test = as.NewAutomountService(as.WithNamespace(testNamespace), as.WithPodname(testPodname)) + args = []string{"oc", "-n", testNamespace, "get", "pods", testPodname, "-o", "json", "|", "jq", "-r", ".spec"} + assert.ElementsMatch(t, args, test.Args()) +} + +// Test_Automountservice_ReelTimeout is the unit test for automountservice}_ReelTimeout(). +func TestAutomountservice_ReelTimeout(t *testing.T) { + test := as.NewAutomountService(as.WithTimeout(testTimeoutDuration)) + assert.NotNil(t, test) + assert.Equal(t, testTimeoutDuration, test.Timeout()) + test.ReelTimeout() +} + +// Test_Automountservice_ReelMatch is the unit test for Automountservice_ReelMatch(). +func TestAutomountservice_ReelMatch(t *testing.T) { + for filename, testcase := range testCases { + matchMock := getMockOutput(t, filename) + test := as.NewAutomountService() + assert.NotNil(t, test) + // validate regular expression when serviceaccount is set to false + re := regexp.MustCompile(as.SaRegex) + matches := re.FindStringSubmatch(matchMock) + fmt.Println("-----") + for i := 0; i < len(matches); i++ { + fmt.Println("i=", i, " matches= ", matches[i]) + } + fmt.Println("-----") + assert.Len(t, matches, testcase.count) + if len(matches) == 0 { + matches = []string{""} + } + step := test.ReelMatch("", "", matches[0]) + assert.Nil(t, step) + assert.Equal(t, tnf.SUCCESS, test.Result()) + assert.Equal(t, test.Token(), testcase.token) + } +} diff --git a/cmd/generic/doc.go b/pkg/tnf/handlers/automountservice/doc.go similarity index 83% rename from cmd/generic/doc.go rename to pkg/tnf/handlers/automountservice/doc.go index 8fe4ff063..892de61d6 100644 --- a/cmd/generic/doc.go +++ b/pkg/tnf/handlers/automountservice/doc.go @@ -1,5 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. -// +// Copyright (C) 2020-2022 Red Hat, Inc. // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or @@ -14,5 +13,5 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// Package main introduces an executable (jsontest) used to run JSON based tests. -package main +// Package automountservice provides a test for automountservice. +package automountservice diff --git a/pkg/tnf/handlers/automountservice/testData/podFalse.yaml b/pkg/tnf/handlers/automountservice/testData/podFalse.yaml new file mode 100644 index 000000000..2020b8c27 --- /dev/null +++ b/pkg/tnf/handlers/automountservice/testData/podFalse.yaml @@ -0,0 +1,117 @@ +--- +{ + "affinity": { + "podAntiAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": [ + { + "labelSelector": { + "matchExpressions": [ + { + "key": "app", + "operator": "In", + "values": [ + "test" + ] + } + ] + }, + "topologyKey": "kubernetes.io/hostname" + } + ] + } + }, + "automountServiceAccountToken": false, + "containers": [ + { + "command": [ + "/bin/bash", + "-c", + "echo 'logs' && tail -f /dev/null" + ], + "image": "quay.io/testnetworkfunction/cnf-test-partner:latest", + "imagePullPolicy": "IfNotPresent", + "lifecycle": { + "preStop": { + "exec": { + "command": [ + "/bin/sh", + "-c", + "killall -0 tail" + ] + } + } + }, + "name": "test", + "ports": [ + { + "containerPort": 8080, + "name": "testport", + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "250m", + "memory": "512Mi" + }, + "requests": { + "cpu": "250m", + "memory": "512Mi" + } + }, + "securityContext": { + "capabilities": { + "drop": [ + "KILL", + "MKNOD", + "SETGID", + "SETUID" + ] + }, + "runAsUser": 1000700000 + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "imagePullSecrets": [ + { + "name": "default-dockercfg-ckml7" + } + ], + "nodeName": "worker-1.clus0.t5g.lab.eng.rdu2.redhat.com", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": { + "fsGroup": 1000700000, + "seLinuxOptions": { + "level": "s0:c26,c25" + } + }, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/memory-pressure", + "operator": "Exists" + } + ] +} diff --git a/pkg/tnf/handlers/automountservice/testData/podNotSet.yaml b/pkg/tnf/handlers/automountservice/testData/podNotSet.yaml new file mode 100644 index 000000000..850b0e42f --- /dev/null +++ b/pkg/tnf/handlers/automountservice/testData/podNotSet.yaml @@ -0,0 +1,116 @@ +--- +{ + "affinity": { + "podAntiAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": [ + { + "labelSelector": { + "matchExpressions": [ + { + "key": "app", + "operator": "In", + "values": [ + "test" + ] + } + ] + }, + "topologyKey": "kubernetes.io/hostname" + } + ] + } + }, + "containers": [ + { + "command": [ + "/bin/bash", + "-c", + "echo 'logs' && tail -f /dev/null" + ], + "image": "quay.io/testnetworkfunction/cnf-test-partner:latest", + "imagePullPolicy": "IfNotPresent", + "lifecycle": { + "preStop": { + "exec": { + "command": [ + "/bin/sh", + "-c", + "killall -0 tail" + ] + } + } + }, + "name": "test", + "ports": [ + { + "containerPort": 8080, + "name": "testport", + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "250m", + "memory": "512Mi" + }, + "requests": { + "cpu": "250m", + "memory": "512Mi" + } + }, + "securityContext": { + "capabilities": { + "drop": [ + "KILL", + "MKNOD", + "SETGID", + "SETUID" + ] + }, + "runAsUser": 1000700000 + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "imagePullSecrets": [ + { + "name": "default-dockercfg-ckml7" + } + ], + "nodeName": "worker-1.clus0.t5g.lab.eng.rdu2.redhat.com", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": { + "fsGroup": 1000700000, + "seLinuxOptions": { + "level": "s0:c26,c25" + } + }, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/memory-pressure", + "operator": "Exists" + } + ] +} diff --git a/pkg/tnf/handlers/automountservice/testData/podTrue.yaml b/pkg/tnf/handlers/automountservice/testData/podTrue.yaml new file mode 100644 index 000000000..d21e4152c --- /dev/null +++ b/pkg/tnf/handlers/automountservice/testData/podTrue.yaml @@ -0,0 +1,117 @@ +--- +{ + "affinity": { + "podAntiAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": [ + { + "labelSelector": { + "matchExpressions": [ + { + "key": "app", + "operator": "In", + "values": [ + "test" + ] + } + ] + }, + "topologyKey": "kubernetes.io/hostname" + } + ] + } + }, + "automountServiceAccountToken": true, + "containers": [ + { + "command": [ + "/bin/bash", + "-c", + "echo 'logs' && tail -f /dev/null" + ], + "image": "quay.io/testnetworkfunction/cnf-test-partner:latest", + "imagePullPolicy": "IfNotPresent", + "lifecycle": { + "preStop": { + "exec": { + "command": [ + "/bin/sh", + "-c", + "killall -0 tail" + ] + } + } + }, + "name": "test", + "ports": [ + { + "containerPort": 8080, + "name": "testport", + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "250m", + "memory": "512Mi" + }, + "requests": { + "cpu": "250m", + "memory": "512Mi" + } + }, + "securityContext": { + "capabilities": { + "drop": [ + "KILL", + "MKNOD", + "SETGID", + "SETUID" + ] + }, + "runAsUser": 1000700000 + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "imagePullSecrets": [ + { + "name": "default-dockercfg-ckml7" + } + ], + "nodeName": "worker-1.clus0.t5g.lab.eng.rdu2.redhat.com", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": { + "fsGroup": 1000700000, + "seLinuxOptions": { + "level": "s0:c26,c25" + } + }, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/memory-pressure", + "operator": "Exists" + } + ] +} diff --git a/pkg/tnf/handlers/automountservice/testData/saFalse.yaml b/pkg/tnf/handlers/automountservice/testData/saFalse.yaml new file mode 100644 index 000000000..9b9482616 --- /dev/null +++ b/pkg/tnf/handlers/automountservice/testData/saFalse.yaml @@ -0,0 +1,26 @@ +--- +{ + "apiVersion": "v1", + "automountServiceAccountToken": false, + "imagePullSecrets": [ + { + "name": "default-dockercfg-ckml7" + } + ], + "kind": "ServiceAccount", + "metadata": { + "creationTimestamp": "2021-11-23T23:49:25Z", + "name": "default", + "namespace": "salah3", + "resourceVersion": "193777061", + "uid": "ccbda73b-ad2f-44d8-b188-4dc2cb1945ac" + }, + "secrets": [ + { + "name": "default-token-xrczr" + }, + { + "name": "default-dockercfg-ckml7" + } + ] +} diff --git a/pkg/tnf/handlers/automountservice/testData/saNotSet.yaml b/pkg/tnf/handlers/automountservice/testData/saNotSet.yaml new file mode 100644 index 000000000..c9fbec7f3 --- /dev/null +++ b/pkg/tnf/handlers/automountservice/testData/saNotSet.yaml @@ -0,0 +1,25 @@ +--- +{ + "apiVersion": "v1", + "imagePullSecrets": [ + { + "name": "default-dockercfg-ckml7" + } + ], + "kind": "ServiceAccount", + "metadata": { + "creationTimestamp": "2021-11-23T23:49:25Z", + "name": "default", + "namespace": "salah3", + "resourceVersion": "193777061", + "uid": "ccbda73b-ad2f-44d8-b188-4dc2cb1945ac" + }, + "secrets": [ + { + "name": "default-token-xrczr" + }, + { + "name": "default-dockercfg-ckml7" + } + ] +} diff --git a/pkg/tnf/handlers/automountservice/testData/saTrue.yaml b/pkg/tnf/handlers/automountservice/testData/saTrue.yaml new file mode 100644 index 000000000..9c92f1f3b --- /dev/null +++ b/pkg/tnf/handlers/automountservice/testData/saTrue.yaml @@ -0,0 +1,26 @@ +--- +{ + "apiVersion": "v1", + "automountServiceAccountToken": true, + "imagePullSecrets": [ + { + "name": "default-dockercfg-ckml7" + } + ], + "kind": "ServiceAccount", + "metadata": { + "creationTimestamp": "2021-11-23T23:49:25Z", + "name": "default", + "namespace": "salah3", + "resourceVersion": "193777061", + "uid": "ccbda73b-ad2f-44d8-b188-4dc2cb1945ac" + }, + "secrets": [ + { + "name": "default-token-xrczr" + }, + { + "name": "default-dockercfg-ckml7" + } + ] +} diff --git a/pkg/tnf/handlers/base/redhat/doc.go b/pkg/tnf/handlers/base/redhat/doc.go index 3316b364d..cd18118b7 100644 --- a/pkg/tnf/handlers/base/redhat/doc.go +++ b/pkg/tnf/handlers/base/redhat/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/base/redhat/version.go b/pkg/tnf/handlers/base/redhat/version.go index 4ad866619..cc3a523fb 100644 --- a/pkg/tnf/handlers/base/redhat/version.go +++ b/pkg/tnf/handlers/base/redhat/version.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -47,8 +47,6 @@ type Release struct { timeout time.Duration // args stores the command and arguments. args []string - // release contains the contents of /etc/redhat-release if it exists, or "NOT Red Hat Based" if it does not exist. - release string // isRedHatBased contains whether the container is based on Red Hat technologies. isRedHatBased bool } diff --git a/pkg/tnf/handlers/base/redhat/version_test.go b/pkg/tnf/handlers/base/redhat/version_test.go index e941cbcc6..80360c958 100644 --- a/pkg/tnf/handlers/base/redhat/version_test.go +++ b/pkg/tnf/handlers/base/redhat/version_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/bootconfigentries/bootconfigentries.go b/pkg/tnf/handlers/bootconfigentries/bootconfigentries.go index 683782f7b..ccb290c6e 100644 --- a/pkg/tnf/handlers/bootconfigentries/bootconfigentries.go +++ b/pkg/tnf/handlers/bootconfigentries/bootconfigentries.go @@ -38,12 +38,12 @@ type BootConfigEntries struct { } // NewBootConfigEntries creates a BootConfigEntries tnf.Test. -func NewBootConfigEntries(timeout time.Duration, nodeName string) *BootConfigEntries { +func NewBootConfigEntries(timeout time.Duration) *BootConfigEntries { return &BootConfigEntries{ timeout: timeout, result: tnf.ERROR, args: []string{ - "echo", "\"ls /host/boot/loader/entries/\"", "|", "oc", "debug", "-q", "node/" + nodeName, + "ls /host/boot/loader/entries/", }, } } diff --git a/pkg/tnf/handlers/bootconfigentries/bootconfigentries_test.go b/pkg/tnf/handlers/bootconfigentries/bootconfigentries_test.go index 9335b0818..281b694e7 100644 --- a/pkg/tnf/handlers/bootconfigentries/bootconfigentries_test.go +++ b/pkg/tnf/handlers/bootconfigentries/bootconfigentries_test.go @@ -27,13 +27,13 @@ import ( ) func TestNewBootConfigEntries(t *testing.T) { - newBootConfig := bootconfigentries.NewBootConfigEntries(testTimeoutDuration, testNodeName) + newBootConfig := bootconfigentries.NewBootConfigEntries(testTimeoutDuration) assert.NotNil(t, newBootConfig) assert.Equal(t, tnf.ERROR, newBootConfig.Result()) } func Test_ReelFirst(t *testing.T) { - newBootConfig := bootconfigentries.NewBootConfigEntries(testTimeoutDuration, testNodeName) + newBootConfig := bootconfigentries.NewBootConfigEntries(testTimeoutDuration) assert.NotNil(t, newBootConfig) firstStep := newBootConfig.ReelFirst() re := regexp.MustCompile(firstStep.Expect[0]) @@ -43,7 +43,7 @@ func Test_ReelFirst(t *testing.T) { } func Test_ReelMatch(t *testing.T) { - newBootConfig := bootconfigentries.NewBootConfigEntries(testTimeoutDuration, testNodeName) + newBootConfig := bootconfigentries.NewBootConfigEntries(testTimeoutDuration) assert.NotNil(t, newBootConfig) step := newBootConfig.ReelMatch("", "", testInput) assert.Nil(t, step) @@ -52,7 +52,7 @@ func Test_ReelMatch(t *testing.T) { // Just ensure there are no panics. func Test_ReelEof(t *testing.T) { - newBootConfig := bootconfigentries.NewBootConfigEntries(testTimeoutDuration, testNodeName) + newBootConfig := bootconfigentries.NewBootConfigEntries(testTimeoutDuration) assert.NotNil(t, newBootConfig) newBootConfig.ReelEOF() } @@ -62,5 +62,4 @@ const ( testInput = `ostree-1-rhcos.conf ostree-2-rhcos.conf ` - testNodeName = "crc-l6qvn-master-0" ) diff --git a/pkg/tnf/handlers/clusterrolebinding/clusterrolebinding.go b/pkg/tnf/handlers/clusterrolebinding/clusterrolebinding.go index 10632c9b2..e9d077fbd 100644 --- a/pkg/tnf/handlers/clusterrolebinding/clusterrolebinding.go +++ b/pkg/tnf/handlers/clusterrolebinding/clusterrolebinding.go @@ -44,9 +44,9 @@ func NewClusterRoleBinding(timeout time.Duration, serviceAccountName, podNamespa timeout: timeout, result: tnf.ERROR, args: []string{ - "oc get clusterrolebindings -o custom-columns='NAME:metadata.name,SERVICE_ACCOUNTS:subjects[?(@.kind==\"ServiceAccount\")]' | grep -E '" + + "oc get clusterrolebindings -o custom-columns='NAME:metadata.name,SERVICE_ACCOUNTS:subjects[?(@.kind==\"ServiceAccount\")]' | grep -E ' name:" + serviceAccountSubString + - "|SERVICE_ACCOUNTS'"}, + " |SERVICE_ACCOUNTS'"}, } } @@ -60,7 +60,7 @@ func (crb *ClusterRoleBinding) Args() []string { return crb.args } -// GetIdentifier returns the tnf.Test specific identifiesa. +// GetIdentifier returns the tnf.Test specific identifier. func (crb *ClusterRoleBinding) GetIdentifier() identifier.Identifier { return identifier.ClusterRoleBindingIdentifier } diff --git a/pkg/tnf/handlers/clusterversion/clusterversion.go b/pkg/tnf/handlers/clusterversion/clusterversion.go new file mode 100644 index 000000000..276436c11 --- /dev/null +++ b/pkg/tnf/handlers/clusterversion/clusterversion.go @@ -0,0 +1,131 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package clusterversion + +import ( + "regexp" + "time" + + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/identifier" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" +) + +const ( + verRegex = "(?s).+" + numVersionsOcp = 3 + numVersionsMinikube = 2 +) + +// TestMetadata holds OCP version strings and test metadata +type TestMetadata struct { + versions ClusterVersion + result int + timeout time.Duration + args []string +} + +// ClusterVersion holds OCP version strings +type ClusterVersion struct { + Ocp, Oc, K8s string +} + +// NewClusterVersion creates a new TestMetadata tnf.Test. +// Just gets the ocp version for client and server +func NewClusterVersion(timeout time.Duration) *TestMetadata { + args := []string{"oc", "version"} + return &TestMetadata{ + timeout: timeout, + result: tnf.ERROR, + args: args, + } +} + +// Args returns the command line args for the test. +func (ver *TestMetadata) Args() []string { + return ver.args +} + +// GetVersions returns OCP client version. +func (ver *TestMetadata) GetVersions() ClusterVersion { + return ver.versions +} + +// GetIdentifier returns the tnf.Test specific identifier. +func (ver *TestMetadata) GetIdentifier() identifier.Identifier { + return identifier.ClusterVersionIdentifier +} + +// Timeout returns the timeout in seconds for the test. +func (ver *TestMetadata) Timeout() time.Duration { + return ver.timeout +} + +// Result returns the test result. +func (ver *TestMetadata) Result() int { + return ver.result +} + +// ReelFirst returns a step which expects the ping statistics within the test timeout. +func (ver *TestMetadata) ReelFirst() *reel.Step { + return &reel.Step{ + Expect: []string{verRegex}, + Timeout: ver.timeout, + } +} + +func deleteEmpty(s []string) []string { + var r []string + for _, str := range s { + if str != "" { + r = append(r, str) + } + } + return r +} + +// ReelMatch ensures that list of nodes is not empty and stores the names as []string +func (ver *TestMetadata) ReelMatch(_, _, match string) *reel.Step { + re := regexp.MustCompile("(Server Version: )|(Client Version: )|(Kubernetes Version: )|(\n)") + versions := re.Split(match, -1) + versions = deleteEmpty(versions) + if len(versions) != numVersionsOcp && len(versions) != numVersionsMinikube { + ver.result = tnf.FAILURE + return nil + } + ver.result = tnf.SUCCESS + + if len(versions) == numVersionsOcp { + ver.versions.Oc = versions[0] + ver.versions.Ocp = versions[1] + ver.versions.K8s = versions[2] + } else { + ver.versions.Oc = versions[0] + ver.versions.Ocp = "n/a" + ver.versions.K8s = versions[1] + } + return nil +} + +// ReelTimeout does nothing; no action is necessary upon timeout. +func (ver *TestMetadata) ReelTimeout() *reel.Step { + return nil +} + +// ReelEOF does nothing; no action is necessary upon EOF. +func (ver *TestMetadata) ReelEOF() { +} diff --git a/pkg/tnf/handlers/clusterversion/clusterversion_test.go b/pkg/tnf/handlers/clusterversion/clusterversion_test.go new file mode 100644 index 000000000..1bacf5590 --- /dev/null +++ b/pkg/tnf/handlers/clusterversion/clusterversion_test.go @@ -0,0 +1,121 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package clusterversion_test + +import ( + "regexp" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf" + ver "github.com/test-network-function/test-network-function/pkg/tnf/handlers/clusterversion" +) + +func Test_NewNodeNames(t *testing.T) { + newVer := ver.NewClusterVersion(testTimeoutDuration) + assert.NotNil(t, newVer) + assert.Equal(t, testTimeoutDuration, newVer.Timeout()) + assert.Equal(t, newVer.Result(), tnf.ERROR) +} + +func Test_ReelFirstPositiveOcp(t *testing.T) { + newVer := ver.NewClusterVersion(testTimeoutDuration) + assert.NotNil(t, newVer) + firstStep := newVer.ReelFirst() + re := regexp.MustCompile(firstStep.Expect[0]) + matches := re.FindStringSubmatch(testInputSuccessOcp) + assert.Len(t, matches, 1) + assert.Equal(t, testInputSuccessOcp, matches[0]) +} + +func Test_ReelFirstPositiveMinikube(t *testing.T) { + newVer := ver.NewClusterVersion(testTimeoutDuration) + assert.NotNil(t, newVer) + firstStep := newVer.ReelFirst() + re := regexp.MustCompile(firstStep.Expect[0]) + matches := re.FindStringSubmatch(testInputSuccessMinikube) + assert.Len(t, matches, 1) + assert.Equal(t, testInputSuccessMinikube, matches[0]) +} + +func Test_ReelFirstPositiveEmpty(t *testing.T) { + newVer := ver.NewClusterVersion(testTimeoutDuration) + assert.NotNil(t, newVer) + firstStep := newVer.ReelFirst() + re := regexp.MustCompile(firstStep.Expect[0]) + matches := re.FindStringSubmatch(testInputFailure) + assert.Len(t, matches, 1) + assert.Equal(t, testInputFailure, matches[0]) +} + +func Test_ReelFirstNegative(t *testing.T) { + newVer := ver.NewClusterVersion(testTimeoutDuration) + assert.NotNil(t, newVer) + firstStep := newVer.ReelFirst() + re := regexp.MustCompile(firstStep.Expect[0]) + matches := re.FindStringSubmatch(testInputError) + assert.Len(t, matches, 0) +} + +func Test_ReelMatchSuccessOcp(t *testing.T) { + newVer := ver.NewClusterVersion(testTimeoutDuration) + assert.NotNil(t, newVer) + step := newVer.ReelMatch("", "", testInputSuccessOcp) + assert.Nil(t, step) + assert.Equal(t, tnf.SUCCESS, newVer.Result()) + assert.Equal(t, newVer.GetVersions().Oc, "4.7.16") + assert.Equal(t, newVer.GetVersions().Ocp, "4.8.3") + assert.Equal(t, newVer.GetVersions().K8s, "v1.21.1+051ac4f") +} + +func Test_ReelMatchSuccessMinikube(t *testing.T) { + newVer := ver.NewClusterVersion(testTimeoutDuration) + assert.NotNil(t, newVer) + step := newVer.ReelMatch("", "", testInputSuccessMinikube) + assert.Nil(t, step) + assert.Equal(t, tnf.SUCCESS, newVer.Result()) + assert.Equal(t, newVer.GetVersions().Oc, "4.7.16") + assert.Equal(t, newVer.GetVersions().Ocp, "n/a") + assert.Equal(t, newVer.GetVersions().K8s, "v1.21.1+051ac4f") +} + +func Test_ReelMatchFail(t *testing.T) { + newVer := ver.NewClusterVersion(testTimeoutDuration) + assert.NotNil(t, newVer) + step := newVer.ReelMatch("", "", testInputFailure) + assert.Nil(t, step) + assert.Equal(t, tnf.FAILURE, newVer.Result()) + assert.Equal(t, newVer.GetVersions().Ocp, "") + assert.Equal(t, newVer.GetVersions().Oc, "") + assert.Equal(t, newVer.GetVersions().K8s, "") +} + +// Just ensure there are no panics. +func Test_ReelEof(t *testing.T) { + newVer := ver.NewClusterVersion(testTimeoutDuration) + assert.NotNil(t, newVer) + newVer.ReelEOF() +} + +const ( + testTimeoutDuration = time.Second * 2 + testInputError = "" + testInputFailure = "NAME\n" + testInputSuccessOcp = "Client Version: 4.7.16\nServer Version: 4.8.3\nKubernetes Version: v1.21.1+051ac4f\n" + testInputSuccessMinikube = "Client Version: 4.7.16\nKubernetes Version: v1.21.1+051ac4f\n" +) diff --git a/pkg/tnf/handlers/nodehugepages/doc.go b/pkg/tnf/handlers/clusterversion/doc.go similarity index 87% rename from pkg/tnf/handlers/nodehugepages/doc.go rename to pkg/tnf/handlers/clusterversion/doc.go index 94314c2ef..7dd2f7963 100644 --- a/pkg/tnf/handlers/nodehugepages/doc.go +++ b/pkg/tnf/handlers/clusterversion/doc.go @@ -14,5 +14,5 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// Package nodehugepages provides a test for verifying a node's hugepages settings -package nodehugepages +// Package clusterversion provides a test for reading the cluster's node names +package clusterversion diff --git a/pkg/tnf/handlers/cnffsdiff/cnffsdiff.go b/pkg/tnf/handlers/cnffsdiff/cnffsdiff.go index 8fad03662..0c2652351 100644 --- a/pkg/tnf/handlers/cnffsdiff/cnffsdiff.go +++ b/pkg/tnf/handlers/cnffsdiff/cnffsdiff.go @@ -29,15 +29,19 @@ type CnfFsDiff struct { result int timeout time.Duration args []string - diff string } const ( - // SuccessfulOutputRegex matches a successfully run "fsdiff" command. That does not mean that no errors or drops - // occurred during the test. - SuccessfulOutputRegex = `(?m)empty\n` - // AcceptAllRegex matches all strings - AcceptAllRegex = `(?m)(.|\n)+` + varlibrpm = `(?m)[\t|\s]\/var\/lib\/rpm[.]*` + varlibdpkg = `(?m)[\t|\s]\/var\/lib\/dpkg[.]*` + bin = `(?m)[\t|\s]\/bin[.]*` + sbin = `(?m)[\t|\s]\/sbin[.]*` + lib = `(?m)[\t|\s]\/lib[.]*` + usrbin = `(?m)[\t|\s]\/usr\/bin[.]*` + usrsbin = `(?m)[\t|\s]\/usr\/sbin[.]*` + usrlib = `(?m)[\t|\s]\/usr\/lib[.]*` + successfulOutputRegex = `(?m){}` + acceptAllRegex = `(?m)(.|\n)+` ) // Args returns the command line args for the test. @@ -71,10 +75,12 @@ func (p *CnfFsDiff) ReelFirst() *reel.Step { // ReelMatch checks if the test passed the first regex which means there were no installation on the container // or the second regex which accepts everything and means that something in the container was installed. func (p *CnfFsDiff) ReelMatch(pattern, before, match string) *reel.Step { - if pattern == SuccessfulOutputRegex { - p.result = tnf.SUCCESS - } else { + p.result = tnf.SUCCESS + switch pattern { + case varlibrpm, varlibdpkg, bin, sbin, lib, usrbin, usrsbin, usrlib: p.result = tnf.FAILURE + case successfulOutputRegex: + p.result = tnf.SUCCESS } return nil } @@ -90,13 +96,13 @@ func (p *CnfFsDiff) ReelEOF() { // Command returns command line args for checking the fs difference between a container and it's image func Command(containerID string) []string { - return []string{"/diff-fs.sh", containerID} + return []string{"chroot", "/host", "podman", "diff", "--format", "json", containerID} } // NewFsDiff creates a new `FsDiff` test which checks the fs difference between a container and it's image -func NewFsDiff(timeout time.Duration, containerID string) *CnfFsDiff { +func NewFsDiff(timeout time.Duration, containerID, nodeName string) *CnfFsDiff { return &CnfFsDiff{ - result: tnf.ERROR, + result: tnf.SUCCESS, timeout: timeout, args: Command(containerID), } @@ -104,5 +110,5 @@ func NewFsDiff(timeout time.Duration, containerID string) *CnfFsDiff { // GetReelFirstRegularExpressions returns the regular expressions used for matching in ReelFirst. func (p *CnfFsDiff) GetReelFirstRegularExpressions() []string { - return []string{SuccessfulOutputRegex, AcceptAllRegex} + return []string{varlibrpm, varlibdpkg, bin, sbin, lib, usrbin, usrsbin, usrlib, successfulOutputRegex, acceptAllRegex} } diff --git a/pkg/tnf/handlers/command/command.json b/pkg/tnf/handlers/command/command.json new file mode 100644 index 000000000..75ca56c32 --- /dev/null +++ b/pkg/tnf/handlers/command/command.json @@ -0,0 +1,21 @@ + +{ + "identifier" : { + "url" : "http://test-network-function.com/tests/command", + "version": "v1.0.0" + }, + "description": "Handler to execute user custom commands.", + "testResult": 0, + "testTimeout": {{ .TIMEOUT }}, + "reelFirstStep": { + "execute": "{{ .COMMAND }}", + "expect": ["(?m).*"], + "timeout": {{ .TIMEOUT }} + }, + "resultContexts":[ + { + "pattern": "(?m).*", + "defaultResult": 1 + } + ] + } \ No newline at end of file diff --git a/pkg/tnf/handlers/command/command_test.go b/pkg/tnf/handlers/command/command_test.go new file mode 100644 index 000000000..5da65f945 --- /dev/null +++ b/pkg/tnf/handlers/command/command_test.go @@ -0,0 +1,113 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package command + +import ( + "path" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/generic" + + "github.com/test-network-function/test-network-function/pkg/tnf/identifier" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" + "github.com/xeipuuv/gojsonschema" +) + +const ( + testTimeoutDuration = time.Second * 1 +) + +var ( + pathRelativeToRoot = path.Join("..", "..", "..", "..") + genericTestSchemaFile = path.Join("schemas", "generic-test.schema.json") + checkSubFilename = "command.json" + expectedPassPattern = "(?m).*" + pathToTestSchemaFile = path.Join(pathRelativeToRoot, genericTestSchemaFile) + testCommand = "oc get pods -n tnf" +) + +func createTest() (*tnf.Tester, []reel.Handler, *gojsonschema.Result, error) { + values := make(map[string]interface{}) + values["COMMAND"] = testCommand + values["TIMEOUT"] = testTimeoutDuration.Nanoseconds() + return generic.NewGenericFromMap(checkSubFilename, pathToTestSchemaFile, values) +} +func TestCommand_Args(t *testing.T) { + test, handlers, jsonParseResult, err := createTest() + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + assert.Nil(t, (*test).Args()) +} + +func TestCommand_GetIdentifier(t *testing.T) { + test, handlers, jsonParseResult, err := createTest() + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + assert.Equal(t, identifier.CommandIdentifier, (*test).GetIdentifier()) +} + +func TestCommand_ReelFirst(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + step := handler.ReelFirst() + assert.Equal(t, testCommand, step.Execute) + assert.Contains(t, step.Expect, expectedPassPattern) + assert.Equal(t, testTimeoutDuration, step.Timeout) +} + +func TestCommand_ReelEOF(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + // just ensure there isn't a panic + handler.ReelEOF() +} + +func TestCommand_ReelTimeout(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + assert.Nil(t, handler.ReelTimeout()) +} + +func TestCommand_ReelMatch(t *testing.T) { + tester, handlers, jsonParseResult, err := createTest() + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + // Positive Test + step := handler.ReelMatch(expectedPassPattern, "", "OK") + assert.Nil(t, step) + assert.Equal(t, tnf.SUCCESS, (*tester).Result()) +} diff --git a/cmd/catalog/doc.go b/pkg/tnf/handlers/command/doc.go similarity index 84% rename from cmd/catalog/doc.go rename to pkg/tnf/handlers/command/doc.go index e43d69173..b9aae1fae 100644 --- a/cmd/catalog/doc.go +++ b/pkg/tnf/handlers/command/doc.go @@ -1,5 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. -// +// Copyright (C) 2020-2022 Red Hat, Inc. // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or @@ -14,5 +13,5 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// Package main provides a CLI driven tool to generate the test catalog. -package main +// Package command provides a test for command. +package command diff --git a/pkg/tnf/handlers/container/doc.go b/pkg/tnf/handlers/container/doc.go index 8308879f7..94a88e4cc 100644 --- a/pkg/tnf/handlers/container/doc.go +++ b/pkg/tnf/handlers/container/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/container/pod.go b/pkg/tnf/handlers/container/pod.go index 63ef5a8c1..e1d56e47d 100644 --- a/pkg/tnf/handlers/container/pod.go +++ b/pkg/tnf/handlers/container/pod.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -71,7 +71,7 @@ func (p *Pod) Result() int { // ReelFirst returns a step which expects an pod status for the given pod. func (p *Pod) ReelFirst() *reel.Step { return &reel.Step{ - Expect: []string{testcases.GetOutRegExp(testcases.AllowAll)}, + Expect: []string{testcases.GetOutRegExp(testcases.AllowEmpty)}, Timeout: p.timeout, } } @@ -100,14 +100,15 @@ func (p *Pod) ReelMatch(_, _, match string) *reel.Step { p.result = tnf.SUCCESS return nil } - replacer := strings.NewReplacer(`[`, ``, "\"", ``, `]`, ``, `, `, `,`) + replacer := strings.NewReplacer(`[`, ``, "\"", ``, `]`, ``, "\n", ``, "\t", ``) + match = replacer.Replace(match) f := func(c rune) bool { return c == ',' } matchSlice := strings.FieldsFunc(match, f) for _, status := range matchSlice { - if contains(p.ExpectStatus, status) { + if contains(p.ExpectStatus, strings.Trim(status, " ")) { if p.Action == testcases.Deny { // Single deny match is failure. return nil } @@ -138,12 +139,12 @@ func (p *Pod) ReelTimeout() *reel.Step { func (p *Pod) ReelEOF() { } -// Facts collects facts of the container +// Facts collects facts of the pod func (p *Pod) Facts() string { return p.facts } -// NewPod creates a `Container` test on the configured test cases. +// NewPod creates a `Pod` test on the configured test cases. func NewPod(args []string, name, namespace string, expectedStatus []string, resultType testcases.TestResultType, action testcases.TestAction, timeout time.Duration) *Pod { return &Pod{ Name: name, diff --git a/pkg/tnf/handlers/container/pod_test.go b/pkg/tnf/handlers/container/pod_test.go index 40b3faf1d..e341ae4dd 100644 --- a/pkg/tnf/handlers/container/pod_test.go +++ b/pkg/tnf/handlers/container/pod_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -60,7 +60,7 @@ func TestPod_ReelFirst(t *testing.T) { c := container.NewPod(args, name, namespace, stringExpectedStatus, testcases.StringType, testcases.Allow, testTimeoutDuration) step := c.ReelFirst() assert.Equal(t, "", step.Execute) - assert.Equal(t, []string{testcases.GetOutRegExp(testcases.AllowAll)}, step.Expect) + assert.Equal(t, []string{testcases.GetOutRegExp(testcases.AllowEmpty)}, step.Expect) assert.Equal(t, testTimeoutDuration, step.Timeout) } diff --git a/pkg/tnf/handlers/containerid/containerid.go b/pkg/tnf/handlers/containerid/containerid.go deleted file mode 100644 index 042e17229..000000000 --- a/pkg/tnf/handlers/containerid/containerid.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package containerid - -import ( - "regexp" - "time" - - "github.com/test-network-function/test-network-function/pkg/tnf" - "github.com/test-network-function/test-network-function/pkg/tnf/dependencies" - "github.com/test-network-function/test-network-function/pkg/tnf/identifier" - "github.com/test-network-function/test-network-function/pkg/tnf/reel" -) - -// ContainerID provides a way to find an id of a container from inside of it. -type ContainerID struct { - result int - timeout time.Duration - args []string - id string -} - -const ( - // SuccessfulOutputRegex matches a cgroup name that should be generated by crio and includes the container id - // inside of it in a known location - SuccessfulOutputRegex = `crio-(\w+)\.scope` -) - -// Args returns the command line args for the test. -func (id *ContainerID) Args() []string { - return id.args -} - -// GetIdentifier returns the tnf.Test specific identifier. -func (id *ContainerID) GetIdentifier() identifier.Identifier { - return identifier.ContainerIDIdentifier -} - -// Timeout returns the timeout in seconds for the test. -func (id *ContainerID) Timeout() time.Duration { - return id.timeout -} - -// Result returns the test result. -func (id *ContainerID) Result() int { - return id.result -} - -// ReelFirst returns a step which expects the container id within the test timeout. -func (id *ContainerID) ReelFirst() *reel.Step { - return &reel.Step{ - Expect: []string{SuccessfulOutputRegex}, - Timeout: id.timeout, - } -} - -// ReelMatch parses the the result of "/proc/self/cgroup" looking for a cgroup generated by crio -// and resolve the container id from it -func (id *ContainerID) ReelMatch(_, _, match string) *reel.Step { - re := regexp.MustCompile(SuccessfulOutputRegex) - matched := re.FindStringSubmatch(match) - if matched != nil { - id.result = tnf.SUCCESS - id.id = matched[1] - } else { - id.result = tnf.FAILURE - } - return nil -} - -// ReelTimeout returns a step which kills the container id test by sending it ^C. -func (id *ContainerID) ReelTimeout() *reel.Step { - return nil -} - -// ReelEOF does nothing; container id requires no intervention on eof. -func (id *ContainerID) ReelEOF() { -} - -// Command returns command line args for getting the cgroups of the host machine -func Command() []string { - return []string{"cat", dependencies.CgroupProcfsPath} -} - -// NewContainerID creates a new container id test which lists all cgroups of the host from inside a pod -// and resolve the id of the container itself from it -func NewContainerID(timeout time.Duration) *ContainerID { - return &ContainerID{ - result: tnf.ERROR, - timeout: timeout, - args: Command(), - } -} - -// GetID returns the container id -func (id *ContainerID) GetID() string { - return id.id -} diff --git a/pkg/tnf/handlers/crdstatusexistence/crdstatusexistence.json b/pkg/tnf/handlers/crdstatusexistence/crdstatusexistence.json new file mode 100644 index 000000000..90065c428 --- /dev/null +++ b/pkg/tnf/handlers/crdstatusexistence/crdstatusexistence.json @@ -0,0 +1,28 @@ +{ + "identifier": { + "url": "http://test-network-function.com/tests/crdStatusExistence", + "version": "v1.0.0" + }, + "description": "This test checks whether a given CRD has defined status subresource spec for all its versions.", + "testResult": 0, + "testTimeout": {{ .TIMEOUT }}, + "reelFirstStep": { + "execute": + "oc get crd {{ .CRD_NAME }} -o json | jq -r '[.spec.versions[]] | if all(.schema.openAPIV3Schema.properties.status) then \"OK\" else \"FAIL\" end'", + "expect": [ + "(?m)OK", + "(?m)FAIL" + ], + "timeout": {{ .TIMEOUT }} + }, + "resultContexts": [ + { + "pattern": "(?m)OK", + "defaultResult": 1 + }, + { + "pattern": "(?m)FAIL", + "defaultResult": 2 + } + ] + } diff --git a/pkg/tnf/handlers/crdstatusexistence/crdstatusexistence_test.go b/pkg/tnf/handlers/crdstatusexistence/crdstatusexistence_test.go new file mode 100644 index 000000000..0cf38554c --- /dev/null +++ b/pkg/tnf/handlers/crdstatusexistence/crdstatusexistence_test.go @@ -0,0 +1,111 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package crdstatusexistence_test + +import ( + "fmt" + "path" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/generic" + "github.com/test-network-function/test-network-function/pkg/tnf/identifier" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" + "github.com/xeipuuv/gojsonschema" +) + +const ( + testTimeoutDuration = 10 * time.Second +) + +var ( + genericTestSchemaFile = path.Join("schemas", "generic-test.schema.json") + jsonTestFileName = "crdstatusexistence.json" + expectedPassPattern = "(?m)OK" + expectedFailPattern = "(?m)FAIL" + pathRelativeToRoot = path.Join("..", "..", "..", "..") + pathToTestSchemaFile = path.Join(pathRelativeToRoot, genericTestSchemaFile) + testCrdName = "testCrdFakeName" +) + +func createTest() (*tnf.Tester, []reel.Handler, *gojsonschema.Result, error) { + values := make(map[string]interface{}) + values["CRD_NAME"] = testCrdName + values["TIMEOUT"] = testTimeoutDuration.Nanoseconds() + return generic.NewGenericFromMap(jsonTestFileName, pathToTestSchemaFile, values) +} + +func TestPods_Args(t *testing.T) { + test, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Nil(t, (*test).Args()) +} + +func TestPods_GetIdentifier(t *testing.T) { + test, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, identifier.CrdStatusExistenceIdentifier, (*test).GetIdentifier()) +} + +func TestPods_ReelFirst(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + step := handler.ReelFirst() + expectedReelFirstExecute := fmt.Sprintf( + "oc get crd %s -o json | jq -r '[.spec.versions[]] | if all(.schema.openAPIV3Schema.properties.status) then \"OK\" else \"FAIL\" end'", + testCrdName) + assert.Equal(t, expectedReelFirstExecute, step.Execute) + assert.Contains(t, step.Expect, expectedPassPattern, expectedFailPattern) + assert.Equal(t, testTimeoutDuration, step.Timeout) +} + +func TestPods_ReelMatch(t *testing.T) { + tester, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + + // Positive Test + step := handler.ReelMatch(expectedPassPattern, "", "OK") + assert.Nil(t, step) + assert.Equal(t, tnf.SUCCESS, (*tester).Result()) + + // Negative Test + step = handler.ReelMatch(expectedFailPattern, "", "FAIL") + assert.Nil(t, step) + assert.Equal(t, tnf.FAILURE, (*tester).Result()) +} diff --git a/pkg/tnf/handlers/crdstatusexistence/doc.go b/pkg/tnf/handlers/crdstatusexistence/doc.go new file mode 100644 index 000000000..8822126d4 --- /dev/null +++ b/pkg/tnf/handlers/crdstatusexistence/doc.go @@ -0,0 +1,18 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +// Package crdstatusexistence provides a test for checking the existence of status subresource in a given CRD. +package crdstatusexistence diff --git a/pkg/tnf/handlers/csidriver/csidriver.json b/pkg/tnf/handlers/csidriver/csidriver.json new file mode 100644 index 000000000..50237b888 --- /dev/null +++ b/pkg/tnf/handlers/csidriver/csidriver.json @@ -0,0 +1,24 @@ +{ + "identifier": { + "url": "http://test-network-function.com/tests/csiDriver", + "version": "v1.0.0" + }, + "description": "This test checks third party CSI driver installed in cluster for cnf.", + "testResult": 0, + "testTimeout": 10000000000, + "reelFirstStep": { + "execute": + "oc get csidriver -o json\n", + "expect": [ + "(?m)(.|\n)+" + ], + "timeout": 10000000000 + }, + "resultContexts": [ + { + "pattern": "(?m)(.|\n)+", + "defaultResult": 1 + } + ] + } + \ No newline at end of file diff --git a/pkg/tnf/handlers/csidriver/csidriver_test.go b/pkg/tnf/handlers/csidriver/csidriver_test.go new file mode 100644 index 000000000..42afe6149 --- /dev/null +++ b/pkg/tnf/handlers/csidriver/csidriver_test.go @@ -0,0 +1,122 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package csidriver_test + +import ( + "path" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/generic" + "github.com/test-network-function/test-network-function/pkg/tnf/identifier" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" + "github.com/xeipuuv/gojsonschema" +) + +const ( + testTimeoutDuration = time.Second * 10 +) + +var ( + genericTestSchemaFile = path.Join("schemas", "generic-test.schema.json") + csiDriverFilename = "csidriver.json" + /* #nosec G101 */ + expectedPassPattern = "(?m)(.|\n)+" + pathRelativeToRoot = path.Join("..", "..", "..", "..") + pathToTestSchemaFile = path.Join(pathRelativeToRoot, genericTestSchemaFile) +) + +func createTest() (*tnf.Tester, []reel.Handler, *gojsonschema.Result, error) { + return generic.NewGenericFromJSONFile(csiDriverFilename, pathToTestSchemaFile) +} + +func TestCSIs_Args(t *testing.T) { + test, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Nil(t, (*test).Args()) +} + +func TestCSIs_GetIdentifier(t *testing.T) { + test, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, identifier.CSIDriverIdentifier, (*test).GetIdentifier()) +} + +func TestCSIs_ReelFirst(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + step := handler.ReelFirst() + assert.Equal(t, "oc get csidriver -o json\n", step.Execute) + assert.Equal(t, []string{expectedPassPattern}, step.Expect) + assert.Equal(t, testTimeoutDuration, step.Timeout) +} + +func TestCSIs_ReelEOF(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + // just ensure there isn't a panic + handler.ReelEOF() +} + +func TestCSIs_ReelTimeout(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + assert.Nil(t, handler.ReelTimeout()) +} + +func TestCSIs_ReelMatch(t *testing.T) { + tester, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + + // Positive Test + step := handler.ReelMatch(expectedPassPattern, "", "anythingMatches") + assert.Nil(t, step) + assert.Equal(t, tnf.SUCCESS, (*tester).Result()) +} diff --git a/pkg/tnf/handlers/deployments/doc.go b/pkg/tnf/handlers/csidriver/doc.go similarity index 88% rename from pkg/tnf/handlers/deployments/doc.go rename to pkg/tnf/handlers/csidriver/doc.go index f79292b11..b730d3ab6 100644 --- a/pkg/tnf/handlers/deployments/doc.go +++ b/pkg/tnf/handlers/csidriver/doc.go @@ -14,5 +14,5 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// Package deployments provides a test for reading the namespace's deployments -package deployments +// Package csidriver provides a test for reading the CNF cluster csi driver info +package csidriver diff --git a/pkg/tnf/handlers/daemonset/daemonset.go b/pkg/tnf/handlers/daemonset/daemonset.go new file mode 100644 index 000000000..3a49cc726 --- /dev/null +++ b/pkg/tnf/handlers/daemonset/daemonset.go @@ -0,0 +1,159 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package daemonset + +import ( + "strconv" + "strings" + "time" + + log "github.com/sirupsen/logrus" + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/identifier" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" +) + +type Status struct { + Name string + Desired int + Current int + Ready int + Available int + Misscheduled int +} + +// DaemonSet is the reel handler struct. +type DaemonSet struct { + result int + timeout time.Duration + args []string + status Status +} + +const ( + dsRegex = "(?s).+" +) + +// NewDaemonSet returns a new DaemonSet handler struct. +func NewDaemonSet(timeout time.Duration, daemonset, namespace string) *DaemonSet { + return &DaemonSet{ + timeout: timeout, + result: tnf.ERROR, + args: []string{"oc", "-n", namespace, "get", "ds", daemonset, "-o", + "go-template='{{ .spec.template.metadata.name }} ", + "{{ if .status.desiredNumberScheduled}}{{ .status.desiredNumberScheduled }}{{else}}{{\"0\"}}{{end}}", + "{{ if .status.currentNumberScheduled}}{{ .status.currentNumberScheduled }}{{else}}{{\"0\"}}{{end}}", + "{{ if .status.numberAvailable}}{{ .status.numberAvailable }}{{else}}{{\"0\"}}{{end}}", + "{{ if .status.numberReady}}{{ .status.numberReady }}{{else}}{{\"0\"}}{{end}}", + "{{ if .status.numberMisscheduled}}{{ .status.numberMisscheduled }}{{else}}{{\"0\"}}{{end}} {{ printf \"\\n\" }}'", + }, + status: Status{}, + } +} + +// Args returns the initial execution/send command strings for handler DaemonSet. +func (ds *DaemonSet) Args() []string { + return ds.args +} + +// GetIdentifier returns the tnf.Test specific identifier. +func (ds *DaemonSet) GetIdentifier() identifier.Identifier { + // Return the DaemonSet handler identifier. + return identifier.DaemonSetIdentifier +} + +// Timeout returns the timeout for the test. +func (ds *DaemonSet) Timeout() time.Duration { + return ds.timeout +} + +// Result returns the test result. +func (ds *DaemonSet) Result() int { + return ds.result +} + +// ReelFirst returns a reel step for handler DaemonSet. +func (ds *DaemonSet) ReelFirst() *reel.Step { + return &reel.Step{ + Expect: []string{dsRegex}, + Timeout: ds.timeout, + } +} + +// ReelMatch parses the DaemonSet output and set the test result on match. +func (ds *DaemonSet) ReelMatch(_, _, match string) *reel.Step { + const numExpectedFields = 6 + trimmedMatch := strings.Trim(match, "\n") + lines := strings.Split(trimmedMatch, "\n")[0:] // Keep First line only + + for _, line := range lines { + if line == "" { + continue + } + fields := strings.Fields(line) + if len(fields) != numExpectedFields { + return nil + } + err := processResult(&ds.status, fields) + if err != nil { + log.Error("Error processing output ", err) + ds.status = Status{} + return nil + } + ds.result = tnf.SUCCESS + return nil + } + return nil +} +func processResult(status *Status, fields []string) error { + var err error + status.Name = fields[0] + status.Desired, err = strconv.Atoi(fields[1]) + if err != nil { + return err + } + status.Current, err = strconv.Atoi(fields[2]) + if err != nil { + return err + } + status.Available, err = strconv.Atoi(fields[3]) + if err != nil { + return err + } + status.Ready, err = strconv.Atoi(fields[4]) + if err != nil { + return err + } + status.Misscheduled, err = strconv.Atoi(fields[5]) + if err != nil { + return err + } + return nil +} + +// ReelTimeout function for DaemonSet will be called by the reel FSM when a expect timeout occurs. +func (ds *DaemonSet) ReelTimeout() *reel.Step { + return nil +} + +// ReelEOF function for DaemonSet will be called by the reel FSM when a EOF is read. +func (ds *DaemonSet) ReelEOF() { +} + +func (ds *DaemonSet) GetStatus() Status { + return ds.status +} diff --git a/pkg/tnf/handlers/daemonset/daemonset_test.go b/pkg/tnf/handlers/daemonset/daemonset_test.go new file mode 100644 index 000000000..dbb72d3c2 --- /dev/null +++ b/pkg/tnf/handlers/daemonset/daemonset_test.go @@ -0,0 +1,121 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package daemonset + +import ( + "fmt" + "os" + "path" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/identifier" +) + +const ( + testTimeoutDuration = 10 * time.Second + testNamespace = "default" + testDebugDaemonset = "debug" + testDataDirectory = "testdata" +) + +type DaemonSetTest struct { + daemonset Status + result int +} + +var testCases = map[string]DaemonSetTest{ + "valid_daemonset": {daemonset: Status{ + Name: "debug", + Desired: 1, + Current: 1, + Ready: 1, + Available: 1, + Misscheduled: 0, + }, result: tnf.SUCCESS}, + "non_valid_daemonset": {daemonset: Status{ + Name: "test", + Desired: 2, + Current: 1, + Ready: 1, + Available: 1, + Misscheduled: 0, + }, result: tnf.SUCCESS}, + "non_valid_output": {daemonset: Status{ + Name: "", + Desired: 0, + Current: 0, + Ready: 0, + Available: 0, + Misscheduled: 0, + }, result: tnf.ERROR}, +} + +func getMockOutputFilename(testName string) string { + return path.Join(testDataDirectory, testName) +} + +func getMockOutput(t *testing.T, testName string) string { + fileName := getMockOutputFilename(testName) + b, err := os.ReadFile(fileName) + assert.Nil(t, err) + return string(b) +} + +// Test_NewDaemonSet is the unit test for NewDaemonSet(). +func Test_NewDaemonSet(t *testing.T) { + newDs := NewDaemonSet(testTimeoutDuration, testDebugDaemonset, testNamespace) + assert.NotNil(t, newDs) + assert.Equal(t, testTimeoutDuration, newDs.Timeout()) + assert.Equal(t, newDs.Result(), tnf.ERROR) + assert.NotNil(t, newDs.GetStatus()) +} + +// Test_DaemonSet_GetIdentifier is the unit test for DaemonSet_GetIdentifier(). +func TestDaemonSet_GetIdentifier(t *testing.T) { + newDs := NewDaemonSet(testTimeoutDuration, testDebugDaemonset, testNamespace) + assert.Equal(t, identifier.DaemonSetIdentifier, newDs.GetIdentifier()) +} + +// Test_DaemonSet_ReelEOF is the unit test for DaemonSet_ReelEOF(). +func TestDaemonSet_ReelEOF(t *testing.T) { + newDs := NewDaemonSet(testTimeoutDuration, testDebugDaemonset, testNamespace) + assert.NotNil(t, newDs) + newDs.ReelEOF() +} + +// Test_DaemonSet_ReelTimeout is the unit test for DaemonSet}_ReelTimeout(). +func TestDaemonSet_ReelTimeout(t *testing.T) { + ds := NewDaemonSet(testTimeoutDuration, "debug", "default") + step := ds.ReelTimeout() + assert.Nil(t, step) +} + +// Test_DaemonSet_ReelMatch is the unit test for DaemonSet_ReelMatch(). +func TestDaemonSet_ReelMatch(t *testing.T) { + for testName, testCase := range testCases { + fmt.Println("process case ", testName) + ds := NewDaemonSet(testTimeoutDuration, testCase.daemonset.Name, "default") + matchMock := getMockOutput(t, testName) + step := ds.ReelMatch("", "", matchMock) + assert.Nil(t, step) + assert.Equal(t, testCase.daemonset, ds.GetStatus()) + assert.Equal(t, testCase.result, ds.result) + } +} diff --git a/pkg/tnf/handlers/daemonset/doc.go b/pkg/tnf/handlers/daemonset/doc.go new file mode 100644 index 000000000..280e3740a --- /dev/null +++ b/pkg/tnf/handlers/daemonset/doc.go @@ -0,0 +1,17 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +// Package daemonset provides a test for daemonset. +package daemonset diff --git a/pkg/tnf/handlers/daemonset/testdata/non_valid_daemonset b/pkg/tnf/handlers/daemonset/testdata/non_valid_daemonset new file mode 100644 index 000000000..7f092794c --- /dev/null +++ b/pkg/tnf/handlers/daemonset/testdata/non_valid_daemonset @@ -0,0 +1,2 @@ +test 2 1 1 1 0 + % \ No newline at end of file diff --git a/pkg/tnf/handlers/daemonset/testdata/non_valid_output b/pkg/tnf/handlers/daemonset/testdata/non_valid_output new file mode 100644 index 000000000..09aa7ea33 --- /dev/null +++ b/pkg/tnf/handlers/daemonset/testdata/non_valid_output @@ -0,0 +1 @@ +debug 2 2 0 0 diff --git a/pkg/tnf/handlers/daemonset/testdata/valid_daemonset b/pkg/tnf/handlers/daemonset/testdata/valid_daemonset new file mode 100644 index 000000000..1da1f8c7d --- /dev/null +++ b/pkg/tnf/handlers/daemonset/testdata/valid_daemonset @@ -0,0 +1,2 @@ +debug 1 1 1 1 0 + % \ No newline at end of file diff --git a/pkg/tnf/handlers/deployments/deployments_test.go b/pkg/tnf/handlers/deployments/deployments_test.go deleted file mode 100644 index 5515f8134..000000000 --- a/pkg/tnf/handlers/deployments/deployments_test.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package deployments_test - -import ( - "regexp" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/test-network-function/test-network-function/pkg/tnf" - dp "github.com/test-network-function/test-network-function/pkg/tnf/handlers/deployments" -) - -func Test_NewDeployments(t *testing.T) { - newDp := dp.NewDeployments(testTimeoutDuration, testNamespace) - assert.NotNil(t, newDp) - assert.Equal(t, testTimeoutDuration, newDp.Timeout()) - assert.Equal(t, newDp.Result(), tnf.ERROR) - assert.NotNil(t, newDp.GetDeployments()) -} - -func Test_ReelFirstPositive(t *testing.T) { - newDp := dp.NewDeployments(testTimeoutDuration, testNamespace) - assert.NotNil(t, newDp) - firstStep := newDp.ReelFirst() - re := regexp.MustCompile(firstStep.Expect[0]) - matches := re.FindStringSubmatch(testInputSuccess) - assert.Len(t, matches, 1) - assert.Equal(t, testInputSuccess, matches[0]) -} - -func Test_ReelFirstNegative(t *testing.T) { - newDp := dp.NewDeployments(testTimeoutDuration, testNamespace) - assert.NotNil(t, newDp) - firstStep := newDp.ReelFirst() - re := regexp.MustCompile(firstStep.Expect[0]) - matches := re.FindStringSubmatch(testInputError) - assert.Len(t, matches, 0) -} - -func Test_ReelMatchSuccess(t *testing.T) { - newDp := dp.NewDeployments(testTimeoutDuration, testNamespace) - assert.NotNil(t, newDp) - step := newDp.ReelMatch("", "", testInputSuccess) - assert.Nil(t, step) - assert.Equal(t, tnf.SUCCESS, newDp.Result()) - assert.Len(t, newDp.GetDeployments(), testInputSuccessNumLines) - - expectedDeployments := dp.DeploymentMap{ - "cdi-apiserver": {1, 1, 1, 1, 0}, - "hyperconverged-cluster-operator": {1, 0, 1, 0, 1}, - "virt-api": {2, 2, 2, 2, 0}, - "vm-import-operator": {0, 0, 0, 0, 0}, - } - deployments := newDp.GetDeployments() - - for name, expected := range expectedDeployments { - deployment, ok := deployments[name] - assert.True(t, ok) - assert.Equal(t, expected, deployment) - } -} - -// Just ensure there are no panics. -func Test_ReelEof(t *testing.T) { - newDp := dp.NewDeployments(testTimeoutDuration, testNamespace) - assert.NotNil(t, newDp) - newDp.ReelEOF() -} - -const ( - testTimeoutDuration = time.Second * 2 - testNamespace = "testNamespace" - testInputError = "" - testInputSuccessNumLines = 17 - testInputSuccess = `NAME REPLICAS READY UPDATED AVAILABLE UNAVAILABLE - cdi-apiserver 1 1 1 1 - cdi-deployment 1 1 1 1 - cdi-operator 1 1 1 1 - cdi-uploadproxy 1 1 1 1 - cluster-network-addons-operator 1 1 1 1 - hostpath-provisioner-operator 1 1 1 1 - hyperconverged-cluster-operator 1 1 1 - kubemacpool-mac-controller-manager 1 1 1 1 - kubevirt-ssp-operator 1 1 1 - nmstate-webhook 2 2 2 2 - node-maintenance-operator 1 1 1 - v2v-vmware 1 1 1 1 - virt-api 2 2 2 2 - virt-controller 2 2 2 2 - virt-operator 2 2 2 2 - virt-template-validator 2 2 2 2 - vm-import-operator 0 ` -) diff --git a/pkg/tnf/handlers/deploymentsdrain/deploymentsdrain.go b/pkg/tnf/handlers/deploymentsdrain/deploymentsdrain.go index 1eafd5814..1ee91e8c2 100644 --- a/pkg/tnf/handlers/deploymentsdrain/deploymentsdrain.go +++ b/pkg/tnf/handlers/deploymentsdrain/deploymentsdrain.go @@ -17,6 +17,7 @@ package deploymentsdrain import ( + "strings" "time" "github.com/test-network-function/test-network-function/pkg/tnf" @@ -34,20 +35,22 @@ type DeploymentsDrain struct { result int timeout time.Duration args []string + node string } // NewDeploymentsDrain creates a new DeploymentsDrain tnf.Test. -func NewDeploymentsDrain(timeout time.Duration, node string) *DeploymentsDrain { +func NewDeploymentsDrain(timeout time.Duration, nodeName string) *DeploymentsDrain { drainTimeout := timeout * drainTimeoutPercentage / 100 drainTimeoutString := drainTimeout.String() return &DeploymentsDrain{ timeout: timeout, result: tnf.ERROR, args: []string{ - "oc", "adm", "drain", node, "--pod-selector=pod-template-hash", "--disable-eviction=true", - "--delete-local-data=true", "--ignore-daemonsets=true", "--timeout=" + drainTimeoutString, + "oc", "adm", "drain", nodeName, "--pod-selector=pod-template-hash", "--disable-eviction=true", + "--delete-emptydir-data=true", "--ignore-daemonsets=true", "--timeout=" + drainTimeoutString, "&&", "echo", "SUCCESS", }, + node: nodeName, } } @@ -56,7 +59,7 @@ func (dd *DeploymentsDrain) Args() []string { return dd.args } -// GetIdentifier returns the tnf.Test specific identifiesa. +// GetIdentifier returns the tnf.Test specific identifier. func (dd *DeploymentsDrain) GetIdentifier() identifier.Identifier { return identifier.DeploymentsDrainIdentifier } @@ -87,7 +90,12 @@ func (dd *DeploymentsDrain) ReelMatch(_, _, _ string) *reel.Step { // ReelTimeout does nothing; no action is necessary upon timeout. func (dd *DeploymentsDrain) ReelTimeout() *reel.Step { - return nil + str := []string{"oc", "adm", "uncordon", dd.node} + return &reel.Step{ + Expect: []string{"(?m).*uncordoned"}, + Timeout: dd.timeout, + Execute: strings.Join(str, " "), + } } // ReelEOF does nothing; no action is necessary upon EOF. diff --git a/pkg/tnf/handlers/deploymentsnodes/deploymentsnodes.go b/pkg/tnf/handlers/deploymentsnodes/deploymentsnodes.go index 55d4c3470..1a302f45d 100644 --- a/pkg/tnf/handlers/deploymentsnodes/deploymentsnodes.go +++ b/pkg/tnf/handlers/deploymentsnodes/deploymentsnodes.go @@ -67,7 +67,7 @@ func (dn *DeploymentsNodes) Args() []string { return dn.args } -// GetIdentifier returns the tnf.Test specific identifiesa. +// GetIdentifier returns the tnf.Test specific identifier. func (dn *DeploymentsNodes) GetIdentifier() identifier.Identifier { return identifier.DeploymentsNodesIdentifier } diff --git a/pkg/tnf/handlers/doc.go b/pkg/tnf/handlers/doc.go index b4e197822..cdce13674 100644 --- a/pkg/tnf/handlers/doc.go +++ b/pkg/tnf/handlers/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/assertion/and.go b/pkg/tnf/handlers/generic/assertion/and.go index ad84e46c3..bd7e14d7b 100644 --- a/pkg/tnf/handlers/generic/assertion/and.go +++ b/pkg/tnf/handlers/generic/assertion/and.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/assertion/and_test.go b/pkg/tnf/handlers/generic/assertion/and_test.go index 056c8dcd5..65f5a99ea 100644 --- a/pkg/tnf/handlers/generic/assertion/and_test.go +++ b/pkg/tnf/handlers/generic/assertion/and_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/assertion/assertion.go b/pkg/tnf/handlers/generic/assertion/assertion.go index e0f3fd6fe..237160314 100644 --- a/pkg/tnf/handlers/generic/assertion/assertion.go +++ b/pkg/tnf/handlers/generic/assertion/assertion.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -150,13 +150,9 @@ func (a *Assertion) UnmarshalJSON(b []byte) error { return err } - if err := a.unmarshalGroupIdxJSON(objMap); err != nil { + if err = a.unmarshalGroupIdxJSON(objMap); err != nil { return err } - if err := a.unmarshalConditionJSON(objMap); err != nil { - return err - } - - return nil + return a.unmarshalConditionJSON(objMap) } diff --git a/pkg/tnf/handlers/generic/assertion/assertions.go b/pkg/tnf/handlers/generic/assertion/assertions.go index 59549a4a2..749d19192 100644 --- a/pkg/tnf/handlers/generic/assertion/assertions.go +++ b/pkg/tnf/handlers/generic/assertion/assertions.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -52,7 +52,7 @@ func (a *Assertions) UnmarshalJSON(b []byte) error { return err } - // Unmarshall the assertions Array. + // Unmarshal the assertions Array. if err := a.unmarshalAssertionsJSON(objMap); err != nil { return err } diff --git a/pkg/tnf/handlers/generic/assertion/assertions_test.go b/pkg/tnf/handlers/generic/assertion/assertions_test.go index f1244ef5a..ef49a1579 100644 --- a/pkg/tnf/handlers/generic/assertion/assertions_test.go +++ b/pkg/tnf/handlers/generic/assertion/assertions_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,7 +18,7 @@ package assertion_test import ( "encoding/json" - "io/ioutil" + "os" "path" "regexp" "testing" @@ -131,7 +131,7 @@ func getTestFile(testName string) string { // TestAssertions_UnmarshalJSON also tests Assertion.UnmarshalJSON. func TestAssertions_UnmarshalJSON(t *testing.T) { for testName, testCase := range assertionsTestCases { - contents, err := ioutil.ReadFile(path.Join(getTestFile(testName))) + contents, err := os.ReadFile(path.Join(getTestFile(testName))) assert.Nil(t, err) assert.NotNil(t, contents) diff --git a/pkg/tnf/handlers/generic/assertion/doc.go b/pkg/tnf/handlers/generic/assertion/doc.go index e9869cccd..2fcc99b57 100644 --- a/pkg/tnf/handlers/generic/assertion/doc.go +++ b/pkg/tnf/handlers/generic/assertion/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/assertion/logic.go b/pkg/tnf/handlers/generic/assertion/logic.go index 7b75d1527..d83b6805b 100644 --- a/pkg/tnf/handlers/generic/assertion/logic.go +++ b/pkg/tnf/handlers/generic/assertion/logic.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/assertion/logic_test.go b/pkg/tnf/handlers/generic/assertion/logic_test.go index 258beb619..b52f85a15 100644 --- a/pkg/tnf/handlers/generic/assertion/logic_test.go +++ b/pkg/tnf/handlers/generic/assertion/logic_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/assertion/or.go b/pkg/tnf/handlers/generic/assertion/or.go index 94e2fd304..a17ee0fba 100644 --- a/pkg/tnf/handlers/generic/assertion/or.go +++ b/pkg/tnf/handlers/generic/assertion/or.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/assertion/or_test.go b/pkg/tnf/handlers/generic/assertion/or_test.go index 660958fec..ae7d0df8b 100644 --- a/pkg/tnf/handlers/generic/assertion/or_test.go +++ b/pkg/tnf/handlers/generic/assertion/or_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/condition/condition.go b/pkg/tnf/handlers/generic/condition/condition.go index 1348284a7..f8df8b020 100644 --- a/pkg/tnf/handlers/generic/condition/condition.go +++ b/pkg/tnf/handlers/generic/condition/condition.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/condition/doc.go b/pkg/tnf/handlers/generic/condition/doc.go index af855d3af..fc4ca8bd6 100644 --- a/pkg/tnf/handlers/generic/condition/doc.go +++ b/pkg/tnf/handlers/generic/condition/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/condition/intcondition/doc.go b/pkg/tnf/handlers/generic/condition/intcondition/doc.go index 2e18bbd94..b28174ef2 100644 --- a/pkg/tnf/handlers/generic/condition/intcondition/doc.go +++ b/pkg/tnf/handlers/generic/condition/intcondition/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/condition/intcondition/int.go b/pkg/tnf/handlers/generic/condition/intcondition/int.go index 3101267c3..c0a195907 100644 --- a/pkg/tnf/handlers/generic/condition/intcondition/int.go +++ b/pkg/tnf/handlers/generic/condition/intcondition/int.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/condition/intcondition/int_test.go b/pkg/tnf/handlers/generic/condition/intcondition/int_test.go index 98b1465b0..162460c2c 100644 --- a/pkg/tnf/handlers/generic/condition/intcondition/int_test.go +++ b/pkg/tnf/handlers/generic/condition/intcondition/int_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/condition/stringcondition/doc.go b/pkg/tnf/handlers/generic/condition/stringcondition/doc.go index e7437550d..c198019d4 100644 --- a/pkg/tnf/handlers/generic/condition/stringcondition/doc.go +++ b/pkg/tnf/handlers/generic/condition/stringcondition/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/condition/stringcondition/string.go b/pkg/tnf/handlers/generic/condition/stringcondition/string.go index 31f17e4f6..e821cbcb7 100644 --- a/pkg/tnf/handlers/generic/condition/stringcondition/string.go +++ b/pkg/tnf/handlers/generic/condition/stringcondition/string.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/condition/stringcondition/string_test.go b/pkg/tnf/handlers/generic/condition/stringcondition/string_test.go index 95bdfef9f..de1541a52 100644 --- a/pkg/tnf/handlers/generic/condition/stringcondition/string_test.go +++ b/pkg/tnf/handlers/generic/condition/stringcondition/string_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/doc.go b/pkg/tnf/handlers/generic/doc.go index 5ea9eecd5..350d8509f 100644 --- a/pkg/tnf/handlers/generic/doc.go +++ b/pkg/tnf/handlers/generic/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/generic.go b/pkg/tnf/handlers/generic/generic.go index 75d6ffdaa..eceb59907 100644 --- a/pkg/tnf/handlers/generic/generic.go +++ b/pkg/tnf/handlers/generic/generic.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -19,7 +19,7 @@ package generic import ( "bytes" "encoding/json" - "io/ioutil" + "os" "regexp" "text/template" "time" @@ -195,7 +195,7 @@ func (g *Generic) ReelEOF() { // NewGenericFromJSONFile instantiates and initializes a Generic from a JSON-serialized file. func NewGenericFromJSONFile(filename, schemaPath string) (*tnf.Tester, []reel.Handler, *gojsonschema.Result, error) { - inputBytes, err := ioutil.ReadFile(filename) + inputBytes, err := os.ReadFile(filename) if err != nil { return nil, nil, nil, err } @@ -220,7 +220,7 @@ func newGenericFromJSON(inputBytes []byte, schemaPath string) (*tnf.Tester, []re // suites. If the supplied template/values do not conform to the generic-test.schema.json schema, creation fails and // the result is returned to the caller for further inspection. func NewGenericFromTemplate(templateFile, schemaPath, valuesFile string) (*tnf.Tester, []reel.Handler, *gojsonschema.Result, error) { - tplBytes, err := ioutil.ReadFile(valuesFile) + tplBytes, err := os.ReadFile(valuesFile) if err != nil { return nil, nil, nil, err } @@ -238,7 +238,7 @@ func NewGenericFromTemplate(templateFile, schemaPath, valuesFile string) (*tnf.T // suites. If the supplied values do not conform to the generic-test.schema.json schema, creation fails and the result // is returned to the caller for further inspection. func NewGenericFromMap(templateFile, schemaPath string, values map[string]interface{}) (*tnf.Tester, []reel.Handler, *gojsonschema.Result, error) { - templateBytes, err := ioutil.ReadFile(templateFile) + templateBytes, err := os.ReadFile(templateFile) if err != nil { return nil, nil, nil, err } diff --git a/pkg/tnf/handlers/generic/generic_test.go b/pkg/tnf/handlers/generic/generic_test.go index 417ff5ab9..a163ff498 100644 --- a/pkg/tnf/handlers/generic/generic_test.go +++ b/pkg/tnf/handlers/generic/generic_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -225,8 +225,8 @@ func TestGeneric(t *testing.T) { // this assertion also prevents `tester` from being `nil` inside the following `if` assert.Equal(t, testCase.expectedCreationErr, err != nil) if !testCase.expectedCreationErr { - assert.Equal(t, testCase.expectedTester, tester != nil) //nolint:staticcheck - assert.Equal(t, testCase.expectedTimeout, (*tester).Timeout()) //nolint:staticcheck + assert.Equal(t, testCase.expectedTester, tester != nil) + assert.Equal(t, testCase.expectedTimeout, (*tester).Timeout()) assert.Equal(t, testCase.expectedHandlers, handlers != nil) if testCase.expectedHandlers { diff --git a/pkg/tnf/handlers/generic/match.go b/pkg/tnf/handlers/generic/match.go index 6f418a5fa..58e97196c 100644 --- a/pkg/tnf/handlers/generic/match.go +++ b/pkg/tnf/handlers/generic/match.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/resultcontext.go b/pkg/tnf/handlers/generic/resultcontext.go index 6a364ce28..33b84330a 100644 --- a/pkg/tnf/handlers/generic/resultcontext.go +++ b/pkg/tnf/handlers/generic/resultcontext.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/generic/resultcontext_test.go b/pkg/tnf/handlers/generic/resultcontext_test.go index 67029747c..8e7169a86 100644 --- a/pkg/tnf/handlers/generic/resultcontext_test.go +++ b/pkg/tnf/handlers/generic/resultcontext_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,7 +18,7 @@ package generic_test import ( "encoding/json" - "io/ioutil" + "os" "path" "testing" @@ -90,7 +90,7 @@ func TestResultContext_MarshalJSON(t *testing.T) { actualContents, err := json.MarshalIndent(resultContext, "", " ") assert.Nil(t, err) // Compare against an expected rendering which has been pre-verified. - expectedContents, err := ioutil.ReadFile(testFileName) + expectedContents, err := os.ReadFile(testFileName) assert.Nil(t, err) assert.Equal(t, string(expectedContents), string(actualContents)) } diff --git a/pkg/tnf/handlers/generic/testdata/bad_yaml.yaml b/pkg/tnf/handlers/generic/testdata/bad_yaml.yaml index 45097746d..7225d2e1a 100644 --- a/pkg/tnf/handlers/generic/testdata/bad_yaml.yaml +++ b/pkg/tnf/handlers/generic/testdata/bad_yaml.yaml @@ -1 +1 @@ -Not a Key/Value Pair!!! \ No newline at end of file +Not a Key/Value Pair!!! diff --git a/pkg/tnf/handlers/graceperiod/graceperiod.go b/pkg/tnf/handlers/graceperiod/graceperiod.go deleted file mode 100644 index e3e543fac..000000000 --- a/pkg/tnf/handlers/graceperiod/graceperiod.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package graceperiod - -import ( - "regexp" - "strconv" - "time" - - "github.com/test-network-function/test-network-function/pkg/tnf" - "github.com/test-network-function/test-network-function/pkg/tnf/identifier" - "github.com/test-network-function/test-network-function/pkg/tnf/reel" -) - -const ( - gpRegex = "(?s).+" -) - -// GracePeriod holds information from extracting terminationGracePeriod from a Pod definition. -type GracePeriod struct { - gracePeriod int // Output variable for retrieving the result - result int - timeout time.Duration - args []string -} - -// NewGracePeriod creates a new GracePeriod tnf.Test. -func NewGracePeriod(timeout time.Duration, podName, podNamespace string) *GracePeriod { - return &GracePeriod{ - timeout: timeout, - result: tnf.ERROR, - args: []string{"oc", "-n", podNamespace, "get", "pod", podName, "-o", "jsonpath=\"{.spec.terminationGracePeriodSeconds}\""}, - } -} - -// Args returns the command line args for the test. -func (gp *GracePeriod) Args() []string { - return gp.args -} - -// GetIdentifier returns the tnf.Test specific identifier. -func (gp *GracePeriod) GetIdentifier() identifier.Identifier { - return identifier.GracePeriodIdentifier -} - -// Timeout returns the timeout in seconds for the test. -func (gp *GracePeriod) Timeout() time.Duration { - return gp.timeout -} - -// Result returns the test result. -func (gp *GracePeriod) Result() int { - return gp.result -} - -// ReelFirst returns a step which expects the pod's grace period within the test timeout. -func (gp *GracePeriod) ReelFirst() *reel.Step { - return &reel.Step{ - Expect: []string{gpRegex}, - Timeout: gp.timeout, - } -} - -// ReelMatch ensures that the terminationGracePeriod exist, and stores the correct grace period within -// the GracePeriod struct for later retrieval. -func (gp *GracePeriod) ReelMatch(_, _, match string) *reel.Step { - re := regexp.MustCompile(gpRegex) - matched := re.FindStringSubmatch(match) - if matched != nil { - if len(matched) != 1 { - gp.result = tnf.FAILURE - return nil - } - gracePeriod, err := strconv.Atoi(matched[0]) - if err != nil { - gp.result = tnf.FAILURE - return nil - } - gp.result = tnf.SUCCESS - gp.gracePeriod = gracePeriod - } else { - gp.result = tnf.FAILURE - } - return nil -} - -// ReelTimeout does nothing; no action is needed upon timeout. -func (gp *GracePeriod) ReelTimeout() *reel.Step { - return nil -} - -// ReelEOF does nothing; no aciton is needed upon EOF. -func (gp *GracePeriod) ReelEOF() { -} - -// GetGracePeriod extracts the terminationGracePeriod from a Pod. -func (gp *GracePeriod) GetGracePeriod() int { - return gp.gracePeriod -} diff --git a/cmd/claim/doc.go b/pkg/tnf/handlers/handler_template/doc.tmpl similarity index 82% rename from cmd/claim/doc.go rename to pkg/tnf/handlers/handler_template/doc.tmpl index b050892a2..f48241f5b 100644 --- a/cmd/claim/doc.go +++ b/pkg/tnf/handlers/handler_template/doc.tmpl @@ -1,5 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. -// +// Copyright (C) 2020-2022 Red Hat, Inc. // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or @@ -14,5 +13,5 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// Package main provides a CLI driven tool to append test suite result to existing claim file. -package main +// Package {{ .LowerHandlername }} provides a test for {{ .LowerHandlername }}. +package {{ .LowerHandlername }} diff --git a/pkg/tnf/handlers/handler_template/handler.tmpl b/pkg/tnf/handlers/handler_template/handler.tmpl new file mode 100644 index 000000000..dd933d25e --- /dev/null +++ b/pkg/tnf/handlers/handler_template/handler.tmpl @@ -0,0 +1,94 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package {{ .LowerHandlername }} + +import ( + "time" + + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/identifier" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" +) + +// {{ .UpperHandlername }} is the reel handler struct. +type {{ .UpperHandlername }} struct { + result int + timeout time.Duration + args []string + // adding special parameters +} + +const ( +// adding special variables +) + +// New{{ .UpperHandlername }} returns a new {{ .UpperHandlername }} handler struct. +// TODO: Add needed parameters to this function and initialize the handler properly. +func New{{ .UpperHandlername }}(timeout time.Duration) *{{ .UpperHandlername }} { + return &{{ .UpperHandlername }}{ + timeout: timeout, + result: tnf.ERROR, + args: []string{}, // TODO: Add proper execution command. + } +} + +// Args returns the initial execution/send command strings for handler {{ .UpperHandlername }}. +func (h *{{ .UpperHandlername }}) Args() []string { + return h.args +} + +// GetIdentifier returns the tnf.Test specific identifier. +func (h *{{ .UpperHandlername }}) GetIdentifier() identifier.Identifier { + // Return the {{ .UpperHandlername }} handler identifier. + return identifier.Identifier{} +} + +// Timeout returns the timeout for the test. +func (h *{{ .UpperHandlername }}) Timeout() time.Duration { + return h.timeout +} + +// Result returns the test result. +func (h *{{ .UpperHandlername }}) Result() int { + return h.result +} + +// ReelFirst returns a reel step for handler {{ .UpperHandlername }}. +func (h *{{ .UpperHandlername }}) ReelFirst() *reel.Step { + return &reel.Step{ + Expect: []string{}, // TODO : pass the list of possible regex in here + Timeout: h.timeout, + } +} + +// ReelMatch parses the {{ .UpperHandlername }} output and set the test result on match. +func (h *{{ .UpperHandlername }}) ReelMatch(_, _, match string) *reel.Step { + // TODO : add the matching logic here and return an appropriate tnf result. + h.result = tnf.ERROR + return nil +} + +// ReelTimeout function for {{ .UpperHandlername }} will be called by the reel FSM when a expect timeout occurs. +func (h *{{ .UpperHandlername }}) ReelTimeout() *reel.Step { + // TODO : Add code here in case a timeout reaction is needed. + return nil +} + +// ReelEOF function for {{ .UpperHandlername }} will be called by the reel FSM when a EOF is read. +func (h *{{ .UpperHandlername }}) ReelEOF() { + // TODO : Add code here in case a EOF reaction is needed. +} diff --git a/pkg/tnf/handlers/handler_template/handler_test.tmpl b/pkg/tnf/handlers/handler_template/handler_test.tmpl new file mode 100644 index 000000000..1f8be2120 --- /dev/null +++ b/pkg/tnf/handlers/handler_template/handler_test.tmpl @@ -0,0 +1,60 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package {{ .LowerHandlername }} + +import ( + "testing" +) + +const ( +// adding special variable +) + +// Test_New{{ .UpperHandlername }} is the unit test for New{{ .UpperHandlername }}(). +func Test_New{{ .UpperHandlername }}(t *testing.T) { + // Todo: Write test. +} + +// Test_{{ .UpperHandlername }}_Args is the unit test for {{ .UpperHandlername }}_Args(). +func Test{{ .UpperHandlername }}_Args(t *testing.T) { + // Todo: Write test. +} + +// Test_{{ .UpperHandlername }}_GetIdentifier is the unit test for {{ .UpperHandlername }}_GetIdentifier(). +func Test{{ .UpperHandlername }}_GetIdentifier(t *testing.T) { + // Todo: Write test. +} + +// Test_{{ .UpperHandlername }}_ReelFirst is the unit test for {{ .UpperHandlername }}_ReelFirst(). +func Test{{ .UpperHandlername }}_ReelFirst(t *testing.T) { + // Todo: Write test. +} + +// Test_{{ .UpperHandlername }}_ReelEOF is the unit test for {{ .UpperHandlername }}_ReelEOF(). +func Test{{ .UpperHandlername }}_ReelEOF(t *testing.T) { + // Todo: Write test. +} + +// Test_{{ .UpperHandlername }}_ReelTimeout is the unit test for {{ .UpperHandlername }}}_ReelTimeout(). +func Test{{ .UpperHandlername }}_ReelTimeout(t *testing.T) { + // Todo: Write test. +} + +// Test_{{ .UpperHandlername }}_ReelMatch is the unit test for {{ .UpperHandlername }}_ReelMatch(). +func Test{{ .UpperHandlername }}_ReelMatch(t *testing.T) { + // Todo: Write test. +} diff --git a/pkg/tnf/handlers/hostname/doc.go b/pkg/tnf/handlers/hostname/doc.go deleted file mode 100644 index d67766d76..000000000 --- a/pkg/tnf/handlers/hostname/doc.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -// Package hostname provides a hostname discovery test utilizing the `hostname` Unix command. -package hostname diff --git a/pkg/tnf/handlers/hostname/hostname.go b/pkg/tnf/handlers/hostname/hostname.go deleted file mode 100644 index 96aa14e0e..000000000 --- a/pkg/tnf/handlers/hostname/hostname.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package hostname - -import ( - "time" - - "github.com/test-network-function/test-network-function/pkg/tnf" - "github.com/test-network-function/test-network-function/pkg/tnf/dependencies" - "github.com/test-network-function/test-network-function/pkg/tnf/identifier" - "github.com/test-network-function/test-network-function/pkg/tnf/reel" -) - -// Hostname provides a hostname test implemented using command line tool "hostname". -type Hostname struct { - result int - timeout time.Duration - args []string - // The hostname - hostname string -} - -const ( - // Command is the command name for the unix "hostname" command. - Command = dependencies.HostnameBinaryName - // SuccessfulOutputRegex is the regular expression match for hostname output. - SuccessfulOutputRegex = `.+` -) - -// Args returns the command line args for the test. -func (h *Hostname) Args() []string { - return h.args -} - -// GetIdentifier returns the tnf.Test specific identifier. -func (h *Hostname) GetIdentifier() identifier.Identifier { - return identifier.HostnameIdentifier -} - -// Timeout return the timeout for the test. -func (h *Hostname) Timeout() time.Duration { - return h.timeout -} - -// Result returns the test result. -func (h *Hostname) Result() int { - return h.result -} - -// ReelFirst returns a step which expects an hostname summary for the given device. -func (h *Hostname) ReelFirst() *reel.Step { - return &reel.Step{ - Expect: []string{SuccessfulOutputRegex}, - Timeout: h.timeout, - } -} - -// ReelMatch parses the hostname output and set the test result on match. -// Returns no step; the test is complete. -func (h *Hostname) ReelMatch(_, _, match string) *reel.Step { - h.hostname = match - h.result = tnf.SUCCESS - return nil -} - -// ReelTimeout does nothing; hostname requires no explicit intervention for a timeout. -func (h *Hostname) ReelTimeout() *reel.Step { - return nil -} - -// ReelEOF does nothing; hostname requires no explicit intervention for EOF. -func (h *Hostname) ReelEOF() { -} - -// GetHostname returns the extracted hostname, if one is extracted. -func (h *Hostname) GetHostname() string { - return h.hostname -} - -// NewHostname creates a new `Hostname` test which runs the "hostname" command. -func NewHostname(timeout time.Duration) *Hostname { - return &Hostname{ - result: tnf.ERROR, - timeout: timeout, - args: []string{Command}, - } -} diff --git a/pkg/tnf/handlers/hostname/hostname_test.go b/pkg/tnf/handlers/hostname/hostname_test.go deleted file mode 100644 index 50defb887..000000000 --- a/pkg/tnf/handlers/hostname/hostname_test.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package hostname_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/test-network-function/test-network-function/pkg/tnf" - "github.com/test-network-function/test-network-function/pkg/tnf/handlers/hostname" - "github.com/test-network-function/test-network-function/pkg/tnf/identifier" -) - -const ( - testTimeoutDuration = time.Second * 2 -) - -func TestHostname_Args(t *testing.T) { - h := hostname.NewHostname(testTimeoutDuration) - assert.Equal(t, []string{"hostname"}, h.Args()) -} - -func TestHostname_GetIdentifier(t *testing.T) { - h := hostname.NewHostname(testTimeoutDuration) - assert.Equal(t, identifier.HostnameIdentifier, h.GetIdentifier()) -} - -func TestHostname_ReelFirst(t *testing.T) { - h := hostname.NewHostname(testTimeoutDuration) - step := h.ReelFirst() - assert.Equal(t, "", step.Execute) - assert.Equal(t, []string{hostname.SuccessfulOutputRegex}, step.Expect) - assert.Equal(t, testTimeoutDuration, step.Timeout) -} - -func TestHostname_ReelEof(t *testing.T) { - h := hostname.NewHostname(testTimeoutDuration) - // just ensures lack of panic - h.ReelEOF() -} - -func TestHostname_ReelTimeout(t *testing.T) { - h := hostname.NewHostname(testTimeoutDuration) - step := h.ReelTimeout() - assert.Nil(t, step) -} - -// Also tests GetHostname() and Result() -func TestHostname_ReelMatch(t *testing.T) { - h := hostname.NewHostname(testTimeoutDuration) - matchHostname := "testHostname" - step := h.ReelMatch("", "", matchHostname) - assert.Nil(t, step) - assert.Equal(t, matchHostname, h.GetHostname()) - assert.Equal(t, tnf.SUCCESS, h.Result()) -} - -func TestNewHostname(t *testing.T) { - h := hostname.NewHostname(testTimeoutDuration) - assert.Equal(t, tnf.ERROR, h.Result()) - assert.Equal(t, testTimeoutDuration, h.Timeout()) -} diff --git a/pkg/tnf/handlers/hugepages/hugepages.go b/pkg/tnf/handlers/hugepages/hugepages.go deleted file mode 100644 index c905f8cca..000000000 --- a/pkg/tnf/handlers/hugepages/hugepages.go +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package hugepages - -import ( - "strconv" - "strings" - "time" - - "github.com/test-network-function/test-network-function/pkg/tnf" - "github.com/test-network-function/test-network-function/pkg/tnf/identifier" - "github.com/test-network-function/test-network-function/pkg/tnf/reel" -) - -const ( - hpRegex = "(?s).+" - keyValueNumElements = 2 - // RhelDefaultHugepages const - RhelDefaultHugepages = 0 - // RhelDefaultHugepagesz const - RhelDefaultHugepagesz = 2048 // kB -) - -// Hugepages holds information derived from running "oc get MachineConfig" on the command line. -type Hugepages struct { - hugepagesz int - hugepages int - result int - timeout time.Duration - args []string -} - -// NewHugepages creates a new Hugepages tnf.Test. -func NewHugepages(timeout time.Duration) *Hugepages { - return &Hugepages{ - timeout: timeout, - result: tnf.ERROR, - args: []string{ - "oc", "get", "machineconfigs", "-l", "machineconfiguration.openshift.io/role=worker", - "-o", "custom-columns=KARGS:.spec.kernelArguments", - "|", "grep", "-v", "nil", "|", "grep", "-E", "'hugepage|KARGS'", - }, - } -} - -// Args returns the command line args for the test. -func (hp *Hugepages) Args() []string { - return hp.args -} - -// GetIdentifier returns the tnf.Test specific identifiesa. -func (hp *Hugepages) GetIdentifier() identifier.Identifier { - return identifier.HugepagesIdentifier -} - -// Timeout returns the timeout in seconds for the test. -func (hp *Hugepages) Timeout() time.Duration { - return hp.timeout -} - -// Result returns the test result. -func (hp *Hugepages) Result() int { - return hp.result -} - -// ReelFirst returns a step which expects the ping statistics within the test timeout. -func (hp *Hugepages) ReelFirst() *reel.Step { - return &reel.Step{ - Expect: []string{hpRegex}, - Timeout: hp.timeout, - } -} - -// ReelMatch sets the hugepages parameters based on cluster configuration and RHEL defaults -func (hp *Hugepages) ReelMatch(_, _, match string) *reel.Step { - trimmedMatch := strings.Trim(match, "\n") - lines := strings.Split(trimmedMatch, "\n")[1:] // First line is the headers/titles line - - params := map[string]string{} - - // Each line is of the form [name=value name=value ...] - // Find the relevant parameters and store in params - for _, line := range lines { - line = line[1 : len(line)-1] // trim '[' and ']' - fields := strings.Fields(line) - for _, field := range fields { - nameValue := strings.Split(field, "=") - // Some elements emitted may not be key/value pairs. - if len(nameValue) == keyValueNumElements { - name := nameValue[0] - value := nameValue[1] - if isHugepagesParam(name) { - params[name] = value - } - } - } - } - - // Use params for determining the hugepages settings - hugepages, ok := params["hugepages"] - if ok { - hp.hugepages, _ = strconv.Atoi(hugepages) - } else { - hp.hugepages = RhelDefaultHugepages - } - - hugepagesz, ok := params["hugepagesz"] - if ok { - hp.hugepagesz = atoi(hugepagesz) - } else { - hugepagesz, ok := params["default_hugepagesz"] - if ok { - hp.hugepagesz = atoi(hugepagesz) - } else { - hp.hugepagesz = RhelDefaultHugepagesz - } - } - - hp.result = tnf.SUCCESS - return nil -} - -// ReelTimeout does nothing; no action is necessary upon timeout. -func (hp *Hugepages) ReelTimeout() *reel.Step { - return nil -} - -// ReelEOF does nothing; no action is necessary upon EOF. -func (hp *Hugepages) ReelEOF() { -} - -// GetHugepages func -func (hp *Hugepages) GetHugepages() int { - return hp.hugepages -} - -// GetHugepagesz func -func (hp *Hugepages) GetHugepagesz() int { - return hp.hugepagesz -} - -func isHugepagesParam(param string) bool { - const ( - hugepagesz = "hugepagesz" - defaultHugepagesz = "default_hugepagesz" - hugepages = "hugepages" - ) - return param == hugepagesz || param == defaultHugepagesz || param == hugepages -} - -// atoi takes string in the format size[KMG] and returns an int in KB units -func atoi(s string) int { - num, _ := strconv.Atoi(s[:len(s)-1]) - unit := s[len(s)-1] - switch unit { - case 'M': - num *= 1024 - case 'G': - num *= 1024 * 1024 - } - - return num -} diff --git a/pkg/tnf/handlers/hugepages/hugepages_test.go b/pkg/tnf/handlers/hugepages/hugepages_test.go deleted file mode 100644 index 01ef73a44..000000000 --- a/pkg/tnf/handlers/hugepages/hugepages_test.go +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package hugepages_test - -import ( - "regexp" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/test-network-function/test-network-function/pkg/tnf" - hp "github.com/test-network-function/test-network-function/pkg/tnf/handlers/hugepages" -) - -func Test_NewHugepages(t *testing.T) { - newHp := hp.NewHugepages(testTimeoutDuration) - assert.NotNil(t, newHp) - assert.Equal(t, testTimeoutDuration, newHp.Timeout()) - assert.Equal(t, newHp.Result(), tnf.ERROR) -} - -func Test_ReelFirstPositive(t *testing.T) { - newHp := hp.NewHugepages(testTimeoutDuration) - assert.NotNil(t, newHp) - firstStep := newHp.ReelFirst() - re := regexp.MustCompile(firstStep.Expect[0]) - matches := re.FindStringSubmatch(testInputSuccess) - assert.Len(t, matches, 1) - assert.Equal(t, testInputSuccess, matches[0]) -} - -func Test_ReelFirstPositiveEmpty(t *testing.T) { - newHp := hp.NewHugepages(testTimeoutDuration) - assert.NotNil(t, newHp) - firstStep := newHp.ReelFirst() - re := regexp.MustCompile(firstStep.Expect[0]) - matches := re.FindStringSubmatch(testInputEmpty) - assert.Len(t, matches, 1) - assert.Equal(t, testInputEmpty, matches[0]) -} - -func Test_ReelFirstNegative(t *testing.T) { - newHp := hp.NewHugepages(testTimeoutDuration) - assert.NotNil(t, newHp) - firstStep := newHp.ReelFirst() - re := regexp.MustCompile(firstStep.Expect[0]) - matches := re.FindStringSubmatch(testInputError) - assert.Len(t, matches, 0) -} - -func Test_ReelMatchSuccessEmpty(t *testing.T) { - newHp := hp.NewHugepages(testTimeoutDuration) - assert.NotNil(t, newHp) - step := newHp.ReelMatch("", "", testInputEmpty) - assert.Nil(t, step) - assert.Equal(t, tnf.SUCCESS, newHp.Result()) - assert.Equal(t, hp.RhelDefaultHugepages, newHp.GetHugepages()) - assert.Equal(t, hp.RhelDefaultHugepagesz, newHp.GetHugepagesz()) -} - -func Test_ReelMatchSuccess(t *testing.T) { - newHp := hp.NewHugepages(testTimeoutDuration) - assert.NotNil(t, newHp) - step := newHp.ReelMatch("", "", testInputSuccess) - assert.Nil(t, step) - assert.Equal(t, tnf.SUCCESS, newHp.Result()) - assert.Equal(t, testExpectedHugepages, newHp.GetHugepages()) - assert.Equal(t, testExpectedHugepagesz, newHp.GetHugepagesz()) -} - -// Just ensure there are no panics. -func Test_ReelEof(t *testing.T) { - newHp := hp.NewHugepages(testTimeoutDuration) - assert.NotNil(t, newHp) - newHp.ReelEOF() -} - -const ( - testTimeoutDuration = time.Second * 2 - testInputError = "" - testInputEmpty = "KARGS\n" - testInputSuccess = "KARGS\n[skew_tick=1 nohz=on rcu_nocbs=2-19,22-39,42-59,62-79 tuned.non_isolcpus=30000300,00300003 intel_pstate=disable nosoftlockup tsc=nowatchdog intel_iommu=on iommu=pt isolcpus=managed_irq,2-19,22-39,42-59,62-79 systemd.cpu_affinity=0,1,40,41,20,21,60,61 hugepages=32 default_hugepagesz=32M hugepages=64 hugepagesz=1G nmi_watchdog=0 audit=0 mce=off processor.max_cstate=1 idle=poll intel_idle.max_cstate=0]\n" - testExpectedHugepages = 64 - testExpectedHugepagesz = 1024 * 1024 -) diff --git a/pkg/tnf/handlers/imagepullpolicy/doc.go b/pkg/tnf/handlers/imagepullpolicy/doc.go new file mode 100644 index 000000000..7ea02d536 --- /dev/null +++ b/pkg/tnf/handlers/imagepullpolicy/doc.go @@ -0,0 +1,17 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +// Package imagepullpolicy provides a test for imagepullpolicy. +package imagepullpolicy diff --git a/pkg/tnf/handlers/imagepullpolicy/imagepullpolicy.json b/pkg/tnf/handlers/imagepullpolicy/imagepullpolicy.json new file mode 100644 index 000000000..2f8c9c56d --- /dev/null +++ b/pkg/tnf/handlers/imagepullpolicy/imagepullpolicy.json @@ -0,0 +1,32 @@ +{ + "identifier" : { + "url" : "http://test-network-function.com/tests/imagepullpolicy", + "version": "v1.0.0" + }, + "description": "A generic test used to get Image Pull Policy type", + "testResult": 0, + "testTimeout": 5000000000, + "reelFirstStep": { + "execute": "oc get pod {{.POD_NAME}} -n {{.POD_NAMESPACE}} -o json | jq -r '.spec.containers[{{.CONTAINER_NUM}}].imagePullPolicy'", + "expect":["(?m)IfNotPresent", + "(?m)Always", + "(?m)Never", + "(?)"], + "timeout": 5000000000 + }, + "resultContexts":[ + { + "pattern": "(?m)IfNotPresent", + "defaultResult": 1 + }, + { + "pattern": "(?m)Always", + "defaultResult": 2 + }, + { + "pattern": "(?m)Never", + "defaultResult": 2 + } + ] + } + \ No newline at end of file diff --git a/pkg/tnf/handlers/imagepullpolicy/imagepullpolicy_test.go b/pkg/tnf/handlers/imagepullpolicy/imagepullpolicy_test.go new file mode 100644 index 000000000..fe83f3203 --- /dev/null +++ b/pkg/tnf/handlers/imagepullpolicy/imagepullpolicy_test.go @@ -0,0 +1,138 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package imagepullpolicy_test + +import ( + "fmt" + "path" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/generic" + "github.com/test-network-function/test-network-function/pkg/tnf/identifier" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" + "github.com/xeipuuv/gojsonschema" +) + +const ( + testTimeoutDuration = time.Second * 5 +) + +var ( + genericTestSchemaFile = path.Join("schemas", "generic-test.schema.json") + imagepullFilename = "imagepullpolicy.json" + /* #nosec G101 */ + expectedPassPattern = "(?m)IfNotPresent" + expectedFailPattern = "(?m)Always" + pathRelativeToRoot = path.Join("..", "..", "..", "..") + pathToTestSchemaFile = path.Join(pathRelativeToRoot, genericTestSchemaFile) + testPodNameSpace = "testnamespace" + testPodName = "testPodname" + testContainerNum = 0 + testInputSuccess = "IfNotPresent" + testInputFilure = "Always" +) + +func createTest() (*tnf.Tester, []reel.Handler, *gojsonschema.Result, error) { + values := make(map[string]interface{}) + values["POD_NAMESPACE"] = testPodNameSpace + values["POD_NAME"] = testPodName + values["CONTAINER_NUM"] = testContainerNum + return generic.NewGenericFromMap(imagepullFilename, pathToTestSchemaFile, values) +} + +func TestImagePullPolicy_Args(t *testing.T) { + test, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Nil(t, (*test).Args()) +} + +func TestImagePullPolicy_GetIdentifier(t *testing.T) { + test, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, identifier.ImagePullPolicyIdentifier, (*test).GetIdentifier()) +} + +func TestImagePullPolicy_ReelFirst(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + step := handler.ReelFirst() + expectedCommand := fmt.Sprintf("oc get pod %s -n %s -o json | jq -r '.spec.containers[%d].imagePullPolicy'", testPodName, testPodNameSpace, testContainerNum) + assert.Equal(t, expectedCommand, step.Execute) + assert.Contains(t, step.Expect, expectedPassPattern, expectedFailPattern) + assert.Equal(t, testTimeoutDuration, step.Timeout) +} + +func TestImagePullPolicy_ReelEof(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + // just ensure there isn't a panic + handler.ReelEOF() +} + +func TestImagePullPolicy_ReelTimeout(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + assert.Nil(t, handler.ReelTimeout()) +} + +func TestImagePullPolicy_ReelMatch(t *testing.T) { + tester, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + step := handler.ReelMatch(expectedPassPattern, "", testInputSuccess) + + assert.Nil(t, step) + assert.Equal(t, tnf.SUCCESS, (*tester).Result()) + step = handler.ReelMatch(expectedFailPattern, "", testInputFilure) + + assert.Nil(t, step) + assert.Equal(t, tnf.FAILURE, (*tester).Result()) +} diff --git a/pkg/tnf/handlers/ipaddr/doc.go b/pkg/tnf/handlers/ipaddr/doc.go index 4e62315ef..9cc21b309 100644 --- a/pkg/tnf/handlers/ipaddr/doc.go +++ b/pkg/tnf/handlers/ipaddr/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/ipaddr/ipaddr.go b/pkg/tnf/handlers/ipaddr/ipaddr.go index 7054671db..28041676f 100644 --- a/pkg/tnf/handlers/ipaddr/ipaddr.go +++ b/pkg/tnf/handlers/ipaddr/ipaddr.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -26,6 +26,7 @@ import ( "github.com/test-network-function/test-network-function/pkg/tnf/dependencies" "github.com/test-network-function/test-network-function/pkg/tnf/identifier" "github.com/test-network-function/test-network-function/pkg/tnf/reel" + "github.com/test-network-function/test-network-function/pkg/utils" ) // IPAddr provides an ip addr test implemented using command line tool `ip addr`. @@ -111,7 +112,16 @@ func ipAddrCmd(dev string) []string { return strings.Split(fmt.Sprintf("%s %s", ipAddrCommand, dev), " ") } +func ipAddrCmdNsenter(containerPID, dev string) []string { + return strings.Split(fmt.Sprintf("%s%s %s", utils.AddNsenterPrefix(containerPID), ipAddrCommand, dev), " ") +} + // NewIPAddr creates a new `ip addr` test for the given device. func NewIPAddr(timeout time.Duration, device string) *IPAddr { return &IPAddr{result: tnf.ERROR, timeout: timeout, args: ipAddrCmd(device)} } + +// NewIPAddr creates a new `ip addr` test for the given device. +func NewIPAddrNsenter(timeout time.Duration, containerPID, device string) *IPAddr { + return &IPAddr{result: tnf.ERROR, timeout: timeout, args: ipAddrCmdNsenter(containerPID, device)} +} diff --git a/pkg/tnf/handlers/ipaddr/ipaddr_test.go b/pkg/tnf/handlers/ipaddr/ipaddr_test.go index c7478b288..a934703ba 100644 --- a/pkg/tnf/handlers/ipaddr/ipaddr_test.go +++ b/pkg/tnf/handlers/ipaddr/ipaddr_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,7 +18,7 @@ package ipaddr_test import ( "fmt" - "io/ioutil" + "os" "path" "testing" "time" @@ -63,7 +63,7 @@ func getMockOutputFilename(testName string) string { func getMockOutput(t *testing.T, testName string) string { fileName := getMockOutputFilename(testName) - b, err := ioutil.ReadFile(fileName) + b, err := os.ReadFile(fileName) assert.Nil(t, err) return string(b) } diff --git a/pkg/tnf/handlers/liveness/doc.go b/pkg/tnf/handlers/liveness/doc.go new file mode 100644 index 000000000..7d0ad399d --- /dev/null +++ b/pkg/tnf/handlers/liveness/doc.go @@ -0,0 +1,17 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +// Package liveness provides a test for liveness. +package liveness diff --git a/pkg/tnf/handlers/liveness/liveness.gotemplate b/pkg/tnf/handlers/liveness/liveness.gotemplate new file mode 100644 index 000000000..cbde15fce --- /dev/null +++ b/pkg/tnf/handlers/liveness/liveness.gotemplate @@ -0,0 +1,3 @@ +{{- range .spec.containers -}} + {{ if .livenessProbe }} {{"liveness-defined\n"}} {{- else -}} {{"liveness-not-defined\n"}}{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/pkg/tnf/handlers/liveness/liveness.json b/pkg/tnf/handlers/liveness/liveness.json new file mode 100644 index 000000000..98b5ad7ee --- /dev/null +++ b/pkg/tnf/handlers/liveness/liveness.json @@ -0,0 +1,26 @@ +{ + "identifier" : { + "url" : "http://test-network-function.com/tests/liveness", + "version": "v1.0.0" + }, + "description": "Test check if liveness is defined.", + "testResult": 0, + "testTimeout": 5000000000, + "reelFirstStep": { + "execute": "oc get pod -n {{.POD_NAMESPACE}} {{.POD_NAME}} -o go-template-file={{.GO_TEMPLATE_PATH}}/liveness.gotemplate", + "expect":[ "(?m)liveness-not-defined", + "(?m)liveness-defined"], + "timeout": 5000000000 + }, + "resultContexts":[ + { + "pattern": "(?m)liveness-not-defined", + "defaultResult": 2 + }, + { + "pattern": "(?m)liveness-defined", + "defaultResult": 1 + } + ] + } + \ No newline at end of file diff --git a/pkg/tnf/handlers/liveness/liveness_test.go b/pkg/tnf/handlers/liveness/liveness_test.go new file mode 100644 index 000000000..d716d2c9d --- /dev/null +++ b/pkg/tnf/handlers/liveness/liveness_test.go @@ -0,0 +1,137 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package liveness + +import ( + "fmt" + "path" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/generic" + "github.com/test-network-function/test-network-function/pkg/tnf/identifier" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" + "github.com/xeipuuv/gojsonschema" +) + +const ( + testTimeoutDuration = time.Second * 5 +) + +var ( + genericTestSchemaFile = path.Join("schemas", "generic-test.schema.json") + livenessFilename = "liveness.json" + /* #nosec G101 */ + expectedPassPattern = "(?m)liveness-defined" + expectedFailPattern = "(?m)liveness-not-defined" + pathRelativeToRoot = path.Join("..", "..", "..", "..") + pathToTestSchemaFile = path.Join(pathRelativeToRoot, genericTestSchemaFile) + testPodNameSpace = "testnamespace" + testPodName = "testPodname" +) + +func createTest() (*tnf.Tester, []reel.Handler, *gojsonschema.Result, error) { + values := make(map[string]interface{}) + values["POD_NAMESPACE"] = testPodNameSpace + values["POD_NAME"] = testPodName + values["GO_TEMPLATE_PATH"] = "." + return generic.NewGenericFromMap(livenessFilename, pathToTestSchemaFile, values) +} + +func TestLiveness_Args(t *testing.T) { + test, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Nil(t, (*test).Args()) +} + +func TestLiveness_GetIdentifier(t *testing.T) { + test, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, identifier.LivenessURLIdentifier, (*test).GetIdentifier()) +} + +func TestLiveness_ReelFirst(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + step := handler.ReelFirst() + expectedCommand := fmt.Sprintf("oc get pod -n %s %s -o go-template-file=./liveness.gotemplate", + testPodNameSpace, testPodName) + assert.Equal(t, expectedCommand, step.Execute) + assert.Contains(t, step.Expect, expectedPassPattern, expectedFailPattern) + assert.Equal(t, testTimeoutDuration, step.Timeout) +} + +func TestLiveness_ReelEof(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + // just ensure there isn't a panic + handler.ReelEOF() +} + +func TestLiveness_ReelTimeout(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + assert.Nil(t, handler.ReelTimeout()) +} + +func TestLiveness_ReelMatch(t *testing.T) { + tester, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + step := handler.ReelMatch(expectedFailPattern, "", "liveness-not-defined") + + assert.Nil(t, step) + assert.Equal(t, tnf.FAILURE, (*tester).Result()) + + step = handler.ReelMatch(expectedPassPattern, "", "liveness-defined") + + assert.Nil(t, step) + assert.Equal(t, tnf.SUCCESS, (*tester).Result()) +} diff --git a/pkg/tnf/handlers/node/nodes_test.go b/pkg/tnf/handlers/node/nodes_test.go index 1ed672472..64e5f7efe 100644 --- a/pkg/tnf/handlers/node/nodes_test.go +++ b/pkg/tnf/handlers/node/nodes_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/nodedebug/nodedebug.go b/pkg/tnf/handlers/nodedebug/nodedebug.go index b79f68fbe..cf0e6271c 100644 --- a/pkg/tnf/handlers/nodedebug/nodedebug.go +++ b/pkg/tnf/handlers/nodedebug/nodedebug.go @@ -50,7 +50,7 @@ func NewNodeDebug(timeout time.Duration, nodeName, command string, trim, split b timeout: timeout, result: tnf.ERROR, args: []string{ - "echo", "-e", "\"chroot /host\n\"", command, "|", "oc", "debug", "node/" + nodeName, + command, }, Trim: trim, Split: split, diff --git a/pkg/tnf/handlers/nodehugepages/nodehugepages.go b/pkg/tnf/handlers/nodehugepages/nodehugepages.go deleted file mode 100644 index edfeb3e49..000000000 --- a/pkg/tnf/handlers/nodehugepages/nodehugepages.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package nodehugepages - -import ( - "strconv" - "strings" - "time" - - "github.com/test-network-function/test-network-function/pkg/tnf" - "github.com/test-network-function/test-network-function/pkg/tnf/identifier" - "github.com/test-network-function/test-network-function/pkg/tnf/reel" -) - -const ( - nhRegex = "(?s).+" -) - -// NodeHugepages test -type NodeHugepages struct { - hugepagesz int // The cluster's configuration - hugepages int // - result int - timeout time.Duration - args []string -} - -// NewNodeHugepages creates a new NodeHugepages tnf.Test. -func NewNodeHugepages(timeout time.Duration, node string, hugepagesz, hugepages int) *NodeHugepages { - return &NodeHugepages{ - hugepagesz: hugepagesz, - hugepages: hugepages, - timeout: timeout, - result: tnf.ERROR, - args: []string{ - "echo", "\"grep -E 'HugePages_Total:|Hugepagesize:' /proc/meminfo\"", "|", "oc", "debug", "node/" + node, - }, - } -} - -// Args returns the command line args for the test. -func (nh *NodeHugepages) Args() []string { - return nh.args -} - -// GetIdentifier returns the tnf.Test specific identifier. -func (nh *NodeHugepages) GetIdentifier() identifier.Identifier { - return identifier.NodeHugepagesIdentifier -} - -// Timeout returns the timeout in seconds for the test. -func (nh *NodeHugepages) Timeout() time.Duration { - return nh.timeout -} - -// Result returns the test result. -func (nh *NodeHugepages) Result() int { - return nh.result -} - -// ReelFirst returns a step which expects the output within the test timeout. -func (nh *NodeHugepages) ReelFirst() *reel.Step { - return &reel.Step{ - Expect: []string{nhRegex}, - Timeout: nh.timeout, - } -} - -// ReelMatch tests the node's hugepages configuration -func (nh *NodeHugepages) ReelMatch(_, _, match string) *reel.Step { - trimmedMatch := strings.Trim(match, "\n") - lines := strings.Split(trimmedMatch, "\n") - - const numExpectedLines = 2 - - if len(lines) != numExpectedLines { - return nil - } - - for _, line := range lines { - fields := strings.Fields(line) - name := fields[0][:len(fields[0])-1] - value := fields[1] - if !nh.validateParameter(name, value) { - nh.result = tnf.FAILURE - } - } - - if nh.result != tnf.FAILURE { - nh.result = tnf.SUCCESS - } - return nil -} - -// ReelTimeout does nothing; no action is necessary upon timeout. -func (nh *NodeHugepages) ReelTimeout() *reel.Step { - return nil -} - -// ReelEOF does nothing; no action is necessary upon EOF. -func (nh *NodeHugepages) ReelEOF() { -} - -func (nh *NodeHugepages) validateParameter(name, value string) bool { - const ( - hugePagesTotal = "HugePages_Total" - hugepagesize = "Hugepagesize" - ) - num, _ := strconv.Atoi(value) - switch name { - case hugePagesTotal: - return num == nh.hugepages - case hugepagesize: - return num == nh.hugepagesz - } - return false -} diff --git a/pkg/tnf/handlers/nodehugepages/nodehugepages_test.go b/pkg/tnf/handlers/nodehugepages/nodehugepages_test.go deleted file mode 100644 index 54b9df075..000000000 --- a/pkg/tnf/handlers/nodehugepages/nodehugepages_test.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package nodehugepages_test - -import ( - "regexp" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/test-network-function/test-network-function/pkg/tnf" - nh "github.com/test-network-function/test-network-function/pkg/tnf/handlers/nodehugepages" -) - -func Test_NewNodeHugepages(t *testing.T) { - newNh := nh.NewNodeHugepages(testTimeoutDuration, testNode, testExpectedHugepagesz, testExpectedHugepages) - assert.NotNil(t, newNh) - assert.Equal(t, testTimeoutDuration, newNh.Timeout()) - assert.Equal(t, newNh.Result(), tnf.ERROR) -} - -func Test_ReelFirstPositive(t *testing.T) { - newNh := nh.NewNodeHugepages(testTimeoutDuration, testNode, testExpectedHugepagesz, testExpectedHugepages) - assert.NotNil(t, newNh) - firstStep := newNh.ReelFirst() - re := regexp.MustCompile(firstStep.Expect[0]) - matches := re.FindStringSubmatch(testInputSuccess) - assert.Len(t, matches, 1) - assert.Equal(t, testInputSuccess, matches[0]) -} - -func Test_ReelFirstNegative(t *testing.T) { - newNh := nh.NewNodeHugepages(testTimeoutDuration, testNode, testExpectedHugepagesz, testExpectedHugepages) - assert.NotNil(t, newNh) - firstStep := newNh.ReelFirst() - re := regexp.MustCompile(firstStep.Expect[0]) - matches := re.FindStringSubmatch(testInputError) - assert.Len(t, matches, 0) -} - -func Test_ReelMatchSuccess(t *testing.T) { - newNh := nh.NewNodeHugepages(testTimeoutDuration, testNode, testExpectedHugepagesz, testExpectedHugepages) - assert.NotNil(t, newNh) - step := newNh.ReelMatch("", "", testInputSuccess) - assert.Nil(t, step) - assert.Equal(t, tnf.SUCCESS, newNh.Result()) -} - -func Test_ReelMatchFailure(t *testing.T) { - newNh := nh.NewNodeHugepages(testTimeoutDuration, testNode, testExpectedHugepagesz, testExpectedHugepages) - assert.NotNil(t, newNh) - step := newNh.ReelMatch("", "", testInputFailure) - assert.Nil(t, step) - assert.Equal(t, tnf.FAILURE, newNh.Result()) -} - -// Just ensure there are no panics. -func Test_ReelEof(t *testing.T) { - newNh := nh.NewNodeHugepages(testTimeoutDuration, testNode, testExpectedHugepagesz, testExpectedHugepages) - assert.NotNil(t, newNh) - newNh.ReelEOF() -} - -const ( - testTimeoutDuration = time.Second * 2 - testNode = "testNode" - testInputError = "" - testInputSuccess = "HugePages_Total: 64\nHugepagesize: 1048576 kB\n" - testInputFailure = "HugePages_Total: 32\nHugepagesize: 1000000 kB\n" - testExpectedHugepages = 64 - testExpectedHugepagesz = 1024 * 1024 -) diff --git a/pkg/tnf/handlers/nodenames/nodenames.go b/pkg/tnf/handlers/nodenames/nodenames.go index 16ff155f1..64ba4bfeb 100644 --- a/pkg/tnf/handlers/nodenames/nodenames.go +++ b/pkg/tnf/handlers/nodenames/nodenames.go @@ -71,7 +71,7 @@ func (nn *NodeNames) Args() []string { return nn.args } -// GetIdentifier returns the tnf.Test specific identifiesa. +// GetIdentifier returns the tnf.Test specific identifier. func (nn *NodeNames) GetIdentifier() identifier.Identifier { return identifier.NodeNamesIdentifier } diff --git a/pkg/tnf/handlers/nodeselector/nodeselector.go b/pkg/tnf/handlers/nodeselector/nodeselector.go index ff45bfe5a..260b1691d 100644 --- a/pkg/tnf/handlers/nodeselector/nodeselector.go +++ b/pkg/tnf/handlers/nodeselector/nodeselector.go @@ -40,7 +40,7 @@ func NewNodeSelector(timeout time.Duration, podName, podNamespace string) *NodeS return &NodeSelector{ timeout: timeout, result: tnf.ERROR, - args: []string{"oc", "-n", podNamespace, "get", "pods", podName, "-o", "custom-columns=nodeselector:.spec.nodeSelector,nodeaffinity:.spec.nodeAffinity"}, + args: []string{"oc", "-n", podNamespace, "get", "pods", podName, "-o", "custom-columns=nodeselector:.spec.nodeSelector,nodeaffinity:.spec.affinity.nodeAffinity"}, } } diff --git a/pkg/tnf/handlers/nodetainted/nodetainted.go b/pkg/tnf/handlers/nodetainted/nodetainted.go index 543398b74..cd4b03c07 100644 --- a/pkg/tnf/handlers/nodetainted/nodetainted.go +++ b/pkg/tnf/handlers/nodetainted/nodetainted.go @@ -33,15 +33,16 @@ type NodeTainted struct { result int timeout time.Duration args []string + Match string } // NewNodeTainted creates a new NodeTainted tnf.Test. -func NewNodeTainted(timeout time.Duration, nodeName string) *NodeTainted { +func NewNodeTainted(timeout time.Duration) *NodeTainted { return &NodeTainted{ timeout: timeout, result: tnf.ERROR, args: []string{ - "echo", "cat", "/proc/sys/kernel/tainted", "|", "oc", "debug", "node/" + nodeName, + "cat", "/proc/sys/kernel/tainted", }, } } @@ -51,7 +52,7 @@ func (nt *NodeTainted) Args() []string { return nt.args } -// GetIdentifier returns the tnf.Test specific identifiesa. +// GetIdentifier returns the tnf.Test specific identifier. func (nt *NodeTainted) GetIdentifier() identifier.Identifier { return identifier.NodeTaintedIdentifier } @@ -76,6 +77,7 @@ func (nt *NodeTainted) ReelFirst() *reel.Step { // ReelMatch tests whether node is tainted or not func (nt *NodeTainted) ReelMatch(_, _, match string) *reel.Step { + nt.Match = match if match == "0" { nt.result = tnf.SUCCESS } else { diff --git a/pkg/tnf/handlers/nodetainted/nodetainted_test.go b/pkg/tnf/handlers/nodetainted/nodetainted_test.go index dfe7a13f6..70eae8541 100644 --- a/pkg/tnf/handlers/nodetainted/nodetainted_test.go +++ b/pkg/tnf/handlers/nodetainted/nodetainted_test.go @@ -27,14 +27,14 @@ import ( ) func Test_NewNodeTainted(t *testing.T) { - newNt := nt.NewNodeTainted(testTimeoutDuration, testNodeName) + newNt := nt.NewNodeTainted(testTimeoutDuration) assert.NotNil(t, newNt) assert.Equal(t, testTimeoutDuration, newNt.Timeout()) assert.Equal(t, newNt.Result(), tnf.ERROR) } func Test_ReelFirstPositiveSuccess(t *testing.T) { - newNt := nt.NewNodeTainted(testTimeoutDuration, testNodeName) + newNt := nt.NewNodeTainted(testTimeoutDuration) assert.NotNil(t, newNt) firstStep := newNt.ReelFirst() re := regexp.MustCompile(firstStep.Expect[0]) @@ -44,7 +44,7 @@ func Test_ReelFirstPositiveSuccess(t *testing.T) { } func Test_ReelFirstPositiveFailure(t *testing.T) { - newNt := nt.NewNodeTainted(testTimeoutDuration, testNodeName) + newNt := nt.NewNodeTainted(testTimeoutDuration) assert.NotNil(t, newNt) firstStep := newNt.ReelFirst() re := regexp.MustCompile(firstStep.Expect[0]) @@ -54,7 +54,7 @@ func Test_ReelFirstPositiveFailure(t *testing.T) { } func Test_ReelFirstNegative(t *testing.T) { - newNt := nt.NewNodeTainted(testTimeoutDuration, testNodeName) + newNt := nt.NewNodeTainted(testTimeoutDuration) assert.NotNil(t, newNt) firstStep := newNt.ReelFirst() re := regexp.MustCompile(firstStep.Expect[0]) @@ -63,7 +63,7 @@ func Test_ReelFirstNegative(t *testing.T) { } func Test_ReelMatchSuccess(t *testing.T) { - newNt := nt.NewNodeTainted(testTimeoutDuration, testNodeName) + newNt := nt.NewNodeTainted(testTimeoutDuration) assert.NotNil(t, newNt) step := newNt.ReelMatch("", "", testMatchSuccess) assert.Nil(t, step) @@ -71,7 +71,7 @@ func Test_ReelMatchSuccess(t *testing.T) { } func Test_ReelMatchFail(t *testing.T) { - newNt := nt.NewNodeTainted(testTimeoutDuration, testNodeName) + newNt := nt.NewNodeTainted(testTimeoutDuration) assert.NotNil(t, newNt) step := newNt.ReelMatch("", "", testMatchFailure) assert.Nil(t, step) @@ -80,13 +80,12 @@ func Test_ReelMatchFail(t *testing.T) { // Just ensure there are no panics. func Test_ReelEof(t *testing.T) { - newNt := nt.NewNodeTainted(testTimeoutDuration, testNodeName) + newNt := nt.NewNodeTainted(testTimeoutDuration) assert.NotNil(t, newNt) newNt.ReelEOF() } const ( - testNodeName = "testNode" testTimeoutDuration = time.Second * 2 testInputError = "" testInputFailure = "1\n" diff --git a/pkg/tnf/handlers/operator/doc.go b/pkg/tnf/handlers/operator/doc.go index 7286ffc2e..58bdacc48 100644 --- a/pkg/tnf/handlers/operator/doc.go +++ b/pkg/tnf/handlers/operator/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/operator/operator.go b/pkg/tnf/handlers/operator/operator.go index 981584d48..b6f4b5aff 100644 --- a/pkg/tnf/handlers/operator/operator.go +++ b/pkg/tnf/handlers/operator/operator.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/operator/operator_test.go b/pkg/tnf/handlers/operator/operator_test.go index 83ce19173..16d78a17b 100644 --- a/pkg/tnf/handlers/operator/operator_test.go +++ b/pkg/tnf/handlers/operator/operator_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/owners/owners.go b/pkg/tnf/handlers/owners/owners.go index 882b68eb1..805db4ded 100644 --- a/pkg/tnf/handlers/owners/owners.go +++ b/pkg/tnf/handlers/owners/owners.go @@ -27,6 +27,12 @@ import ( const ( owRegex = "(?s)OWNERKIND\n.+" + // statefulSet variable + statefulSet = "StatefulSet" + // replicaSet variable + replicaSet = "ReplicaSet" + // daemonSet variable + daemonSet = "DaemonSet" ) // Owners tests pod owners @@ -51,7 +57,7 @@ func (ow *Owners) Args() []string { return ow.args } -// GetIdentifier returns the tnf.Test specific identifiesa. +// GetIdentifier returns the tnf.Test specific identifier. func (ow *Owners) GetIdentifier() identifier.Identifier { return identifier.OwnersIdentifier } @@ -76,7 +82,8 @@ func (ow *Owners) ReelFirst() *reel.Step { // ReelMatch ensures that list of nodes is not empty and stores the names as []string func (ow *Owners) ReelMatch(_, _, match string) *reel.Step { - if strings.Contains(match, "ReplicaSet") && !strings.Contains(match, "DaemonSet") { + if (strings.Contains(match, statefulSet) || strings.Contains(match, replicaSet)) && + !strings.Contains(match, daemonSet) { ow.result = tnf.SUCCESS } else { ow.result = tnf.FAILURE diff --git a/pkg/tnf/handlers/owners/owners_test.go b/pkg/tnf/handlers/owners/owners_test.go index 52f3dcf51..4f5b31153 100644 --- a/pkg/tnf/handlers/owners/owners_test.go +++ b/pkg/tnf/handlers/owners/owners_test.go @@ -95,6 +95,7 @@ var ( } testInputSuccessSlice = []string{ "OWNERKIND\nReplicaSet\n", + "OWNERKIND\nStatefulSet\n", "OWNERKIND\nOwner\nOWNERKIND\nReplicaSet\n", } ) diff --git a/pkg/tnf/handlers/ping/doc.go b/pkg/tnf/handlers/ping/doc.go index 1b0d1b768..f6b5edf57 100644 --- a/pkg/tnf/handlers/ping/doc.go +++ b/pkg/tnf/handlers/ping/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/handlers/ping/ping.go b/pkg/tnf/handlers/ping/ping.go index d755560eb..8d5115fe2 100644 --- a/pkg/tnf/handlers/ping/ping.go +++ b/pkg/tnf/handlers/ping/ping.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -25,6 +25,7 @@ import ( "github.com/test-network-function/test-network-function/pkg/tnf/dependencies" "github.com/test-network-function/test-network-function/pkg/tnf/identifier" "github.com/test-network-function/test-network-function/pkg/tnf/reel" + "github.com/test-network-function/test-network-function/pkg/utils" ) // Ping provides a ping test implemented using command line tool `ping`. @@ -130,6 +131,14 @@ func Command(host string, count int) []string { return []string{dependencies.PingBinaryName, host} } +// Command same as command but uses nsenter to in the command +func CommandNsenter(containerPID, host string, count int) []string { + if count > 0 { + return []string{utils.AddNsenterPrefix(containerPID), dependencies.PingBinaryName, "-c", strconv.Itoa(count), host} + } + return []string{utils.AddNsenterPrefix(containerPID), dependencies.PingBinaryName, host} +} + // NewPing creates a new `Ping` test which pings `hosts` with `count` requests, or indefinitely if `count` is not // positive, and executes within `timeout` seconds. func NewPing(timeout time.Duration, host string, count int) *Ping { @@ -140,6 +149,15 @@ func NewPing(timeout time.Duration, host string, count int) *Ping { } } +// NewPingNsenter same as NewPing but takes a containerID to run ping with the nsenter command with the node OC. +func NewPingNsenter(timeout time.Duration, containerPID, host string, count int) *Ping { + return &Ping{ + result: tnf.ERROR, + timeout: timeout, + args: CommandNsenter(containerPID, host, count), + } +} + // GetReelFirstRegularExpressions returns the regular expressions used for matching in ReelFirst. func (p *Ping) GetReelFirstRegularExpressions() []string { return []string{ConnectInvalidArgumentRegex, SuccessfulOutputRegex} diff --git a/pkg/tnf/handlers/ping/ping_test.go b/pkg/tnf/handlers/ping/ping_test.go index abd7d0f54..50da86dc4 100644 --- a/pkg/tnf/handlers/ping/ping_test.go +++ b/pkg/tnf/handlers/ping/ping_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,7 +18,7 @@ package ping_test import ( "fmt" - "io/ioutil" + "os" "path" "strconv" "testing" @@ -102,7 +102,7 @@ func getMockOutputFilename(testName string) string { func getMockOutput(t *testing.T, testName string) string { fileName := getMockOutputFilename(testName) - b, err := ioutil.ReadFile(fileName) + b, err := os.ReadFile(fileName) assert.Nil(t, err) return string(b) } diff --git a/pkg/tnf/handlers/hugepages/doc.go b/pkg/tnf/handlers/podsets/doc.go similarity index 84% rename from pkg/tnf/handlers/hugepages/doc.go rename to pkg/tnf/handlers/podsets/doc.go index 3af93656a..8c953d9f3 100644 --- a/pkg/tnf/handlers/hugepages/doc.go +++ b/pkg/tnf/handlers/podsets/doc.go @@ -14,5 +14,5 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// Package hugepages provides a test for reading the worker nodes' hugepages configuration in MachineConfig CRs -package hugepages +// Package PodSet-deployments/statefulsets provides a test for reading the namespace's PodSet deployments/statefulsets +package podsets diff --git a/pkg/tnf/handlers/deployments/deployments.go b/pkg/tnf/handlers/podsets/podsets.go similarity index 52% rename from pkg/tnf/handlers/deployments/deployments.go rename to pkg/tnf/handlers/podsets/podsets.go index 669cfe657..543ab3352 100644 --- a/pkg/tnf/handlers/deployments/deployments.go +++ b/pkg/tnf/handlers/podsets/podsets.go @@ -14,7 +14,7 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -package deployments +package podsets import ( "strconv" @@ -30,79 +30,84 @@ const ( dpRegex = "(?s).+" ) -// Deployment holds information about a single deployment -type Deployment struct { +// PodSet holds information about a single Deployment/statefulsets +type PodSet struct { Replicas int Ready int UpToDate int - Available int + Available int // this param applies only to deployments, not to statefulsets Unavailable int + Current int // this param applies only to statefulsets, not to deployments } -// DeploymentMap name to Deployment -type DeploymentMap map[string]Deployment +// PodSetMap maps a deployment/statefulset name to a PodSet +type PodSetMap map[string]PodSet -// Deployments holds information derived from running "oc -n get deployments" on the command line. -type Deployments struct { - deployments DeploymentMap - result int - timeout time.Duration - args []string +// PodSets holds information derived from running "oc -n get deployments/statefulsets" on the command line. +type PodSets struct { + podsets PodSetMap + namespace string + result int + timeout time.Duration + args []string } -// NewDeployments creates a new Deployments tnf.Test. -func NewDeployments(timeout time.Duration, namespace string) *Deployments { - return &Deployments{ - timeout: timeout, - result: tnf.ERROR, - args: []string{"oc", "-n", namespace, "get", "deployments", "-o", "custom-columns=" + +// NewPodSets creates a new PodSets tnf.Test. +func NewPodSets(timeout time.Duration, namespace, resourceType string) *PodSets { + return &PodSets{ + timeout: timeout, + namespace: namespace, + result: tnf.ERROR, + args: []string{"oc", "-n", namespace, "get", resourceType, "-o", "custom-columns=" + "NAME:.metadata.name," + "REPLICAS:.spec.replicas," + "READY:.status.readyReplicas," + "UPDATED:.status.updatedReplicas," + "AVAILABLE:.status.availableReplicas," + - "UNAVAILABLE:.status.unavailableReplicas", + "UNAVAILABLE:.status.unavailableReplicas," + + "CURRENT:.status.currentReplicas", }, - deployments: DeploymentMap{}, + + podsets: PodSetMap{}, } } -// GetDeployments returns deployments extracted from running the Deployments tnf.Test. -func (dp *Deployments) GetDeployments() DeploymentMap { - return dp.deployments +// GetPodSets returns deployments/statefulsets extracted from running the PodSets tnf.Test. +func (ps *PodSets) GetPodSets() PodSetMap { + return ps.podsets } // Args returns the command line args for the test. -func (dp *Deployments) Args() []string { - return dp.args +func (ps *PodSets) Args() []string { + return ps.args } -// GetIdentifier returns the tnf.Test specific identifiesa. -func (dp *Deployments) GetIdentifier() identifier.Identifier { - return identifier.DeploymentsIdentifier +// GetIdentifier returns the tnf.Test specific identifier. +func (ps *PodSets) GetIdentifier() identifier.Identifier { + return identifier.PodSetsIdentifier } // Timeout returns the timeout in seconds for the test. -func (dp *Deployments) Timeout() time.Duration { - return dp.timeout +func (ps *PodSets) Timeout() time.Duration { + return ps.timeout } // Result returns the test result. -func (dp *Deployments) Result() int { - return dp.result +func (ps *PodSets) Result() int { + return ps.result } // ReelFirst returns a step which expects the ping statistics within the test timeout. -func (dp *Deployments) ReelFirst() *reel.Step { +func (ps *PodSets) ReelFirst() *reel.Step { return &reel.Step{ Expect: []string{dpRegex}, - Timeout: dp.timeout, + Timeout: ps.timeout, } } // ReelMatch ensures that list of nodes is not empty and stores the names as []string -func (dp *Deployments) ReelMatch(_, _, match string) *reel.Step { - const numExepctedFields = 6 +func (ps *PodSets) ReelMatch(_, _, match string) *reel.Step { + const numExepctedFields = 7 trimmedMatch := strings.Trim(match, "\n") lines := strings.Split(trimmedMatch, "\n")[1:] // First line is the headers/titles line @@ -114,20 +119,23 @@ func (dp *Deployments) ReelMatch(_, _, match string) *reel.Step { if len(fields) != numExepctedFields { return nil } - dp.deployments[fields[0]] = Deployment{atoi(fields[1]), atoi(fields[2]), atoi(fields[3]), atoi(fields[4]), atoi(fields[5])} + // we can have the same deployment/statefulset in different namespaces + // this ensures the uniqueness of the deployment/statefulset in the test + key := ps.namespace + ":" + fields[0] + ps.podsets[key] = PodSet{atoi(fields[1]), atoi(fields[2]), atoi(fields[3]), atoi(fields[4]), atoi(fields[5]), atoi(fields[6])} } - dp.result = tnf.SUCCESS + ps.result = tnf.SUCCESS return nil } // ReelTimeout does nothing; no action is necessary upon timeout. -func (dp *Deployments) ReelTimeout() *reel.Step { +func (ps *PodSets) ReelTimeout() *reel.Step { return nil } // ReelEOF does nothing; no action is necessary upon EOF. -func (dp *Deployments) ReelEOF() { +func (ps *PodSets) ReelEOF() { } func atoi(s string) int { diff --git a/pkg/tnf/handlers/podsets/podsets_test.go b/pkg/tnf/handlers/podsets/podsets_test.go new file mode 100644 index 000000000..48d477ad3 --- /dev/null +++ b/pkg/tnf/handlers/podsets/podsets_test.go @@ -0,0 +1,174 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package podsets_test + +import ( + "regexp" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf" + ps "github.com/test-network-function/test-network-function/pkg/tnf/handlers/podsets" +) + +func Test_NewPodSets(t *testing.T) { + newDp := ps.NewPodSets(testTimeoutDuration, testNamespace, resourceType) + assert.NotNil(t, newDp) + assert.Equal(t, testTimeoutDuration, newDp.Timeout()) + assert.Equal(t, newDp.Result(), tnf.ERROR) + assert.NotNil(t, newDp.GetPodSets()) +} + +func Test_StatefulsetNewPodSets(t *testing.T) { + newDp := ps.NewPodSets(testTimeoutDuration, testNamespace, "statefulset") + assert.NotNil(t, newDp) + assert.Equal(t, testTimeoutDuration, newDp.Timeout()) + assert.Equal(t, newDp.Result(), tnf.ERROR) + assert.NotNil(t, newDp.GetPodSets()) +} +func Test_ReelFirstPositive(t *testing.T) { + newDp := ps.NewPodSets(testTimeoutDuration, testNamespace, resourceType) + assert.NotNil(t, newDp) + firstStep := newDp.ReelFirst() + re := regexp.MustCompile(firstStep.Expect[0]) + matches := re.FindStringSubmatch(testInputSuccess) + assert.Len(t, matches, 1) + assert.Equal(t, testInputSuccess, matches[0]) +} +func Test_StatefulsetReelFirstPositive(t *testing.T) { + newDp := ps.NewPodSets(testTimeoutDuration, testNamespace, "statefulset") + assert.NotNil(t, newDp) + firstStep := newDp.ReelFirst() + re := regexp.MustCompile(firstStep.Expect[0]) + matches := re.FindStringSubmatch(statefulSuccess) + assert.Len(t, matches, 1) + assert.Equal(t, statefulSuccess, matches[0]) +} +func Test_ReelFirstNegative(t *testing.T) { + newDp := ps.NewPodSets(testTimeoutDuration, testNamespace, resourceType) + assert.NotNil(t, newDp) + firstStep := newDp.ReelFirst() + re := regexp.MustCompile(firstStep.Expect[0]) + matches := re.FindStringSubmatch(testInputError) + assert.Len(t, matches, 0) +} +func Test_StatefulsetReelFirstNegative(t *testing.T) { + newDp := ps.NewPodSets(testTimeoutDuration, testNamespace, "statefulset") + assert.NotNil(t, newDp) + firstStep := newDp.ReelFirst() + re := regexp.MustCompile(firstStep.Expect[0]) + matches := re.FindStringSubmatch(testInputError) + assert.Len(t, matches, 0) +} + +func Test_ReelMatchSuccess(t *testing.T) { + newDp := ps.NewPodSets(testTimeoutDuration, testNamespace, resourceType) + assert.NotNil(t, newDp) + step := newDp.ReelMatch("", "", testInputSuccess) + assert.Nil(t, step) + assert.Equal(t, tnf.SUCCESS, newDp.Result()) + assert.Len(t, newDp.GetPodSets(), testInputSuccessNumLines) + + expectedDeployments := ps.PodSetMap{ + "testNamespace:cdi-apiserver": {1, 1, 1, 1, 0, 0}, + "testNamespace:hyperconverged-cluster-operator": {1, 0, 1, 0, 1, 0}, + "testNamespace:virt-api": {2, 2, 2, 2, 0, 0}, + "testNamespace:vm-import-operator": {0, 0, 0, 0, 0, 0}, + } + deployments := newDp.GetPodSets() + + for name, expected := range expectedDeployments { + deployment, ok := deployments[name] + assert.True(t, ok) + assert.Equal(t, expected, deployment) + } +} + +func Test_StatefulsetReelMatchSuccess(t *testing.T) { + newDp := ps.NewPodSets(testTimeoutDuration, testNamespace, "statefulset") + assert.NotNil(t, newDp) + step := newDp.ReelMatch("", "", statefulSuccess) + assert.Nil(t, step) + assert.Equal(t, tnf.SUCCESS, newDp.Result()) + assert.Len(t, newDp.GetPodSets(), testInputSuccessNumLines) + + expectedDeployments := ps.PodSetMap{ + "testNamespace:cdi-apiserver": {1, 1, 1, 0, 0, 1}, + "testNamespace:hyperconverged-cluster-operator": {1, 0, 1, 0, 1, 1}, + "testNamespace:virt-api": {2, 2, 2, 0, 0, 2}, + "testNamespace:vm-import-operator": {0, 0, 0, 0, 0, 0}, + } + statefulsets := newDp.GetPodSets() + + for name, expected := range expectedDeployments { + statefulset, ok := statefulsets[name] + assert.True(t, ok) + assert.Equal(t, expected, statefulset) + } +} + +// Just ensure there are no panics. +func Test_ReelEof(t *testing.T) { + newDp := ps.NewPodSets(testTimeoutDuration, testNamespace, resourceType) + assert.NotNil(t, newDp) + newDp.ReelEOF() +} + +const ( + resourceType = "deployment" + testTimeoutDuration = time.Second * 2 + testNamespace = "testNamespace" + testInputError = "" + testInputSuccessNumLines = 17 + testInputSuccess = `NAME REPLICAS READY UPDATED AVAILABLE UNAVAILABLE CURRENT + cdi-apiserver 1 1 1 1 + cdi-deployment 1 1 1 1 + cdi-operator 1 1 1 1 + cdi-uploadproxy 1 1 1 1 + cluster-network-addons-operator 1 1 1 1 + hostpath-provisioner-operator 1 1 1 1 + hyperconverged-cluster-operator 1 1 1 + kubemacpool-mac-controller-manager 1 1 1 1 + kubevirt-ssp-operator 1 1 1 + nmstate-webhook 2 2 2 2 + node-maintenance-operator 1 1 1 + v2v-vmware 1 1 1 1 + virt-api 2 2 2 2 + virt-controller 2 2 2 2 + virt-operator 2 2 2 2 + virt-template-validator 2 2 2 2 + vm-import-operator 0 ` + statefulSuccess = `NAME REPLICAS READY UPDATED AVAILABLE UNAVAILABLE CURRENT + cdi-apiserver 1 1 1 1 + cdi-deployment 1 1 1 1 + cdi-operator 1 1 1 1 + cdi-uploadproxy 1 1 1 1 + cluster-network-addons-operator 1 1 1 1 + hostpath-provisioner-operator 1 1 1 1 + hyperconverged-cluster-operator 1 1 1 1 + kubemacpool-mac-controller-manager 1 1 1 1 + kubevirt-ssp-operator 1 1 1 1 + nmstate-webhook 2 2 2 2 + node-maintenance-operator 1 1 1 1 + v2v-vmware 1 1 1 2 + virt-api 2 2 2 2 + virt-controller 2 2 2 2 + virt-operator 2 2 2 2 + virt-template-validator 2 2 2 2 + vm-import-operator 0 ` +) diff --git a/pkg/tnf/handlers/readbootconfig/readbootconfig.go b/pkg/tnf/handlers/readbootconfig/readbootconfig.go index 75cb709fb..2463426e4 100644 --- a/pkg/tnf/handlers/readbootconfig/readbootconfig.go +++ b/pkg/tnf/handlers/readbootconfig/readbootconfig.go @@ -37,12 +37,12 @@ type ReadBootConfig struct { } // NewReadBootConfig creates a ReadBootConfig tnf.Test. -func NewReadBootConfig(timeout time.Duration, nodeName, entryName string) *ReadBootConfig { +func NewReadBootConfig(timeout time.Duration) *ReadBootConfig { return &ReadBootConfig{ timeout: timeout, result: tnf.ERROR, args: []string{ - "echo", "\"cat /host/boot/loader/entries/" + entryName + "\"", "|", "oc", "debug", "-q", "node/" + nodeName, + "cat /host/boot/loader/entries/$(ls /host/boot/loader/entries/ | sort | tail -n 1)", }, } } diff --git a/pkg/tnf/handlers/readbootconfig/readbootconfig_test.go b/pkg/tnf/handlers/readbootconfig/readbootconfig_test.go index c1f1cec9a..86bb1a132 100644 --- a/pkg/tnf/handlers/readbootconfig/readbootconfig_test.go +++ b/pkg/tnf/handlers/readbootconfig/readbootconfig_test.go @@ -28,13 +28,13 @@ import ( ) func TestReadBootConfig(t *testing.T) { - newReadBootConfig := readbootconfig.NewReadBootConfig(testTimeoutDuration, testNodeName, testBootEntryName) + newReadBootConfig := readbootconfig.NewReadBootConfig(testTimeoutDuration) assert.NotNil(t, newReadBootConfig) assert.Equal(t, tnf.ERROR, newReadBootConfig.Result()) } func Test_ReelFirst(t *testing.T) { - newReadBootConfig := readbootconfig.NewReadBootConfig(testTimeoutDuration, testNodeName, testBootEntryName) + newReadBootConfig := readbootconfig.NewReadBootConfig(testTimeoutDuration) assert.NotNil(t, newReadBootConfig) firstStep := newReadBootConfig.ReelFirst() re := regexp.MustCompile(firstStep.Expect[0]) @@ -44,7 +44,7 @@ func Test_ReelFirst(t *testing.T) { } func Test_ReelMatch(t *testing.T) { - newBootConfig := bootconfigentries.NewBootConfigEntries(testTimeoutDuration, testNodeName) + newBootConfig := bootconfigentries.NewBootConfigEntries(testTimeoutDuration) assert.NotNil(t, newBootConfig) step := newBootConfig.ReelMatch("", "", testInput) assert.Nil(t, step) @@ -53,7 +53,7 @@ func Test_ReelMatch(t *testing.T) { // Just ensure there are no panics. func Test_ReelEof(t *testing.T) { - newBootConfig := bootconfigentries.NewBootConfigEntries(testTimeoutDuration, testNodeName) + newBootConfig := bootconfigentries.NewBootConfigEntries(testTimeoutDuration) assert.NotNil(t, newBootConfig) newBootConfig.ReelEOF() } @@ -65,6 +65,4 @@ const ( options random.trust_cpu=on console=tty0 linux /ostree/rhcos-8db99645874 initrd /ostree/rhcos-8db99645874` - testNodeName = "crc-l6qvn-master-0" - testBootEntryName = "ostree-2-rhcos.conf" ) diff --git a/pkg/tnf/handlers/readiness/doc.go b/pkg/tnf/handlers/readiness/doc.go new file mode 100644 index 000000000..a2a1c9a74 --- /dev/null +++ b/pkg/tnf/handlers/readiness/doc.go @@ -0,0 +1,17 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +// Package readiness provides a test for readiness. +package readiness diff --git a/pkg/tnf/handlers/readiness/readiness.gotemplate b/pkg/tnf/handlers/readiness/readiness.gotemplate new file mode 100644 index 000000000..983a7c09f --- /dev/null +++ b/pkg/tnf/handlers/readiness/readiness.gotemplate @@ -0,0 +1,3 @@ +{{- range .spec.containers -}} + {{ if .readinessProbe }} {{"readiness-defined\n"}} {{- else -}} {{"readiness-not-defined\n"}}{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/pkg/tnf/handlers/readiness/readiness.json b/pkg/tnf/handlers/readiness/readiness.json new file mode 100644 index 000000000..a572eed9a --- /dev/null +++ b/pkg/tnf/handlers/readiness/readiness.json @@ -0,0 +1,26 @@ +{ + "identifier" : { + "url" : "http://test-network-function.com/tests/readiness", + "version": "v1.0.0" + }, + "description": "Test check if readiness is defined.", + "testResult": 0, + "testTimeout": 5000000000, + "reelFirstStep": { + "execute": "oc get pod -n {{.POD_NAMESPACE}} {{.POD_NAME}} -o go-template-file={{.GO_TEMPLATE_PATH}}/readiness.gotemplate", + "expect":[ "(?m)readiness-not-defined", + "(?m)readiness-defined"], + "timeout": 5000000000 + }, + "resultContexts":[ + { + "pattern": "(?m)readiness-not-defined", + "defaultResult": 2 + }, + { + "pattern": "(?m)readiness-defined", + "defaultResult": 1 + } + ] + } + \ No newline at end of file diff --git a/pkg/tnf/handlers/readiness/readiness_test.go b/pkg/tnf/handlers/readiness/readiness_test.go new file mode 100644 index 000000000..b1b863a15 --- /dev/null +++ b/pkg/tnf/handlers/readiness/readiness_test.go @@ -0,0 +1,137 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package readiness + +import ( + "fmt" + "path" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/generic" + "github.com/test-network-function/test-network-function/pkg/tnf/identifier" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" + "github.com/xeipuuv/gojsonschema" +) + +const ( + testTimeoutDuration = time.Second * 5 +) + +var ( + genericTestSchemaFile = path.Join("schemas", "generic-test.schema.json") + livenessFilename = "readiness.json" + /* #nosec G101 */ + expectedPassPattern = "(?m)readiness-defined" + expectedFailPattern = "(?m)readiness-not-defined" + pathRelativeToRoot = path.Join("..", "..", "..", "..") + pathToTestSchemaFile = path.Join(pathRelativeToRoot, genericTestSchemaFile) + testPodNameSpace = "testnamespace" + testPodName = "testPodname" +) + +func createTest() (*tnf.Tester, []reel.Handler, *gojsonschema.Result, error) { + values := make(map[string]interface{}) + values["POD_NAMESPACE"] = testPodNameSpace + values["POD_NAME"] = testPodName + values["GO_TEMPLATE_PATH"] = "." + return generic.NewGenericFromMap(livenessFilename, pathToTestSchemaFile, values) +} + +func TestLiveness_Args(t *testing.T) { + test, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Nil(t, (*test).Args()) +} + +func TestLiveness_GetIdentifier(t *testing.T) { + test, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, identifier.ReadinessURLIdentifier, (*test).GetIdentifier()) +} + +func TestLiveness_ReelFirst(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + step := handler.ReelFirst() + expectedCommand := fmt.Sprintf("oc get pod -n %s %s -o go-template-file=./readiness.gotemplate", + testPodNameSpace, testPodName) + assert.Equal(t, expectedCommand, step.Execute) + assert.Contains(t, step.Expect, expectedPassPattern, expectedFailPattern) + assert.Equal(t, testTimeoutDuration, step.Timeout) +} + +func TestLiveness_ReelEof(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + // just ensure there isn't a panic + handler.ReelEOF() +} + +func TestLiveness_ReelTimeout(t *testing.T) { + _, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + assert.Nil(t, handler.ReelTimeout()) +} + +func TestLiveness_ReelMatch(t *testing.T) { + tester, handlers, jsonParseResult, err := createTest() + + assert.Nil(t, err) + assert.True(t, jsonParseResult.Valid()) + assert.NotNil(t, handlers) + + assert.Equal(t, 1, len(handlers)) + handler := handlers[0] + step := handler.ReelMatch(expectedFailPattern, "", "readiness-not-defined") + + assert.Nil(t, step) + assert.Equal(t, tnf.FAILURE, (*tester).Result()) + + step = handler.ReelMatch(expectedPassPattern, "", "readiness-defined") + + assert.Nil(t, step) + assert.Equal(t, tnf.SUCCESS, (*tester).Result()) +} diff --git a/pkg/tnf/handlers/readremotefile/doc.go b/pkg/tnf/handlers/readremotefile/doc.go deleted file mode 100644 index 8d638fdce..000000000 --- a/pkg/tnf/handlers/readremotefile/doc.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -// Package readremotefile provides a test for reading an node's next boot config /boot directory -package readremotefile diff --git a/pkg/tnf/handlers/readremotefile/readremotefile.go b/pkg/tnf/handlers/readremotefile/readremotefile.go deleted file mode 100644 index 6d9d61e2d..000000000 --- a/pkg/tnf/handlers/readremotefile/readremotefile.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package readremotefile - -import ( - "time" - - "github.com/test-network-function/test-network-function/pkg/tnf" - "github.com/test-network-function/test-network-function/pkg/tnf/identifier" - "github.com/test-network-function/test-network-function/pkg/tnf/reel" -) - -const ( - successfulOutputRegex = `(?s).+` -) - -// ReadRemoteFile holds information regarding remote file contents at a specified node and path. -type ReadRemoteFile struct { - remoteFileContents string // Output variable that stores the remote file contents - result int - timeout time.Duration - args []string -} - -// NewReadRemoteFile creates a ReadRemoteFile tnf.Test. -func NewReadRemoteFile(timeout time.Duration, nodeName, filePath string) *ReadRemoteFile { - return &ReadRemoteFile{ - timeout: timeout, - result: tnf.ERROR, - args: []string{ - "echo", "\"cat /host" + filePath + "\"", "|", "oc", "debug", "-q", "node/" + nodeName, - }, - } -} - -// GetRemoteFileContents returns the file contents extracted from the specified path while running the ReadRemoteFile tnf.Test. -func (handler *ReadRemoteFile) GetRemoteFileContents() string { - return handler.remoteFileContents -} - -// Args returns the command line args for the test. -func (handler *ReadRemoteFile) Args() []string { - return handler.args -} - -// GetIdentifier returns the tnf.Test specific identifier. -func (handler *ReadRemoteFile) GetIdentifier() identifier.Identifier { - return identifier.ReadRemoteFileURLIdentifier -} - -// Timeout returns the timeout in seconds for the test. -func (handler *ReadRemoteFile) Timeout() time.Duration { - return handler.timeout -} - -// Result returns the test result. -func (handler *ReadRemoteFile) Result() int { - return handler.result -} - -// ReelFirst returns a step which expects the grub kernel arguments within the test timeout. -func (handler *ReadRemoteFile) ReelFirst() *reel.Step { - return &reel.Step{ - Expect: []string{successfulOutputRegex}, - Timeout: handler.timeout, - } -} - -// ReelMatch just forwards the output to handler.remoteFileContents. -func (handler *ReadRemoteFile) ReelMatch(_, _, match string) *reel.Step { - handler.remoteFileContents = match - handler.result = tnf.SUCCESS - return nil -} - -// ReelTimeout does nothing; no action is necessary upon timeout. -func (handler *ReadRemoteFile) ReelTimeout() *reel.Step { - return nil -} - -// ReelEOF does nothing; no action is necessary on EOF. -func (handler *ReadRemoteFile) ReelEOF() { -} diff --git a/pkg/tnf/handlers/readremotefile/readremotefile_test.go b/pkg/tnf/handlers/readremotefile/readremotefile_test.go deleted file mode 100644 index b624de744..000000000 --- a/pkg/tnf/handlers/readremotefile/readremotefile_test.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package readremotefile_test - -import ( - "regexp" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/test-network-function/test-network-function/pkg/tnf" - "github.com/test-network-function/test-network-function/pkg/tnf/handlers/readremotefile" -) - -func TestReadRemoteFile(t *testing.T) { - newReadRemoteFile := readremotefile.NewReadRemoteFile(testTimeoutDuration, testNodeName, testRemotePath) - assert.NotNil(t, newReadRemoteFile) - assert.Equal(t, tnf.ERROR, newReadRemoteFile.Result()) -} - -func Test_ReelFirst(t *testing.T) { - newReadRemoteFile := readremotefile.NewReadRemoteFile(testTimeoutDuration, testNodeName, testRemotePath) - assert.NotNil(t, newReadRemoteFile) - firstStep := newReadRemoteFile.ReelFirst() - re := regexp.MustCompile(firstStep.Expect[0]) - matches := re.FindStringSubmatch(testInput) - assert.Len(t, matches, 1) - assert.Equal(t, testInput, matches[0]) -} - -func Test_ReelMatch(t *testing.T) { - newReadRemoteFile := readremotefile.NewReadRemoteFile(testTimeoutDuration, testNodeName, testRemotePath) - assert.NotNil(t, newReadRemoteFile) - step := newReadRemoteFile.ReelMatch("", "", testInput) - assert.Nil(t, step) - assert.Equal(t, tnf.SUCCESS, newReadRemoteFile.Result()) -} - -// Just ensure there are no panics. -func Test_ReelEOF(t *testing.T) { - newReadRemoteFile := readremotefile.NewReadRemoteFile(testTimeoutDuration, testNodeName, testRemotePath) - assert.NotNil(t, newReadRemoteFile) - newReadRemoteFile.ReelEOF() -} - -const ( - testTimeoutDuration = time.Second * 2 - testInput = ` /usr/lib/sysctl.d/, /run/sysctl.d/, and /etc/sysctl.d/. - # - # Vendors settings live in /usr/lib/sysctl.d/. - # To override a whole file, create a new file with the same in - # /etc/sysctl.d/ and put new settings there. To override - # only specific settings, add a file with a lexically later - # name in /etc/sysctl.d/ and put new settings there. - # - # For more information, see sysctl.conf(5) and sysctl.d(5). - ` - testNodeName = "crc-l6qvn-master-0" - testRemotePath = "/etc/sysctl.conf" -) diff --git a/pkg/tnf/handlers/rolebinding/rolebinding.go b/pkg/tnf/handlers/rolebinding/rolebinding.go index 6f9002b53..c7d615807 100644 --- a/pkg/tnf/handlers/rolebinding/rolebinding.go +++ b/pkg/tnf/handlers/rolebinding/rolebinding.go @@ -40,7 +40,7 @@ type RoleBinding struct { // NewRoleBinding creates a new RoleBinding tnf.Test. func NewRoleBinding(timeout time.Duration, serviceAccountName, podNamespace string) *RoleBinding { - serviceAccountSubString := "name:" + serviceAccountName + " namespace:" + podNamespace + serviceAccountSubString := "name:\\b" + serviceAccountName + "\\b namespace:\\b" + podNamespace + "\\b" return &RoleBinding{ podNamespace: podNamespace, timeout: timeout, @@ -62,7 +62,7 @@ func (rb *RoleBinding) Args() []string { return rb.args } -// GetIdentifier returns the tnf.Test specific identifiesa. +// GetIdentifier returns the tnf.Test specific identifier. func (rb *RoleBinding) GetIdentifier() identifier.Identifier { return identifier.RoleBindingIdentifier } diff --git a/pkg/tnf/handlers/graceperiod/doc.go b/pkg/tnf/handlers/scaling/doc.go similarity index 88% rename from pkg/tnf/handlers/graceperiod/doc.go rename to pkg/tnf/handlers/scaling/doc.go index 007ce10fc..546450436 100644 --- a/pkg/tnf/handlers/graceperiod/doc.go +++ b/pkg/tnf/handlers/scaling/doc.go @@ -14,5 +14,5 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// Package graceperiod provides a test for reading the CNF pod's graceperiod -package graceperiod +// Package scaling provides a test for deployments scale in/out +package scaling diff --git a/pkg/tnf/handlers/scaling/scaling.go b/pkg/tnf/handlers/scaling/scaling.go new file mode 100644 index 000000000..29acb3fdd --- /dev/null +++ b/pkg/tnf/handlers/scaling/scaling.go @@ -0,0 +1,95 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package scaling + +import ( + "fmt" + "strings" + "time" + + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/identifier" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" +) + +const ( + ocCommand = "oc scale --replicas=%d %s %s -n %s" + regex = "^%s.*/%s scaled" +) + +// Scaling holds the Scaling handler parameters. +type Scaling struct { + result int + timeout time.Duration + args []string + regex string +} + +// NewScaling creates a new Scaling handler. +func NewScaling(timeout time.Duration, namespace, podsetName, typesource string, replicaCount int) *Scaling { + command := fmt.Sprintf(ocCommand, replicaCount, typesource, podsetName, namespace) + return &Scaling{ + timeout: timeout, + result: tnf.ERROR, + args: strings.Fields(command), + regex: fmt.Sprintf(regex, typesource, podsetName), + } +} + +// Args returns the command line args for the test. +func (scaling *Scaling) Args() []string { + return scaling.args +} + +// GetIdentifier returns the tnf.Test specific identifier. +func (scaling *Scaling) GetIdentifier() identifier.Identifier { + return identifier.ScalingIdentifier +} + +// Timeout returns the timeout in seconds for the test. +func (scaling *Scaling) Timeout() time.Duration { + return scaling.timeout +} + +// Result returns the test result. +func (scaling *Scaling) Result() int { + return scaling.result +} + +// ReelFirst returns a step which expects the scale command output within the test timeout. +func (scaling *Scaling) ReelFirst() *reel.Step { + return &reel.Step{ + Execute: "", + Expect: []string{scaling.regex}, + Timeout: scaling.timeout, + } +} + +// ReelMatch does nothing, just set the test result as success. +func (scaling *Scaling) ReelMatch(_, _, match string) *reel.Step { + scaling.result = tnf.SUCCESS + return nil +} + +// ReelTimeout does nothing; no action is necessary upon timeout. +func (scaling *Scaling) ReelTimeout() *reel.Step { + return nil +} + +// ReelEOF does nothing; no action is necessary upon EOF. +func (scaling *Scaling) ReelEOF() { +} diff --git a/pkg/tnf/handlers/scaling/scaling_hpa.go b/pkg/tnf/handlers/scaling/scaling_hpa.go new file mode 100644 index 000000000..e72f171fa --- /dev/null +++ b/pkg/tnf/handlers/scaling/scaling_hpa.go @@ -0,0 +1,95 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package scaling + +import ( + "fmt" + "strings" + "time" + + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/identifier" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" +) + +const ( + hpaOcCommand = "oc patch hpa %s -p '{\"spec\":{\"minReplicas\": %d, \"maxReplicas\": %d}}' -n %s" + hpaRegex = "horizontalpodautoscaler.autoscaling/%s patched" +) + +// Scaling holds the Scaling handler parameters. +type HpAScaling struct { + result int + timeout time.Duration + args []string + regex string +} + +// NewScaling creates a new Scaling handler. +func NewHpaScaling(timeout time.Duration, namespace, hpaName string, min, max int) *HpAScaling { + command := fmt.Sprintf(hpaOcCommand, hpaName, min, max, namespace) + return &HpAScaling{ + timeout: timeout, + result: tnf.ERROR, + args: strings.Fields(command), + regex: fmt.Sprintf(hpaRegex, hpaName), + } +} + +// Args returns the command line args for the test. +func (hpascaling *HpAScaling) Args() []string { + return hpascaling.args +} + +// GetIdentifier returns the tnf.Test specific identifier. +func (hpascaling *HpAScaling) GetIdentifier() identifier.Identifier { + return identifier.ScalingIdentifier +} + +// Timeout returns the timeout in seconds for the test. +func (hpascaling *HpAScaling) Timeout() time.Duration { + return hpascaling.timeout +} + +// Result returns the test result. +func (hpascaling *HpAScaling) Result() int { + return hpascaling.result +} + +// ReelFirst returns a step which expects the scale command output within the test timeout. +func (hpascaling *HpAScaling) ReelFirst() *reel.Step { + return &reel.Step{ + Execute: "", + Expect: []string{hpascaling.regex}, + Timeout: hpascaling.timeout, + } +} + +// ReelMatch does nothing, just set the test result as success. +func (hpascaling *HpAScaling) ReelMatch(_, _, match string) *reel.Step { + hpascaling.result = tnf.SUCCESS + return nil +} + +// ReelTimeout does nothing; no action is necessary upon timeout. +func (hpascaling *HpAScaling) ReelTimeout() *reel.Step { + return nil +} + +// ReelEOF does nothing; no action is necessary upon EOF. +func (hpascaling *HpAScaling) ReelEOF() { +} diff --git a/pkg/tnf/handlers/scaling/scaling_hpa_test.go b/pkg/tnf/handlers/scaling/scaling_hpa_test.go new file mode 100644 index 000000000..5429f5f5f --- /dev/null +++ b/pkg/tnf/handlers/scaling/scaling_hpa_test.go @@ -0,0 +1,79 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package scaling_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/scaling" +) + +func Test_HpaNewScaling(t *testing.T) { + handler := scaling.NewHpaScaling(testTimeoutDuration, testPodNamespace, testHpaName, testMinReplicaCount, testMaxReplicaCount) + assert.NotNil(t, handler) + assert.Equal(t, testTimeoutDuration, handler.Timeout()) + assert.Equal(t, handler.Result(), tnf.ERROR) +} + +func Test_ReelHpaFirstPositive(t *testing.T) { + handler := scaling.NewHpaScaling(testTimeoutDuration, testPodNamespace, testHpaName, testMinReplicaCount, testMaxReplicaCount) + assert.NotNil(t, handler) + firstStep := handler.ReelFirst() + re := regexp.MustCompile(firstStep.Expect[0]) + + matches := re.FindStringSubmatch(testHpaInputSuccess) + assert.Len(t, matches, 1) +} + +func Test_ReelHpaFirstNegative(t *testing.T) { + handler := scaling.NewHpaScaling(testTimeoutDuration, testPodNamespace, testHpaName, testMinReplicaCount, testMaxReplicaCount) + assert.NotNil(t, handler) + firstStep := handler.ReelFirst() + re := regexp.MustCompile(firstStep.Expect[0]) + matches := re.FindStringSubmatch(testInputError) + assert.Len(t, matches, 0) +} + +func Test_ReelHpaMatchSuccess(t *testing.T) { + handler := scaling.NewHpaScaling(testTimeoutDuration, testPodNamespace, testHpaName, testMinReplicaCount, testMaxReplicaCount) + assert.NotNil(t, handler) + + step := handler.ReelMatch("", "", testHpaInputSuccess) + assert.Nil(t, step) + assert.Equal(t, tnf.SUCCESS, handler.Result()) +} + +// Just ensure there are no panics. +func Test_ReelHpaEof(t *testing.T) { + handler := scaling.NewHpaScaling(testTimeoutDuration, testPodNamespace, testHpaName, testMinReplicaCount, testMaxReplicaCount) + assert.NotNil(t, handler) + handler.ReelEOF() +} + +const ( + testMaxReplicaCount = 5 + testMinReplicaCount = 2 + testHpaName = "testHpaName" +) + +var ( + testHpaInputSuccess = fmt.Sprintf("horizontalpodautoscaler.autoscaling/%s patched\n", testHpaName) +) diff --git a/pkg/tnf/handlers/scaling/scaling_test.go b/pkg/tnf/handlers/scaling/scaling_test.go new file mode 100644 index 000000000..20bfaca99 --- /dev/null +++ b/pkg/tnf/handlers/scaling/scaling_test.go @@ -0,0 +1,83 @@ +// Copyright (C) 2021 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package scaling_test + +import ( + "fmt" + "regexp" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/scaling" +) + +func Test_NewScaling(t *testing.T) { + handler := scaling.NewScaling(testTimeoutDuration, testPodNamespace, testDeploymentName, sourcetype, testReplicaCount) + assert.NotNil(t, handler) + assert.Equal(t, testTimeoutDuration, handler.Timeout()) + assert.Equal(t, handler.Result(), tnf.ERROR) +} + +func Test_ReelFirstPositive(t *testing.T) { + handler := scaling.NewScaling(testTimeoutDuration, testPodNamespace, testDeploymentName, sourcetype, testReplicaCount) + assert.NotNil(t, handler) + firstStep := handler.ReelFirst() + re := regexp.MustCompile(firstStep.Expect[0]) + + matches := re.FindStringSubmatch(testInputSuccess) + assert.Len(t, matches, 1) +} + +func Test_ReelFirstNegative(t *testing.T) { + handler := scaling.NewScaling(testTimeoutDuration, testPodNamespace, testDeploymentName, sourcetype, testReplicaCount) + assert.NotNil(t, handler) + firstStep := handler.ReelFirst() + re := regexp.MustCompile(firstStep.Expect[0]) + matches := re.FindStringSubmatch(testInputError) + assert.Len(t, matches, 0) +} + +func Test_ReelMatchSuccess(t *testing.T) { + handler := scaling.NewScaling(testTimeoutDuration, testPodNamespace, testDeploymentName, sourcetype, testReplicaCount) + assert.NotNil(t, handler) + + step := handler.ReelMatch("", "", testInputSuccess) + assert.Nil(t, step) + assert.Equal(t, tnf.SUCCESS, handler.Result()) +} + +// Just ensure there are no panics. +func Test_ReelEof(t *testing.T) { + handler := scaling.NewScaling(testTimeoutDuration, testPodNamespace, testDeploymentName, sourcetype, testReplicaCount) + assert.NotNil(t, handler) + handler.ReelEOF() +} + +const ( + testTimeoutDuration = time.Second * 1 + testReplicaCount = 2 + testInputError = "" + testPodNamespace = "testPodNamespace" + testDeploymentName = "testDeploymentName" + sourcetype = "deployment" +) + +var ( + testInputSuccess = fmt.Sprintf("deployment.apps/%s scaled\n", testDeploymentName) +) diff --git a/pkg/tnf/handlers/serviceaccount/serviceaccount.go b/pkg/tnf/handlers/serviceaccount/serviceaccount.go deleted file mode 100644 index 6cb2e5c4e..000000000 --- a/pkg/tnf/handlers/serviceaccount/serviceaccount.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package serviceaccount - -import ( - "regexp" - "time" - - "github.com/test-network-function/test-network-function/pkg/tnf" - "github.com/test-network-function/test-network-function/pkg/tnf/identifier" - "github.com/test-network-function/test-network-function/pkg/tnf/reel" -) - -const ( - saRegex = " serviceAccountName: (.+)" -) - -// ServiceAccount holds information from extracting Service Account information from a Pod definition. -type ServiceAccount struct { - serviceAccountName string // Output variable for retrieving the result - result int - timeout time.Duration - args []string -} - -// NewServiceAccount creates a new ServiceAccount tnf.Test. -func NewServiceAccount(timeout time.Duration, podName, podNamespace string) *ServiceAccount { - return &ServiceAccount{ - timeout: timeout, - result: tnf.ERROR, - args: []string{"oc", "-n", podNamespace, "get", "pods", podName, "-o", "yaml"}, - } -} - -// Args returns the command line args for the test. -func (sa *ServiceAccount) Args() []string { - return sa.args -} - -// GetIdentifier returns the tnf.Test specific identifiesa. -func (sa *ServiceAccount) GetIdentifier() identifier.Identifier { - return identifier.ServiceAccountIdentifier -} - -// Timeout returns the timeout in seconds for the test. -func (sa *ServiceAccount) Timeout() time.Duration { - return sa.timeout -} - -// Result returns the test result. -func (sa *ServiceAccount) Result() int { - return sa.result -} - -// ReelFirst returns a step which expects the ping statistics within the test timeout. -func (sa *ServiceAccount) ReelFirst() *reel.Step { - return &reel.Step{ - Expect: []string{saRegex}, - Timeout: sa.timeout, - } -} - -// ReelMatch ensures that the correct number of ServiceAccount annotations exist, and stores the correct SA within -// the ServiceAccount struct for later retrieval. -func (sa *ServiceAccount) ReelMatch(_, _, match string) *reel.Step { - numExpectedMatches := 2 - saMatchIdx := 1 - re := regexp.MustCompile(saRegex) - matched := re.FindStringSubmatch(match) - if len(matched) < numExpectedMatches { - return nil - } - - sa.serviceAccountName = matched[saMatchIdx] - sa.result = tnf.SUCCESS - - return nil -} - -// ReelTimeout does nothing; no action is needed upon timeout. -func (sa *ServiceAccount) ReelTimeout() *reel.Step { - return nil -} - -// ReelEOF does nothing; no aciton is needed upon EOF. -func (sa *ServiceAccount) ReelEOF() { -} - -// GetServiceAccountName extracts the ServiceAccount (SA) for a Pod, if one exists. -func (sa *ServiceAccount) GetServiceAccountName() string { - return sa.serviceAccountName -} diff --git a/pkg/tnf/handlers/serviceaccount/serviceaccount_test.go b/pkg/tnf/handlers/serviceaccount/serviceaccount_test.go deleted file mode 100644 index 445278b10..000000000 --- a/pkg/tnf/handlers/serviceaccount/serviceaccount_test.go +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright (C) 2021 Red Hat, Inc. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -package serviceaccount_test - -import ( - "regexp" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/test-network-function/test-network-function/pkg/tnf" - sa "github.com/test-network-function/test-network-function/pkg/tnf/handlers/serviceaccount" -) - -func Test_NewServiceAccount(t *testing.T) { - newSa := sa.NewServiceAccount(testTimeoutDuration, testPodName, testPodNamespace) - assert.NotNil(t, newSa) - assert.Equal(t, testTimeoutDuration, newSa.Timeout()) - assert.Equal(t, newSa.Result(), tnf.ERROR) -} - -func Test_ReelFirstPositive(t *testing.T) { - newSa := sa.NewServiceAccount(testTimeoutDuration, testPodName, testPodNamespace) - assert.NotNil(t, newSa) - firstStep := newSa.ReelFirst() - re := regexp.MustCompile(firstStep.Expect[0]) - matches := re.FindStringSubmatch(testPodYaml) - assert.Len(t, matches, 2) - assert.Equal(t, "default", matches[1]) -} - -func Test_ReelFirstNegative(t *testing.T) { - const errorInput = "not really a yaml file\njust someinput for test\nok?" - newSa := sa.NewServiceAccount(testTimeoutDuration, testPodName, testPodNamespace) - assert.NotNil(t, newSa) - firstStep := newSa.ReelFirst() - re := regexp.MustCompile(firstStep.Expect[0]) - matches := re.FindStringSubmatch(errorInput) - assert.Len(t, matches, 0) -} - -func Test_ReelMatch(t *testing.T) { - // Prepare input for ReelMatch - newSa := sa.NewServiceAccount(testTimeoutDuration, testPodName, testPodNamespace) - assert.NotNil(t, newSa) - firstStep := newSa.ReelFirst() - re := regexp.MustCompile(firstStep.Expect[0]) - matches := re.FindStringSubmatch(testPodYaml) - assert.Len(t, matches, 2) - assert.Equal(t, "default", matches[1]) - - // Call ReelMatch - step := newSa.ReelMatch("", "", matches[0]) - assert.Nil(t, step) - assert.Equal(t, tnf.SUCCESS, newSa.Result()) - assert.Equal(t, "default", newSa.GetServiceAccountName()) -} - -// Just ensure there are no panics. -func Test_ReelEof(t *testing.T) { - newSa := sa.NewServiceAccount(testTimeoutDuration, testPodName, testPodNamespace) - assert.NotNil(t, newSa) - newSa.ReelEOF() -} - -const ( - testTimeoutDuration = time.Second * 2 - testPodName = "testPod" - testPodNamespace = "testPodNamespace" - testPodYaml = `apiVersion: v1 - kind: Pod - metadata: - annotations: - k8s.v1.cni.cncf.io/network-status: |- - [{ - "name": "openshift-sdn", - "interface": "eth0", - "ips": [ - "10.116.0.17" - ], - "default": true, - "dns": {} - }] - k8s.v1.cni.cncf.io/networks-status: |- - [{ - "name": "openshift-sdn", - "interface": "eth0", - "ips": [ - "10.116.0.17" - ], - "default": true, - "dns": {} - }] - creationTimestamp: "2021-03-16T14:48:39Z" - labels: - app: test - managedFields: - - apiVersion: v1 - fieldsType: FieldsV1 - fieldsV1: - f:metadata: - f:labels: - .: {} - f:app: {} - f:spec: - f:containers: - k:{"name":"test"}: - .: {} - f:command: {} - f:image: {} - f:imagePullPolicy: {} - f:name: {} - f:resources: - .: {} - f:limits: - .: {} - f:cpu: {} - f:memory: {} - f:requests: - .: {} - f:cpu: {} - f:memory: {} - f:terminationMessagePath: {} - f:terminationMessagePolicy: {} - f:dnsPolicy: {} - f:enableServiceLinks: {} - f:restartPolicy: {} - f:schedulerName: {} - f:securityContext: {} - f:terminationGracePeriodSeconds: {} - manager: oc - operation: Update - time: "2021-03-16T14:48:39Z" - - apiVersion: v1 - fieldsType: FieldsV1 - fieldsV1: - f:metadata: - f:annotations: - .: {} - f:k8s.v1.cni.cncf.io/network-status: {} - f:k8s.v1.cni.cncf.io/networks-status: {} - manager: multus - operation: Update - time: "2021-04-04T10:10:10Z" - - apiVersion: v1 - fieldsType: FieldsV1 - fieldsV1: - f:status: - f:conditions: - k:{"type":"ContainersReady"}: - .: {} - f:lastProbeTime: {} - f:lastTransitionTime: {} - f:status: {} - f:type: {} - k:{"type":"Initialized"}: - .: {} - f:lastProbeTime: {} - f:lastTransitionTime: {} - f:status: {} - f:type: {} - k:{"type":"Ready"}: - .: {} - f:lastProbeTime: {} - f:lastTransitionTime: {} - f:status: {} - f:type: {} - f:containerStatuses: {} - f:hostIP: {} - f:phase: {} - f:podIP: {} - f:podIPs: - .: {} - k:{"ip":"10.116.0.17"}: - .: {} - f:ip: {} - f:startTime: {} - manager: kubelet - operation: Update - time: "2021-04-04T10:10:23Z" - name: test - namespace: default - resourceVersion: "14788662" - selfLink: /api/v1/namespaces/default/pods/test - uid: f5967af2-e838-4fc9-a9f2-b7aa7fd40340 - spec: - containers: - - command: - - tail - - -f - - /dev/null - image: quay.io/testnetworkfunction/cnf-test-partner:latest - imagePullPolicy: Always - name: test - resources: - limits: - cpu: 250m - memory: 512Mi - requests: - cpu: 250m - memory: 512Mi - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /var/run/secrets/kubernetes.io/serviceaccount - name: default-token-zhsqz - readOnly: true - dnsPolicy: ClusterFirst - enableServiceLinks: true - imagePullSecrets: - - name: default-dockercfg-mt7k6 - nodeName: crc-mk4pg-master-0 - priority: 0 - restartPolicy: Always - schedulerName: default-scheduler - securityContext: {} - serviceAccount: default - serviceAccountName: default - terminationGracePeriodSeconds: 30 - tolerations: - - effect: NoExecute - key: node.kubernetes.io/not-ready - operator: Exists - tolerationSeconds: 300 - - effect: NoExecute - key: node.kubernetes.io/unreachable - operator: Exists - tolerationSeconds: 300 - - effect: NoSchedule - key: node.kubernetes.io/memory-pressure - operator: Exists - volumes: - - name: default-token-zhsqz - secret: - defaultMode: 420 - secretName: default-token-zhsqz - status: - conditions: - - lastProbeTime: null - lastTransitionTime: "2021-03-16T14:48:39Z" - status: "True" - type: Initialized - - lastProbeTime: null - lastTransitionTime: "2021-04-04T10:10:23Z" - status: "True" - type: Ready - - lastProbeTime: null - lastTransitionTime: "2021-04-04T10:10:23Z" - status: "True" - type: ContainersReady - - lastProbeTime: null - lastTransitionTime: "2021-03-16T14:48:39Z" - status: "True" - type: PodScheduled - containerStatuses: - - containerID: cri-o://1c8463f7b2cac4aeb5d60048f226cc76c7eb5d7ba1428dcbcdbfbd748eac143d - image: quay.io/testnetworkfunction/cnf-test-partner:latest - imageID: quay.io/testnetworkfunction/cnf-test-partner@sha256:e117af66264c5e6db9effb5ebe1b4c79893bc44f281676446553e98eb4041efe - lastState: {} - name: test - ready: true - restartCount: 0 - started: true - state: - running: - startedAt: "2021-04-04T10:10:22Z" - hostIP: 192.168.126.11 - phase: Running - podIP: 10.116.0.17 - podIPs: - - ip: 10.116.0.17 - qosClass: Guaranteed - startTime: "2021-03-16T14:48:39Z"` -) diff --git a/pkg/tnf/handlers/sysctlallconfigsargs/sysctlallconfigsargs.go b/pkg/tnf/handlers/sysctlallconfigsargs/sysctlallconfigsargs.go index bbe11d991..d892072f8 100644 --- a/pkg/tnf/handlers/sysctlallconfigsargs/sysctlallconfigsargs.go +++ b/pkg/tnf/handlers/sysctlallconfigsargs/sysctlallconfigsargs.go @@ -38,12 +38,12 @@ type SysctlAllConfigsArgs struct { } // NewSysctlAllConfigsArgs creates a SysctlAllConfigsArgs tnf.Test. -func NewSysctlAllConfigsArgs(timeout time.Duration, nodeName string) *SysctlAllConfigsArgs { +func NewSysctlAllConfigsArgs(timeout time.Duration) *SysctlAllConfigsArgs { return &SysctlAllConfigsArgs{ timeout: timeout, result: tnf.ERROR, args: []string{ - "echo", "\"sysctl --system\"", "|", "oc", "debug", "-q", "node/" + nodeName, + "sysctl --system", }, } } diff --git a/pkg/tnf/handlers/sysctlallconfigsargs/sysctlallconfigsargs_test.go b/pkg/tnf/handlers/sysctlallconfigsargs/sysctlallconfigsargs_test.go index 6b774b96d..9651f478a 100644 --- a/pkg/tnf/handlers/sysctlallconfigsargs/sysctlallconfigsargs_test.go +++ b/pkg/tnf/handlers/sysctlallconfigsargs/sysctlallconfigsargs_test.go @@ -27,13 +27,13 @@ import ( ) func TestNewSysctlAllConfigsArgs(t *testing.T) { - newSysctlAllConfigsArgs := sysctlallconfigsargs.NewSysctlAllConfigsArgs(testTimeoutDuration, testNodeName) + newSysctlAllConfigsArgs := sysctlallconfigsargs.NewSysctlAllConfigsArgs(testTimeoutDuration) assert.NotNil(t, newSysctlAllConfigsArgs) assert.Equal(t, tnf.ERROR, newSysctlAllConfigsArgs.Result()) } func Test_ReelFirst(t *testing.T) { - newSysctlAllConfigsArgs := sysctlallconfigsargs.NewSysctlAllConfigsArgs(testTimeoutDuration, testNodeName) + newSysctlAllConfigsArgs := sysctlallconfigsargs.NewSysctlAllConfigsArgs(testTimeoutDuration) assert.NotNil(t, newSysctlAllConfigsArgs) firstStep := newSysctlAllConfigsArgs.ReelFirst() re := regexp.MustCompile(firstStep.Expect[0]) @@ -43,7 +43,7 @@ func Test_ReelFirst(t *testing.T) { } func Test_ReelMatch(t *testing.T) { - newSysctlAllConfigsArgs := sysctlallconfigsargs.NewSysctlAllConfigsArgs(testTimeoutDuration, testNodeName) + newSysctlAllConfigsArgs := sysctlallconfigsargs.NewSysctlAllConfigsArgs(testTimeoutDuration) assert.NotNil(t, newSysctlAllConfigsArgs) step := newSysctlAllConfigsArgs.ReelMatch("", "", testInput) assert.Nil(t, step) @@ -52,7 +52,7 @@ func Test_ReelMatch(t *testing.T) { // Just ensure there are no panics. func Test_ReelEof(t *testing.T) { - newSysctlAllConfigsArgs := sysctlallconfigsargs.NewSysctlAllConfigsArgs(testTimeoutDuration, testNodeName) + newSysctlAllConfigsArgs := sysctlallconfigsargs.NewSysctlAllConfigsArgs(testTimeoutDuration) assert.NotNil(t, newSysctlAllConfigsArgs) newSysctlAllConfigsArgs.ReelEOF() } @@ -88,5 +88,4 @@ const ( fs.inotify.max_user_instances = 8192 * Applying /etc/sysctl.conf ... ` - testNodeName = "crc-l6qvn-master-0" ) diff --git a/pkg/tnf/identifier/doc.go b/pkg/tnf/identifier/doc.go index 0b513dedc..8cfee8790 100644 --- a/pkg/tnf/identifier/doc.go +++ b/pkg/tnf/identifier/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/identifier/identifier.go b/pkg/tnf/identifier/identifier.go index daae5a9b9..d353ba675 100644 --- a/pkg/tnf/identifier/identifier.go +++ b/pkg/tnf/identifier/identifier.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "net/url" + "strings" "github.com/Masterminds/semver/v3" ) @@ -29,6 +30,8 @@ const ( semanticVersionKey = "version" // urlKey is the JSON key representing a URL payload. urlKey = "url" + + urlTests = "http://test-network-function.com/tests" ) // Identifier is a per tnf.Test unique identifier. @@ -79,13 +82,25 @@ func (i *Identifier) UnmarshalJSON(b []byte) error { return err } - if err := i.unmarshalURL(objMap); err != nil { + if err = i.unmarshalURL(objMap); err != nil { return err } - if err := i.unmarshalSemanticVersion(objMap); err != nil { - return err + return i.unmarshalSemanticVersion(objMap) +} + +// GetShortNameFromIdentifier transform an Identifier into a just test name +// returns empty string if Identifier.URL was not correct about the base domain +// for the url +func GetShortNameFromIdentifier(identifier Identifier) string { + if !strings.HasPrefix(identifier.URL, urlTests+"/") { + return "" } + itID := strings.ReplaceAll(strings.TrimPrefix(identifier.URL, urlTests+"/"), "/", "-") + + return itID +} - return nil +func GetIdentifierURLBaseDomain() string { + return urlTests } diff --git a/pkg/tnf/identifier/identifier_test.go b/pkg/tnf/identifier/identifier_test.go index 5e8f9e5a4..571f0e9cd 100644 --- a/pkg/tnf/identifier/identifier_test.go +++ b/pkg/tnf/identifier/identifier_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,7 +18,7 @@ package identifier_test import ( "encoding/json" - "io/ioutil" + "os" "path" "testing" @@ -69,7 +69,7 @@ func getTestFile(testName string) string { func TestIdentifier_UnmarshalJSON(t *testing.T) { for testName, testCase := range testIdentifierUnmarshalJSONTestCases { testFile := getTestFile(testName) - contents, err := ioutil.ReadFile(testFile) + contents, err := os.ReadFile(testFile) assert.Nil(t, err) assert.NotNil(t, contents) @@ -83,3 +83,38 @@ func TestIdentifier_UnmarshalJSON(t *testing.T) { } } } + +func TestGetShortNameFromIdentifier(t *testing.T) { + type testURLTestName struct { + URL string + testName string + expectedResult string + } + testsURLs := []testURLTestName{ + { + URL: identifier.GetIdentifierURLBaseDomain() + "/command", + testName: "command", + expectedResult: "command", + }, + { + URL: identifier.GetIdentifierURLBaseDomain() + "/whatever", + testName: "whatever", + expectedResult: "whatever", + }, + { + URL: "http://test-network-function.org/tests" + "/command", + testName: "command", + expectedResult: "", + }, + { + URL: "http://test-network-function.es/functional-tests" + "/whatever", + testName: "whatever", + expectedResult: "", + }, + } + + for _, test := range testsURLs { + id := identifier.Identifier{URL: test.URL, SemanticVersion: ""} + assert.Equal(t, test.expectedResult, identifier.GetShortNameFromIdentifier(id)) + } +} diff --git a/pkg/tnf/identifier/identifiers.go b/pkg/tnf/identifier/identifiers.go index 1baf1821e..643948f13 100644 --- a/pkg/tnf/identifier/identifiers.go +++ b/pkg/tnf/identifier/identifiers.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -16,53 +16,60 @@ package identifier -import "github.com/test-network-function/test-network-function/pkg/tnf/dependencies" +import ( + "github.com/test-network-function/test-network-function/pkg/tnf/dependencies" +) const ( - nodeselectorIdentifierURL = "http://test-network-function.com/tests/nodeselector" - hostnameIdentifierURL = "http://test-network-function.com/tests/hostname" - ipAddrIdentifierURL = "http://test-network-function.com/tests/ipaddr" - nodesIdentifierURL = "http://test-network-function.com/tests/nodes" - operatorIdentifierURL = "http://test-network-function.com/tests/operator" - pingIdentifierURL = "http://test-network-function.com/tests/ping" - podIdentifierURL = "http://test-network-function.com/tests/container/pod" - versionIdentifierURL = "http://test-network-function.com/tests/generic/version" - containerIDURL = "http://test-network-function.com/tests/generic/containerId" - serviceAccountIdentifierURL = "http://test-network-function.com/tests/serviceaccount" - roleBindingIdentifierURL = "http://test-network-function.com/tests/rolebinding" - clusterRoleBindingIdentifierURL = "http://test-network-function.com/tests/clusterrolebinding" - nodePortIdentifierURL = "http://test-network-function.com/tests/nodeport" - nodeNamesIdentifierURL = "http://test-network-function.com/tests/nodenames" - nodeTaintedIdentifierURL = "http://test-network-function.com/tests/nodetainted" - gracePeriodIdentifierURL = "http://test-network-function.com/tests/gracePeriod" - hugepagesIdentifierURL = "http://test-network-function.com/tests/hugepages" - nodehugepagesIdentifierURL = "http://test-network-function.com/tests/nodehugepages" - deploymentsIdentifierURL = "http://test-network-function.com/tests/deployments" - deploymentsnodesIdentifierURL = "http://test-network-function.com/tests/deploymentsnodes" - deploymentsdrainIdentifierURL = "http://test-network-function.com/tests/deploymentsdrain" - ownersIdentifierURL = "http://test-network-function.com/tests/owners" - cnfFsDiffURL = "http://test-network-function.com/tests/generic/cnf_fs_diff" - podnodenameIdentifierURL = "http://test-network-function.com/tests/podnodename" - nodemcnameIdentifierURL = "http://test-network-function.com/tests/nodemcname" - mckernelargumentsIdentifierURL = "http://test-network-function.com/tests/mckernelarguments" - currentKernelCmdlineArgsIdentifierURL = "http://test-network-function.com/tests/currentKernelCmdlineArgs" - grubKernelCmdlineArgsIdentifierURL = "http://test-network-function.com/tests/grubKernelCmdlineArgs" - sysctlConfigFilesListIdentifierURL = "http://test-network-function.com/tests/sysctlConfigFilesList" - readRemoteFileIdentifierURL = "http://test-network-function.com/tests/readRemoteFile" - uncordonNodeIdentifierURL = "http://test-network-function.com/tests/node/uncordon" - checkSubscriptionIdentifierURL = "http://test-network-function.com/tests/operator/check-subscription" - nodeDebugIdentifierURL = "http://test-network-function.com/tests/nodedebug" - loggingIdentifierURL = "http://test-network-function.com/tests/logging" - podantiaffinityIdentifierURL = "http://test-network-function.com/tests/testPodHighAvailability" - shutdownIdentifierURL = "http://test-network-function.com/tests/shutdown" - - versionOne = "v1.0.0" + commandIdentifierURL = urlTests + "/command" + nodeselectorIdentifierURL = urlTests + "/nodeselector" + ipAddrIdentifierURL = urlTests + "/ipaddr" + nodesIdentifierURL = urlTests + "/nodes" + operatorIdentifierURL = urlTests + "/operator" + pingIdentifierURL = urlTests + "/ping" + podIdentifierURL = urlTests + "/container/pod" + versionIdentifierURL = urlTests + "/generic/version" + roleBindingIdentifierURL = urlTests + "/rolebinding" + clusterRoleBindingIdentifierURL = urlTests + "/clusterrolebinding" + nodePortIdentifierURL = urlTests + "/nodeport" + ImagePullPolicyIdentifierURL = urlTests + "/imagepullpolicy" + nodeNamesIdentifierURL = urlTests + "/nodenames" + nodeTaintedIdentifierURL = urlTests + "/nodetainted" + gracePeriodIdentifierURL = urlTests + "/gracePeriod" + podsetsIdentifierURL = urlTests + "/podsets" + deploymentsnodesIdentifierURL = urlTests + "/deploymentsnodes" + deploymentsdrainIdentifierURL = urlTests + "/deploymentsdrain" + ownersIdentifierURL = urlTests + "/owners" + cnfFsDiffURL = urlTests + "/generic/cnf_fs_diff" + podnodenameIdentifierURL = urlTests + "/podnodename" + nodemcnameIdentifierURL = urlTests + "/nodemcname" + mckernelargumentsIdentifierURL = urlTests + "/mckernelarguments" + currentKernelCmdlineArgsIdentifierURL = urlTests + "/currentKernelCmdlineArgs" + grubKernelCmdlineArgsIdentifierURL = urlTests + "/grubKernelCmdlineArgs" + sysctlConfigFilesListIdentifierURL = urlTests + "/sysctlConfigFilesList" + sysctlAllConfigsArgsURL = urlTests + "/sysctlAllConfigsArgs" + uncordonNodeIdentifierURL = urlTests + "/node/uncordon" + checkSubscriptionIdentifierURL = urlTests + "/operator/check-subscription" + nodeDebugIdentifierURL = urlTests + "/nodedebug" + loggingIdentifierURL = urlTests + "/logging" + podantiaffinityIdentifierURL = urlTests + "/testPodHighAvailability" + shutdownIdentifierURL = urlTests + "/shutdown" + livenessIdentifierURL = urlTests + "/liveness" + readinessIdentifierURL = urlTests + "/readiness" + scalingIdentifierURL = urlTests + "/scaling" + csiDriverIdentifierURL = urlTests + "/csiDriver" + clusterVersionIdentifierURL = urlTests + "/clusterVersion" + crdStatusExistenceIdentifierURL = urlTests + "/crdStatusExistence" + daemonSetIdentifierURL = urlTests + "/daemonset" + automountserviceIdentifierURL = urlTests + "/automountservice" + versionOne = "v1.0.0" ) const ( // Normative is the test type used for a test that returns normative results. Normative = "normative" - // TODO: Informative = "informative" once we have informative tests. + // Informative is the test type used for a test that returns informative results. + Informative = "informative" ) // TestCatalogEntry is a container for required test facets. @@ -97,17 +104,15 @@ type IntrusionSettings struct { // Catalog is the test catalog. var Catalog = map[string]TestCatalogEntry{ - hostnameIdentifierURL: { - Identifier: HostnameIdentifier, - Description: "A generic test used to check the hostname of a target machine/container.", + commandIdentifierURL: { + Identifier: CommandIdentifier, + Description: "A generic test used with any command and would match any output. The caller is responsible for interpreting the output and extracting data from it.", Type: Normative, IntrusionSettings: IntrusionSettings{ ModifiesSystem: false, ModificationIsPersistent: false, }, - BinaryDependencies: []string{ - dependencies.HostnameBinaryName, - }, + BinaryDependencies: []string{}, }, ipAddrIdentifierURL: { Identifier: IPAddrIdentifier, @@ -198,31 +203,6 @@ var Catalog = map[string]TestCatalogEntry{ dependencies.CutBinaryName, }, }, - serviceAccountIdentifierURL: { - Identifier: ServiceAccountIdentifier, - Description: "A generic test used to extract the CNF pod's ServiceAccount name.", - Type: Normative, - IntrusionSettings: IntrusionSettings{ - ModifiesSystem: false, - ModificationIsPersistent: false, - }, - BinaryDependencies: []string{ - dependencies.GrepBinaryName, - dependencies.CutBinaryName, - }, - }, - containerIDURL: { - Identifier: ContainerIDIdentifier, - Description: "A test used to check what is the id of the crio generated container this command is run from", - Type: Normative, - IntrusionSettings: IntrusionSettings{ - ModifiesSystem: false, - ModificationIsPersistent: false, - }, - BinaryDependencies: []string{ - dependencies.CatBinaryName, - }, - }, roleBindingIdentifierURL: { Identifier: RoleBindingIdentifier, Description: "A generic test used to test RoleBindings of CNF pod's ServiceAccount.", @@ -261,6 +241,18 @@ var Catalog = map[string]TestCatalogEntry{ dependencies.GrepBinaryName, }, }, + ImagePullPolicyIdentifierURL: { + Identifier: ImagePullPolicyIdentifier, + Description: "A generic test used to get Image Pull Policy type.", + Type: Normative, + IntrusionSettings: IntrusionSettings{ + ModifiesSystem: false, + ModificationIsPersistent: false, + }, + BinaryDependencies: []string{ + dependencies.OcBinaryName, + }, + }, nodeNamesIdentifierURL: { Identifier: NodeNamesIdentifier, Description: "A generic test used to get node names", @@ -276,7 +268,7 @@ var Catalog = map[string]TestCatalogEntry{ nodeTaintedIdentifierURL: { Identifier: NodeTaintedIdentifier, Description: "A generic test used to test whether node is tainted", - Type: Normative, + Type: Informative, IntrusionSettings: IntrusionSettings{ ModifiesSystem: false, ModificationIsPersistent: false, @@ -300,37 +292,9 @@ var Catalog = map[string]TestCatalogEntry{ dependencies.CutBinaryName, }, }, - hugepagesIdentifierURL: { - Identifier: HugepagesIdentifier, - Description: "A generic test used to read cluster's hugepages configuration", - Type: Normative, - IntrusionSettings: IntrusionSettings{ - ModifiesSystem: false, - ModificationIsPersistent: false, - }, - BinaryDependencies: []string{ - dependencies.GrepBinaryName, - dependencies.CutBinaryName, - dependencies.OcBinaryName, - dependencies.GrepBinaryName, - }, - }, - nodehugepagesIdentifierURL: { - Identifier: NodeHugepagesIdentifier, - Description: "A generic test used to verify a node's hugepages configuration", - Type: Normative, - IntrusionSettings: IntrusionSettings{ - ModifiesSystem: false, - ModificationIsPersistent: false, - }, - BinaryDependencies: []string{ - dependencies.OcBinaryName, - dependencies.GrepBinaryName, - }, - }, - deploymentsIdentifierURL: { - Identifier: DeploymentsIdentifier, - Description: "A generic test used to read namespace's deployments", + podsetsIdentifierURL: { + Identifier: PodSetsIdentifier, + Description: "A generic test used to read namespace's deployments/statefulsets", Type: Normative, IntrusionSettings: IntrusionSettings{ ModifiesSystem: false, @@ -420,7 +384,7 @@ var Catalog = map[string]TestCatalogEntry{ }, ownersIdentifierURL: { Identifier: OwnersIdentifier, - Description: "A generic test used to verify pod is managed by a ReplicaSet", + Description: "A generic test used to verify pod is managed by a ReplicaSet/StatefulSet", Type: Normative, IntrusionSettings: IntrusionSettings{ ModifiesSystem: false, @@ -470,18 +434,6 @@ var Catalog = map[string]TestCatalogEntry{ dependencies.CatBinaryName, }, }, - readRemoteFileIdentifierURL: { - Identifier: ReadRemoteFileURLIdentifier, - Description: "A generic test used to read a specified file at a specified node", - Type: Normative, - IntrusionSettings: IntrusionSettings{ - ModifiesSystem: false, - ModificationIsPersistent: false, - }, - BinaryDependencies: []string{ - dependencies.EchoBinaryName, - }, - }, uncordonNodeIdentifierURL: { Identifier: UncordonNodeURLIdentifier, Description: "A generic test used to uncordon a node", @@ -556,11 +508,101 @@ var Catalog = map[string]TestCatalogEntry{ dependencies.OcBinaryName, }, }, + sysctlAllConfigsArgsURL: { + Identifier: SysctlAllConfigsArgsIdentifier, + Description: "A test used to find all sysctl configuration args", + Type: Normative, + IntrusionSettings: IntrusionSettings{ + ModifiesSystem: false, + ModificationIsPersistent: false, + }, + BinaryDependencies: []string{ + dependencies.SysctlBinaryName, + }, + }, + scalingIdentifierURL: { + Identifier: ScalingIdentifier, + Description: "A test to check the deployments scale in/out. The tests issues the oc scale " + + "command on a deployment for a given number of replicas and checks whether the command output " + + "is valid.", + Type: Normative, + IntrusionSettings: IntrusionSettings{ + ModifiesSystem: true, + ModificationIsPersistent: false, + }, + BinaryDependencies: []string{ + dependencies.OcBinaryName, + }, + }, + csiDriverIdentifierURL: { + Identifier: CSIDriverIdentifier, + Description: "extracts the csi driver info in the cluster", + Type: Normative, + IntrusionSettings: IntrusionSettings{ + ModifiesSystem: false, + ModificationIsPersistent: false, + }, + BinaryDependencies: []string{ + dependencies.OcBinaryName, + }, + }, + clusterVersionIdentifierURL: { + Identifier: ClusterVersionIdentifier, + Description: "Extracts OCP versions from the cluster", + Type: Normative, + IntrusionSettings: IntrusionSettings{ + ModifiesSystem: false, + ModificationIsPersistent: false, + }, + BinaryDependencies: []string{ + dependencies.OcBinaryName, + }, + }, + crdStatusExistenceIdentifierURL: { + Identifier: CrdStatusExistenceIdentifier, + Description: "Checks whether a give CRD has status subresource specification.", + Type: Normative, + IntrusionSettings: IntrusionSettings{ + ModifiesSystem: false, + ModificationIsPersistent: false, + }, + BinaryDependencies: []string{ + dependencies.OcBinaryName, + dependencies.JqBinaryName, + }, + }, + daemonSetIdentifierURL: { + Identifier: DaemonSetIdentifier, + Description: "check whether a given daemonset was deployed successfully", + Type: Normative, + IntrusionSettings: IntrusionSettings{ + ModifiesSystem: false, + ModificationIsPersistent: false, + }, + BinaryDependencies: []string{ + dependencies.OcBinaryName, + }, + }, + automountserviceIdentifierURL: { + Identifier: AutomountServiceIdentifier, + Description: "check if automount service account token is set to false", + Type: Normative, + IntrusionSettings: IntrusionSettings{ + ModifiesSystem: false, + ModificationIsPersistent: false, + }, + BinaryDependencies: []string{ + dependencies.OcBinaryName, + }, + }, } -// HostnameIdentifier is the Identifier used to represent the generic hostname test case. -var HostnameIdentifier = Identifier{ - URL: hostnameIdentifierURL, +// TestIDBaseDomain is the BaseDomain for the IDs of test cases building blocks +var TestIDBaseDomain = urlTests + +// CommandIdentifier is the Identifier used to represent the generic command test case. +var CommandIdentifier = Identifier{ + URL: commandIdentifierURL, SemanticVersion: versionOne, } @@ -606,18 +648,6 @@ var CnfFsDiffIdentifier = Identifier{ SemanticVersion: versionOne, } -// ContainerIDIdentifier is the Identifier used to represent the generic cnf_fs_diff test. -var ContainerIDIdentifier = Identifier{ - URL: containerIDURL, - SemanticVersion: versionOne, -} - -// ServiceAccountIdentifier is the Identifier used to represent the generic serviceAccount test. -var ServiceAccountIdentifier = Identifier{ - URL: serviceAccountIdentifierURL, - SemanticVersion: versionOne, -} - // RoleBindingIdentifier is the Identifier used to represent the generic roleBinding test. var RoleBindingIdentifier = Identifier{ URL: roleBindingIdentifierURL, @@ -636,6 +666,11 @@ var NodePortIdentifier = Identifier{ SemanticVersion: versionOne, } +var ImagePullPolicyIdentifier = Identifier{ + URL: ImagePullPolicyIdentifierURL, + SemanticVersion: versionOne, +} + // NodeNamesIdentifier is the Identifier used to represent the generic NodeNames test. var NodeNamesIdentifier = Identifier{ URL: nodeNamesIdentifierURL, @@ -654,21 +689,9 @@ var GracePeriodIdentifier = Identifier{ SemanticVersion: versionOne, } -// HugepagesIdentifier is the Identifier used to represent the generic Hugepages test. -var HugepagesIdentifier = Identifier{ - URL: hugepagesIdentifierURL, - SemanticVersion: versionOne, -} - -// NodeHugepagesIdentifier is the Identifier used to represent the generic NodeHugepages test. -var NodeHugepagesIdentifier = Identifier{ - URL: nodehugepagesIdentifierURL, - SemanticVersion: versionOne, -} - -// DeploymentsIdentifier is the Identifier used to represent the generic Deployments test. -var DeploymentsIdentifier = Identifier{ - URL: deploymentsIdentifierURL, +// PodSetsIdentifier is the Identifier used to represent the generic PodSets test. +var PodSetsIdentifier = Identifier{ + URL: podsetsIdentifierURL, SemanticVersion: versionOne, } @@ -692,7 +715,7 @@ var OwnersIdentifier = Identifier{ // NodeSelectorIdentifier is the Identifier used to represent the generic NodeSelector test. var NodeSelectorIdentifier = Identifier{ - URL: nodehugepagesIdentifierURL, + URL: nodeselectorIdentifierURL, SemanticVersion: versionOne, } @@ -732,12 +755,6 @@ var SysctlConfigFilesListURLIdentifier = Identifier{ SemanticVersion: versionOne, } -// ReadRemoteFileURLIdentifier is the Identifier used to represent the generic getCurrentKernelCmdlineArgs test. -var ReadRemoteFileURLIdentifier = Identifier{ - URL: readRemoteFileIdentifierURL, - SemanticVersion: versionOne, -} - // UncordonNodeURLIdentifier is the Identifier used to represent a test that uncordons a node. var UncordonNodeURLIdentifier = Identifier{ URL: uncordonNodeIdentifierURL, @@ -773,3 +790,55 @@ var ShutdownURLIdentifier = Identifier{ URL: shutdownIdentifierURL, SemanticVersion: versionOne, } + +// LivenessURLIdentifier is the Identifier used to represent a test that checks if liveness is defined +var LivenessURLIdentifier = Identifier{ + URL: livenessIdentifierURL, + SemanticVersion: versionOne, +} + +// ReadinessURLIdentifier is the Identifier used to represent a test that checks if readiness is defined +var ReadinessURLIdentifier = Identifier{ + URL: readinessIdentifierURL, + SemanticVersion: versionOne, +} + +// SysctlAllConfigsArgsIdentifier is the Identifier used to represent a test that checks all args in all sysctl conf files ordered +// in the same way as they are loaded by the os +var SysctlAllConfigsArgsIdentifier = Identifier{ + URL: sysctlAllConfigsArgsURL, + SemanticVersion: versionOne, +} + +// ScalingIdentifier is the Identifier used to represent a test that checks deployments scale in/out +var ScalingIdentifier = Identifier{ + URL: scalingIdentifierURL, + SemanticVersion: versionOne, +} + +// CSIDriverIdentifier is the Identifier used to represent the CSI driver test case. +var CSIDriverIdentifier = Identifier{ + URL: csiDriverIdentifierURL, + SemanticVersion: versionOne, +} + +// ClusterVersionIdentifier is the Identifier used to represent the OCP versions test case. +var ClusterVersionIdentifier = Identifier{ + URL: clusterVersionIdentifierURL, + SemanticVersion: versionOne, +} + +// CrdStatusExistenceIdentifier is the Identifier used to represent the generic test for CRD status spec existence. +var CrdStatusExistenceIdentifier = Identifier{ + URL: crdStatusExistenceIdentifierURL, + SemanticVersion: versionOne, +} + +var DaemonSetIdentifier = Identifier{ + URL: daemonSetIdentifierURL, + SemanticVersion: versionOne, +} +var AutomountServiceIdentifier = Identifier{ + URL: automountserviceIdentifierURL, + SemanticVersion: versionOne, +} diff --git a/pkg/tnf/interactive/doc.go b/pkg/tnf/interactive/doc.go index 6da282850..2d4185cc2 100644 --- a/pkg/tnf/interactive/doc.go +++ b/pkg/tnf/interactive/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/interactive/oc.go b/pkg/tnf/interactive/oc.go index ad2416aee..3739c8d7f 100644 --- a/pkg/tnf/interactive/oc.go +++ b/pkg/tnf/interactive/oc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -20,47 +20,46 @@ import ( "time" expect "github.com/google/goexpect" + log "github.com/sirupsen/logrus" ) const ( - ocClientCommandSeparator = "--" - ocCommand = "oc" - ocContainerArg = "-c" - ocDefaultShell = "sh" - ocExecCommand = "exec" - ocNamespaceArg = "-n" - ocInteractiveArg = "-it" + ocCommand = "oc" + ocContainerArg = "-c" + ocRsh = "rsh" + ocNamespaceArg = "-n" ) // Oc provides an OpenShift Client designed to wrap the "oc" CLI. type Oc struct { // name of the pod pod string + // node set to true means the sessions is node session // name of the container container string // namespace of the pod namespace string // timeout for commands run in expecter timeout time.Duration - // options for experter, such as expect.Verbose(true) + // options for expecter, such as expect.Verbose(true) opts []Option - // the underlying subprocess implementation, tailored to OpenShift Client - expecter *expect.Expecter // error during the spawn process spawnErr error - // error channel for interactive error stream - errorChannel <-chan error + // interactive context (expector and error channel) + *Context + // done channel to notify the go routine that monitors the error channel + doneChannel chan bool } // SpawnOc creates an OpenShift Client subprocess, spawning the appropriate underlying PTY. func SpawnOc(spawner *Spawner, pod, container, namespace string, timeout time.Duration, opts ...Option) (*Oc, <-chan error, error) { - ocArgs := []string{ocExecCommand, ocNamespaceArg, namespace, ocInteractiveArg, pod, ocContainerArg, container, ocClientCommandSeparator, ocDefaultShell} + ocArgs := []string{ocRsh, ocNamespaceArg, namespace, ocContainerArg, container, pod} context, err := (*spawner).Spawn(ocCommand, ocArgs, timeout, opts...) if err != nil { return nil, context.GetErrorChannel(), err } errorChannel := context.GetErrorChannel() - return &Oc{pod: pod, container: container, namespace: namespace, timeout: timeout, opts: opts, expecter: context.GetExpecter(), spawnErr: err, errorChannel: errorChannel}, errorChannel, nil + return &Oc{pod: pod, container: container, namespace: namespace, timeout: timeout, opts: opts, spawnErr: err, Context: context, doneChannel: make(chan bool)}, errorChannel, nil } // GetExpecter returns a reference to the expect.Expecter reference used to control the OpenShift client. @@ -97,3 +96,24 @@ func (o *Oc) GetOptions() []Option { func (o *Oc) GetErrorChannel() <-chan error { return o.errorChannel } + +// GetDoneChannel returns the receive only done channel +func (o *Oc) GetDoneChannel() <-chan bool { + log.Debugf("read done channel pod %s/%s", o.pod, o.container) + return o.doneChannel +} + +// Close sends the signal to the done channel +func (o *Oc) Close() { + if o == nil { + log.Debugf("Oc is null, nothing to close") + return + } + log.Debugf("send close to channel pod %s/%s ", o.pod, o.container) + o.doneChannel <- true + close(o.doneChannel) + err := (*(o.expecter)).Close() + if err != nil { + log.Errorf("Oc session close failed because of: %s", err) + } +} diff --git a/pkg/tnf/interactive/oc_test.go b/pkg/tnf/interactive/oc_test.go index 6f57ca63d..366fcc037 100644 --- a/pkg/tnf/interactive/oc_test.go +++ b/pkg/tnf/interactive/oc_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/interactive/pty.go b/pkg/tnf/interactive/pty.go index 32c48b27b..35584c050 100644 --- a/pkg/tnf/interactive/pty.go +++ b/pkg/tnf/interactive/pty.go @@ -19,7 +19,7 @@ package interactive import ( "bytes" "html/template" - "io/ioutil" + "os" "time" "github.com/test-network-function/test-network-function/pkg/jsonschema" @@ -37,7 +37,7 @@ const ( // entry-point, which will vary for unit tests, executables, and test suites. If the supplied file does not conform to // the generic-pty.schema.json schema, creation fails and the result is returned to the caller for further inspection. func SpawnGenericPTYFromYAMLFile(ptyPath, schemaPath string, spawner *Spawner) (*Context, *gojsonschema.Result, error) { - ptyBytes, err := ioutil.ReadFile(ptyPath) + ptyBytes, err := os.ReadFile(ptyPath) if err != nil { return nil, nil, err } @@ -66,7 +66,7 @@ func SpawnGenericPTYFromYAML(inputBytes []byte, schemaPath string, spawner *Spaw // suites. If the supplied template/values do not conform to the generic-pty.schema.json schema, creation fails and the // result is returned to the caller for further inspection. func SpawnGenericPTYFromYAMLTemplate(templateFile, valuesFile, schemaPath string, spawner *Spawner) (*Context, *gojsonschema.Result, error) { - tplBytes, err := ioutil.ReadFile(valuesFile) + tplBytes, err := os.ReadFile(valuesFile) if err != nil { return nil, nil, err } @@ -78,7 +78,7 @@ func SpawnGenericPTYFromYAMLTemplate(templateFile, valuesFile, schemaPath string return nil, nil, err } - templateBytes, err := ioutil.ReadFile(templateFile) + templateBytes, err := os.ReadFile(templateFile) if err != nil { return nil, nil, err } diff --git a/pkg/tnf/interactive/shell.go b/pkg/tnf/interactive/shell.go index b4cb6b49f..1582ffdc0 100644 --- a/pkg/tnf/interactive/shell.go +++ b/pkg/tnf/interactive/shell.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,12 +17,15 @@ package interactive import ( + "log" "os" "time" ) const ( shellEnvironmentVariableKey = "SHELL" + defaultTimeoutSeconds = 10 + defaultTimeout = defaultTimeoutSeconds * time.Second ) // SpawnShell creates an interactive shell subprocess based on the value of $SHELL, spawning the appropriate underlying @@ -32,3 +35,14 @@ func SpawnShell(spawner *Spawner, timeout time.Duration, opts ...Option) (*Conte var args []string return (*spawner).Spawn(shellEnv, args, timeout, opts...) } + +// +// +// GetContext spawns a new shell session and returns its context +func GetContext(verbose bool) *Context { + context, err := SpawnShell(CreateGoExpectSpawner(), defaultTimeout, Verbose(verbose), SendTimeout(defaultTimeout)) + if err != nil || context == nil || context.GetExpecter() == nil { + log.Panicf("can't get a proper context for test execution") + } + return context +} diff --git a/pkg/tnf/interactive/shell_test.go b/pkg/tnf/interactive/shell_test.go index 7a25f6730..4394910e8 100644 --- a/pkg/tnf/interactive/shell_test.go +++ b/pkg/tnf/interactive/shell_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/interactive/spawner.go b/pkg/tnf/interactive/spawner.go index 21e1ca783..72d9db079 100644 --- a/pkg/tnf/interactive/spawner.go +++ b/pkg/tnf/interactive/spawner.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,10 +17,13 @@ package interactive import ( + "bufio" + "fmt" "io" "os" "os/exec" "strconv" + "strings" "time" expect "github.com/google/goexpect" @@ -58,8 +61,20 @@ type SpawnFunc interface { // StdoutPipe consult exec.Cmd.StdoutPipe StdoutPipe() (io.Reader, error) + // StderrPipe consult exec.Cmd.StderrPipe + StderrPipe() (io.Reader, error) + // Wait consult exec.Cmd.Wait Wait() error + + // Close calls the exec.Cmd.Kill to stop the process (shell). + Close() error + + // IsRunning returns true if the shell hasn't exited yet. + IsRunning() bool + + // Args returns the command and arguments used to spawn the shell. + Args() []string } // ExecSpawnFunc is an implementation of SpawnFunc using exec.Cmd. @@ -80,6 +95,16 @@ func (e *ExecSpawnFunc) Wait() error { return e.cmd.Wait() } +// IsRunning returns true if e.Cmd.ProcessState is nil, false otherwise +func (e *ExecSpawnFunc) IsRunning() bool { + return e.cmd.ProcessState == nil +} + +// Args wraps e.Cmd.Args +func (e *ExecSpawnFunc) Args() []string { + return e.cmd.Args +} + // Start wraps exec.Cmd.Start. func (e *ExecSpawnFunc) Start() error { return e.cmd.Start() @@ -95,6 +120,16 @@ func (e *ExecSpawnFunc) StdoutPipe() (io.Reader, error) { return e.cmd.StdoutPipe() } +// StderrPipe wraps exec.Cmd.Stderrpipe +func (e *ExecSpawnFunc) StderrPipe() (io.Reader, error) { + return e.cmd.StderrPipe() +} + +// Close wraps exec.Cmd.Kill. +func (e *ExecSpawnFunc) Close() error { + return e.cmd.Process.Kill() +} + // Spawner provides an interface for creating interactive sessions such as oc, ssh, or shell. type Spawner interface { // Spawn creates the interactive session. @@ -146,6 +181,11 @@ type GoExpectSpawner struct { verboseWriterIsSet bool // verboseWriter is an alternate destination for verbose logs. verboseWriter io.Writer + + // sendTimeoutIsSet tracks whether the Send command timeout is set. + sendTimeoutIsSet bool + // sendTimeout is the timeout of send command + sendTimeout time.Duration } // Option is a function pointer to enable lightweight optionals for GoExpectSpawner. @@ -191,6 +231,16 @@ func VerboseWriter(verboseWriter io.Writer) Option { } } +// SendTimeout sets the timeout of send command +func SendTimeout(timeout time.Duration) Option { + return func(g *GoExpectSpawner) Option { + g.sendTimeoutIsSet = true + prev := g.sendTimeout + g.sendTimeout = timeout + return SendTimeout(prev) + } +} + // getDefaultBufferSize returns the default buffer size as sourced from TNF_DEFAULT_BUFFER_SIZE. If // TNF_DEFAULT_BUFFER_SIZE is not set or cannot be parsed as an integer, defaultBufferSize is returned. func getDefaultBufferSize() int { @@ -228,6 +278,10 @@ func (g *GoExpectSpawner) GetGoExpectOptions() []expect.Option { opts = append(opts, expect.VerboseWriter(g.verboseWriter)) } + if g.sendTimeoutIsSet { + opts = append(opts, expect.SendTimeout(g.sendTimeout)) + } + return opts } @@ -236,6 +290,31 @@ func NewGoExpectSpawner() *GoExpectSpawner { return &GoExpectSpawner{} } +// logCmdMirrorPipe logs specified pipe output to logger. +func logCmdMirrorPipe(cmdLine string, pipeToMirror io.Reader, name string, trace bool) io.Reader { + originalPipe := pipeToMirror + r, w, _ := os.Pipe() + tr := io.TeeReader(originalPipe, w) + + go func() { + buf := bufio.NewReader(tr) + for { + line, _, err := buf.ReadLine() + if trace { + log.Trace(name + " for " + cmdLine + " : " + string(line)) + } else { + log.Warn(name + " for " + cmdLine + " : " + string(line)) + } + if err != nil { + // Some Error has happened, goroutine about to exit + log.Warnf("Exiting %s log mirroring goroutine for cmd %s. Error: %s", name, cmdLine, err) + return + } + } + }() + return r +} + // Spawn creates a subprocess, setting standard input and standard output appropriately. This is the base method to // create any interactive PTY based process. func (g *GoExpectSpawner) Spawn(command string, args []string, timeout time.Duration, opts ...Option) (*Context, error) { @@ -250,10 +329,17 @@ func (g *GoExpectSpawner) Spawn(command string, args []string, timeout time.Dura } spawnFunc = (*spawnFunc).Command(command, args...) - stdinPipe, stdoutPipe, err := g.unpackPipes(spawnFunc) + stdinPipe, stdoutPipe, stderrPipe, err := g.unpackPipes(spawnFunc) if err != nil { return nil, err } + + cmdLine := fmt.Sprintf("%s %s", command, strings.Join(args, " ")) + log.Debugf("Spawning interactive shell. Cmd: %s", cmdLine) + + logCmdMirrorPipe(cmdLine, stderrPipe, "STDERR", false) + stdoutPipe = logCmdMirrorPipe(cmdLine, stdoutPipe, "STDOUT", true) + err = g.startCommand(spawnFunc, command, args) if err != nil { return nil, err @@ -275,9 +361,16 @@ func (g *GoExpectSpawner) spawnGeneric(spawnFunc *SpawnFunc, stdinPipe io.WriteC return (*spawnFunc).Wait() }, Close: func() error { - return nil + log.Debug("Killing shell cmd: " + strings.Join((*spawnFunc).Args(), " ")) + return (*spawnFunc).Close() + }, + Check: func() bool { + if !(*spawnFunc).IsRunning() { + log.Error("Unable to send commands to spawned shell. Shell cmd: " + strings.Join((*spawnFunc).Args(), " ")) + return false + } + return true }, - Check: func() bool { return true }, }, timeout, opts...) // coax out the typing var expecter expect.Expecter = gexpecter @@ -295,17 +388,21 @@ func (g *GoExpectSpawner) startCommand(spawnFunc *SpawnFunc, command string, arg return err } -// Helper method to unpack stdin and stdout. -func (g *GoExpectSpawner) unpackPipes(spawnFunc *SpawnFunc) (io.WriteCloser, io.Reader, error) { +//nolint:gocritic // Helper method to unpack stdin and stdout. +func (g *GoExpectSpawner) unpackPipes(spawnFunc *SpawnFunc) (io.WriteCloser, io.Reader, io.Reader, error) { stdinPipe, err := g.extractStdinPipe(spawnFunc) if err != nil { - return nil, nil, err + return nil, nil, nil, err } stdoutPipe, err := g.extractStdoutPipe(spawnFunc) if err != nil { - return nil, nil, err + return nil, nil, nil, err + } + stderrPipe, err := g.extractStderrPipe(spawnFunc) + if err != nil { + return nil, nil, nil, err } - return stdinPipe, stdoutPipe, err + return stdinPipe, stdoutPipe, stderrPipe, err } // Helper method to extract stdin. @@ -326,6 +423,15 @@ func (g *GoExpectSpawner) extractStdoutPipe(spawnFunc *SpawnFunc) (io.Reader, er return stdout, err } +// Helper method to extract stdout. +func (g *GoExpectSpawner) extractStderrPipe(spawnFunc *SpawnFunc) (io.Reader, error) { + stderr, err := (*spawnFunc).StderrPipe() + if err != nil { + log.Errorf("Couldn't extract stderr for the given process: %v", err) + } + return stderr, err +} + // CreateGoExpectSpawner creates a GoExpectSpawner implementation and returns it as a *Spawner for type compatibility // reasons. func CreateGoExpectSpawner() *Spawner { diff --git a/pkg/tnf/interactive/spawner_test.go b/pkg/tnf/interactive/spawner_test.go index c489ece5e..6dc5d8a05 100644 --- a/pkg/tnf/interactive/spawner_test.go +++ b/pkg/tnf/interactive/spawner_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -44,6 +44,7 @@ func init() { var ( defaultGoExpectArgs = []interactive.Option{interactive.Verbose(true)} defaultStdout, defaultStdin, _ = os.Pipe() + defaultStderr, _, _ = os.Pipe() errStart = errors.New("start failed") errStdInPipe = errors.New("failed to access stdin") ) @@ -62,6 +63,10 @@ type goExpectSpawnerTestCase struct { stdoutPipeReturnValue io.Reader stdoutPipeReturnErr error + stderrPipeShouldBeCalled bool + stderrPipeReturnValue io.Reader + stderrPipeReturnErr error + startShouldBeCalled bool startReturnErr error @@ -87,6 +92,10 @@ var goExpectSpawnerTestCases = map[string]goExpectSpawnerTestCase{ stdoutPipeReturnValue: nil, stdoutPipeReturnErr: nil, + stderrPipeShouldBeCalled: false, + stderrPipeReturnValue: nil, + stderrPipeReturnErr: nil, + startShouldBeCalled: false, startReturnErr: nil, @@ -110,13 +119,44 @@ var goExpectSpawnerTestCases = map[string]goExpectSpawnerTestCase{ stdoutPipeReturnValue: nil, stdoutPipeReturnErr: errStdInPipe, + stderrPipeShouldBeCalled: false, + stderrPipeReturnValue: nil, + stderrPipeReturnErr: nil, + + startShouldBeCalled: false, + startReturnErr: nil, + + goExpectSpawnerSpawnReturnContextIsNil: true, + goExpectSpawnerSpawnReturnErr: errStdInPipe, + }, + // 2. Progressing past the creation of stdin and stdout, now cause stderr to fail. + "stderr_pipe_creation_failure": { + // The command is unimportant + goExpectSpawnerSpawnCommand: "ls", + goExpectSpawnerSpawnArgs: []string{"-al"}, + goExpectSpawnerSpawnTimeout: testTimeoutDuration, + goExpectSpawnerSpawnOpts: defaultGoExpectArgs, + + stdinPipeShouldBeCalled: true, + stdinPipeReturnValue: defaultStdin, + stdinPipeReturnErr: nil, + + stdoutPipeShouldBeCalled: true, + stdoutPipeReturnValue: defaultStdout, + stdoutPipeReturnErr: nil, + + // cause StderrPipe() call to fail and ensure the error cascades. + stderrPipeShouldBeCalled: true, + stderrPipeReturnValue: nil, + stderrPipeReturnErr: errStdInPipe, + startShouldBeCalled: false, startReturnErr: nil, goExpectSpawnerSpawnReturnContextIsNil: true, goExpectSpawnerSpawnReturnErr: errStdInPipe, }, - // 3. Progressing past the creation of stdin/stdout, now cause Start to fail. + // 3. Progressing past the creation of stdin/stdout/stderr, now cause Start to fail. "start_failure": { // The command is unimportant goExpectSpawnerSpawnCommand: "ls", @@ -132,6 +172,10 @@ var goExpectSpawnerTestCases = map[string]goExpectSpawnerTestCase{ stdoutPipeReturnValue: defaultStdout, stdoutPipeReturnErr: nil, + stderrPipeShouldBeCalled: true, + stderrPipeReturnValue: defaultStderr, + stderrPipeReturnErr: nil, + // cause Start() call to fail and make sure the error cascades out of Spawn(). startShouldBeCalled: true, startReturnErr: errStart, @@ -155,6 +199,10 @@ var goExpectSpawnerTestCases = map[string]goExpectSpawnerTestCase{ stdoutPipeReturnValue: defaultStdout, stdoutPipeReturnErr: nil, + stderrPipeShouldBeCalled: true, + stderrPipeReturnValue: defaultStderr, + stderrPipeReturnErr: nil, + startShouldBeCalled: true, startReturnErr: nil, @@ -181,6 +229,10 @@ func TestGoExpectSpawner_Spawn(t *testing.T) { mockSpawnFunc.EXPECT().StdoutPipe().Return(testCase.stdoutPipeReturnValue, testCase.stdoutPipeReturnErr) } + if testCase.stderrPipeShouldBeCalled { + mockSpawnFunc.EXPECT().StderrPipe().Return(testCase.stderrPipeReturnValue, testCase.stderrPipeReturnErr) + } + if testCase.startShouldBeCalled { mockSpawnFunc.EXPECT().Start().Return(testCase.startReturnErr) } @@ -225,6 +277,10 @@ func TestExecSpawnFunc(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, stdout) + stderr, err := (*cmd).StderrPipe() + assert.Nil(t, err) + assert.NotNil(t, stderr) + err = (*cmd).Start() assert.Nil(t, err) diff --git a/pkg/tnf/interactive/ssh.go b/pkg/tnf/interactive/ssh.go index b876acef9..9160a61d5 100644 --- a/pkg/tnf/interactive/ssh.go +++ b/pkg/tnf/interactive/ssh.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/interactive/ssh_test.go b/pkg/tnf/interactive/ssh_test.go index 7028a3761..1e23de8ab 100644 --- a/pkg/tnf/interactive/ssh_test.go +++ b/pkg/tnf/interactive/ssh_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/interactive/testdata/ssh.json.tpl.values.bad.nonyaml b/pkg/tnf/interactive/testdata/ssh.json.tpl.values.bad.nonyaml index 08a3a39f2..e568ca35f 100644 --- a/pkg/tnf/interactive/testdata/ssh.json.tpl.values.bad.nonyaml +++ b/pkg/tnf/interactive/testdata/ssh.json.tpl.values.bad.nonyaml @@ -1,3 +1,3 @@ { "notyaml" -} \ No newline at end of file +} diff --git a/pkg/tnf/interactive/testdata/ssh.json.tpl.values.bad.yaml.tpl b/pkg/tnf/interactive/testdata/ssh.json.tpl.values.bad.yaml.tpl index d9e27d1a2..4b271d455 100644 --- a/pkg/tnf/interactive/testdata/ssh.json.tpl.values.bad.yaml.tpl +++ b/pkg/tnf/interactive/testdata/ssh.json.tpl.values.bad.yaml.tpl @@ -1,2 +1,2 @@ bad: - - keys \ No newline at end of file + - keys diff --git a/pkg/tnf/reel/doc.go b/pkg/tnf/reel/doc.go index bd706c7af..4f6c8161d 100644 --- a/pkg/tnf/reel/doc.go +++ b/pkg/tnf/reel/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/reel/reel.go b/pkg/tnf/reel/reel.go index 7d1e01066..34a842689 100644 --- a/pkg/tnf/reel/reel.go +++ b/pkg/tnf/reel/reel.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -19,9 +19,12 @@ package reel import ( "fmt" "regexp" + "strconv" "strings" "time" + log "github.com/sirupsen/logrus" + expect "github.com/google/goexpect" "google.golang.org/grpc/codes" ) @@ -29,14 +32,21 @@ import ( const ( // EndOfTestSentinel is the emulated terminal prompt that will follow command output. EndOfTestSentinel = `END_OF_TEST_SENTINEL` + // ExitKeyword keyword delimiting the command exit status + ExitKeyword = "exit=" ) var ( - // endOfTestSentinelCutset is used to trim a match of the EndOfTestSentinel. - endOfTestSentinelCutset = fmt.Sprintf("%s\n", EndOfTestSentinel) - // EndOfTestRegexPostfix is the postfix added to regular expressions to match the emulated terminal prompt - // (EndOfTestSentinel) - EndOfTestRegexPostfix = fmt.Sprintf("((.|\n)*%s\n)", EndOfTestSentinel) + + // matchSentinel This regular expression is matching stricly the sentinel and exit code. + // This match regular expression matches commands that return no output + matchSentinel = fmt.Sprintf("((.|\n)*%s %s[0-9]+\n)", EndOfTestSentinel, ExitKeyword) + + // EndOfTestRegexPostfix This regular expression is a postfix added to the goexpect regular expressions. This regular expression matches a + // sentinel or marker string that is marking the end of the command output. This is because after the command + // output, the shell might also return a prompt which is not desired. Note: this is currently the same as the string above + // but was splitted for clarity + EndOfTestRegexPostfix = matchSentinel ) // Step is an instruction for a single REEL pass. @@ -98,6 +108,8 @@ type Reel struct { } // DisableTerminalPromptEmulation disables terminal prompt emulation for the reel.Reel. +// This disables terminal shell management and is used only for unit testing where the terminal is not available +// In this mode, go expect operates only on strings not command/shell outputs func DisableTerminalPromptEmulation() Option { return func(r *Reel) Option { r.disableTerminalPromptEmulation = true @@ -106,9 +118,11 @@ func DisableTerminalPromptEmulation() Option { } // Each Step can have zero or more expectations (Step.Expect). This method follows the Adapter design pattern; a raw -// array of strings is turned into a corresponding array of exepct.Batcher. This method side-effects the input +// array of strings is turned into a corresponding array of expect.Batcher. This method side-effects the input // expectations array, following the Builder design pattern. Finally, the first match is stored in the firstMatch // output parameter. +// This command translates individual expectations in the test cases (e.g. success, failure, etc) into expect.Case in go expect +// The expect.Case are later matched in order inside goexpect ExpectBatch function. func (r *Reel) batchExpectations(expectations []string, batcher []expect.Batcher, firstMatch *string) []expect.Batcher { if len(expectations) > 0 { expectCases := r.generateCases(expectations, firstMatch) @@ -123,10 +137,15 @@ func (r *Reel) batchExpectations(expectations []string, batcher []expect.Batcher // parameter to store the first match found in the expectations array. Thus, the order of expectations is important. func (r *Reel) generateCases(expectations []string, firstMatch *string) []expect.Caser { var cases []expect.Caser + // expectations created from test case matches for _, expectation := range expectations { thisCase := r.generateCase(expectation, firstMatch) cases = append(cases, thisCase) } + // extra test case to match when commands do not return anything but exit without error. This expectation makes + // sure that any command exiting successfully will be processed without timeout. + thisCase := r.generateCase("", firstMatch) + cases = append(cases, thisCase) return cases } @@ -142,7 +161,7 @@ func (r *Reel) generateCase(expectation string, firstMatch *string) *expect.Case }} } -// Each Step can have exactly one execution string (Step.Execute). This method follows the Adapter design pattern; a +// Each Step can have exactly one execution string (Step.Execute). This method follows the Adapter design pattern; a // single raw execution string is converted into a corresponding expect.Batcher. The function returns an array of // expect.Batcher, as it is expected that there are likely expectations to follow. func (r *Reel) generateBatcher(execute string) []expect.Batcher { @@ -155,7 +174,7 @@ func (r *Reel) generateBatcher(execute string) []expect.Batcher { } // Determines if an error is an expect.TimeoutError. -func isTimeout(err error) bool { +func IsTimeout(err error) bool { _, ok := err.(expect.TimeoutError) return ok } @@ -168,29 +187,35 @@ func (r *Reel) Step(step *Step, handler Handler) error { return r.Err } exec, exp, timeout := step.unpack() - var batcher []expect.Batcher - batcher = r.generateBatcher(exec) - var firstMatch string - batcher = r.batchExpectations(exp, batcher, &firstMatch) - results, err := (*r.expecter).ExpectBatch(batcher, timeout) - + var batchers []expect.Batcher + batchers = r.generateBatcher(exec) + // firstMatchRe is the first regular expression (expectation) that has matched results + var firstMatchRe string + batchers = r.batchExpectations(exp, batchers, &firstMatchRe) + results, err := (*r.expecter).ExpectBatch(batchers, timeout) if !step.hasExpectations() { return nil } - if err != nil { - if isTimeout(err) { + // record the err in reel in case the next step is nil as we return r.Err at the end + r.Err = err + if IsTimeout(err) { step = handler.ReelTimeout() } else { - return err + step = nil } - } else { - if len(results) > 0 { - result := results[0] - - output := r.stripEmulatedPromptFromOutput(result.Output) - match := r.stripEmulatedPromptFromOutput(result.Match[0]) - + } else if len(results) > 0 { + result := results[0] + output, outputStatus := r.stripEmulatedPromptFromOutput(result.Output) + if outputStatus != 0 { + r.Err = fmt.Errorf("error executing command exit code:%d", outputStatus) + step = nil + continue + } + match, matchStatus := r.stripEmulatedPromptFromOutput(result.Match[0]) + log.Debugf("command status: output=%s, match=%s, outputStatus=%d, matchStatus=%d, caseIndex=%d", output, match, outputStatus, matchStatus, result.CaseIdx) + // Check if the matching case is the extra one added in generateCases() for prompt return in error cases, skip calling ReelMatch if it is + if result.CaseIdx != len(batchers[result.Idx].Cases())-1 { matchIndex := strings.Index(output, match) var before string // special case: the match regex may be nothing at all. @@ -199,11 +224,14 @@ func (r *Reel) Step(step *Step, handler Handler) error { } else { before = "" } - step = handler.ReelMatch(r.stripEmulatedRegularExpression(firstMatch), before, match) + strippedFirstMatchRe := r.stripEmulatedRegularExpression(firstMatchRe) + step = handler.ReelMatch(strippedFirstMatchRe, before, match) + } else { + step = nil } } } - return nil + return r.Err } // Run the target subprocess to completion. The first step to take is supplied by handler. Consequent steps are @@ -254,15 +282,34 @@ func (r *Reel) wrapTestCommand(cmd string) string { // WrapTestCommand wraps cmd so that the output will end in an emulated terminal prompt. func WrapTestCommand(cmd string) string { cmd = strings.TrimRight(cmd, "\n") - return fmt.Sprintf("%s && echo %s\n", cmd, EndOfTestSentinel) + wrappedCommand := fmt.Sprintf("%s ; echo %s %s$?\n", cmd, EndOfTestSentinel, ExitKeyword) + log.Tracef("Command sent: %s", wrappedCommand) + return wrappedCommand } // stripEmulatedPromptFromOutput will elide the emulated terminal prompt from the test output. -func (r *Reel) stripEmulatedPromptFromOutput(output string) string { - if !r.disableTerminalPromptEmulation { - return strings.TrimRight(strings.TrimRight(output, endOfTestSentinelCutset), "\n") +func (r *Reel) stripEmulatedPromptFromOutput(output string) (data string, status int) { + parsed := strings.Split(output, EndOfTestSentinel) + var err error + if !r.disableTerminalPromptEmulation && len(parsed) == 2 { + // if a sentinel was present, then we have at least 2 parsed results + // if command retuned nothing parsed[0]=="" + data = parsed[0] + status, err = strconv.Atoi(strings.Split(strings.Split(parsed[1], ExitKeyword)[1], "\n")[0]) + if err != nil { + // Cannot parse status from output, something is wrong, fail command + status = 1 + log.Errorf("Cannot determine command status. Error: %s", err) + } + // remove trailing \n if present + data = strings.TrimRight(data, "\n") + } else { + // to support unit tests (without sentinel parsing) + data = output + status = 0 + log.Errorf("Cannot determine command status, no sentinel present. Error: %s", err) } - return output + return } // stripEmulatedRegularExpression will elide the modified part of the terminal prompt regular expression. @@ -276,7 +323,8 @@ func (r *Reel) stripEmulatedRegularExpression(match string) string { // addEmulatedRegularExpression will append the additional regular expression to capture the emulated terminal prompt. func (r *Reel) addEmulatedRegularExpression(regularExpressionString string) string { if !r.disableTerminalPromptEmulation { - return fmt.Sprintf("%s%s", regularExpressionString, EndOfTestRegexPostfix) + regularExpressionStringWithMetadata := fmt.Sprintf("%s%s", regularExpressionString, EndOfTestRegexPostfix) + return regularExpressionStringWithMetadata } return regularExpressionString } diff --git a/pkg/tnf/reel/reel_test.go b/pkg/tnf/reel/reel_test.go index d7d79a7ea..ee961c4c0 100644 --- a/pkg/tnf/reel/reel_test.go +++ b/pkg/tnf/reel/reel_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -38,6 +38,7 @@ var ( defaultCommand = []string{"ls"} errReel = errors.New("some reel error") errSendCommand = errors.New("send command error") + errTimeout = expect.TimeoutError(time.Second * 1) ) type newReelTestCase struct { @@ -146,11 +147,11 @@ var reelStepTestCases = map[string]reelStepTestCase{ "timeout_error": { stepInput: &reel.Step{Expect: []string{"expect something"}}, command: defaultCommand, - stepReturnErr: nil, + stepReturnErr: errTimeout, reelErr: nil, expectBatchExpectedInvocationCount: 1, expectBatchResResult: []expect.BatchRes{}, - expectBatchErrResult: expect.TimeoutError(time.Second * 1), + expectBatchErrResult: errTimeout, isTimeout: true, }, "non_timeout_error": { diff --git a/pkg/tnf/test.go b/pkg/tnf/test.go index bca9d8792..ed167a7f7 100644 --- a/pkg/tnf/test.go +++ b/pkg/tnf/test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,14 +17,26 @@ package tnf import ( + "fmt" "time" expect "github.com/google/goexpect" - "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + "github.com/sirupsen/logrus" "github.com/test-network-function/test-network-function/pkg/tnf/identifier" "github.com/test-network-function/test-network-function/pkg/tnf/reel" ) +// ClaimFilePrintf prints to claim and junit report files. +func ClaimFilePrintf(format string, args ...interface{}) { + message := fmt.Sprintf(format+"\n", args...) + _, err := ginkgo.GinkgoWriter.Write([]byte(message)) + if err != nil { + logrus.Errorf("Ginkgo writer could not write msg '%s' because: %s", message, err) + } +} + const ( // ERROR represents an errored test. ERROR = iota @@ -36,21 +48,7 @@ const ( // TestsExtraInfo a collection of messages per test that is added to the claim file // use WriteTestExtraInfo for writing to it -var TestsExtraInfo []map[string][]string = []map[string][]string{} - -// CreateTestExtraInfoWriter creates a function that writes info messages for a specific test -// info messages that were already added by calling the function will exist in the claim file -func CreateTestExtraInfoWriter() func(string) { - testName := ginkgo.CurrentGinkgoTestDescription().FullTestText - if testName == "" { - return func(string) {} - } - extraInfo := map[string][]string{testName: nil} - TestsExtraInfo = append(TestsExtraInfo, extraInfo) - return func(info string) { - extraInfo[testName] = append(extraInfo[testName], info) - } -} +var TestsExtraInfo = []map[string][]string{} // ExitCodeMap maps a test result value to a more appropriate Unix return code. var ExitCodeMap = map[int]int{ @@ -128,6 +126,41 @@ func (t *Test) ReelEOF() { } } +// RunAndValidate runs the test and checks the result +func (t *Test) RunAndValidate() { + t.RunAndValidateWithFailureCallback(nil) +} + +// RunAndValidateWithFailureCallback runs the test, checks the result/error and invokes the cb on failure +func (t *Test) RunAndValidateWithFailureCallback(cb func()) { + testResult, err := t.Run() + if testResult == FAILURE && cb != nil { + cb() + } + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(testResult).To(gomega.Equal(SUCCESS)) +} + +// RunWithCallbacks runs the test, invokes the cb on failure/error/success +// This is useful when the testcase needs to continue whether this test result is success or not +func (t *Test) RunWithCallbacks(successCb, failureCb func(), errorCb func(error)) { + testResult, err := t.Run() + switch testResult { + case SUCCESS: + if successCb != nil { + successCb() + } + case FAILURE: + if failureCb != nil { + failureCb() + } + case ERROR: + if errorCb != nil { + errorCb(err) + } + } +} + // NewTest creates a new Test given a chain of Handlers. func NewTest(expecter *expect.Expecter, tester Tester, chain []reel.Handler, errorChannel <-chan error, opts ...reel.Option) (*Test, error) { args := tester.Args() diff --git a/pkg/tnf/test_test.go b/pkg/tnf/test_test.go index 8fa5e3a45..228fc82ef 100644 --- a/pkg/tnf/test_test.go +++ b/pkg/tnf/test_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -102,10 +102,27 @@ type testRunTestCase struct { reelMatchBefore string reelMatchMatch string reelMatchResult *reel.Step + testRunErr error } -func fakeSentinelOutput() string { - return fmt.Sprintf("someOutput%s\n", reel.EndOfTestRegexPostfix) +func fakeSentinelOutputWithReturnCode(output string, code int) string { + return fmt.Sprintf("%s%s %s%d\n", output, reel.EndOfTestSentinel, reel.ExitKeyword, code) +} + +func fakeSentinelOutput(output string) string { + return fakeSentinelOutputWithReturnCode(output, 0) +} + +func fakeOutput() string { + return "someOutput" +} + +func fakeWrongOutput() string { + return "something else" +} + +func fakeErrorCode() int { + return 1 } // Tests the actual state machine. @@ -136,24 +153,64 @@ var testRunTestCases = map[string]testRunTestCase{ testCommandArgs: defaultTestCommand, reelFirstResult: &reel.Step{ Execute: "ls", - Expect: []string{fakeSentinelOutput()}, + Expect: []string{fakeOutput()}, Timeout: testTimeoutDuration, }, testerResultResult: tnf.ERROR, expectBatchIsCalled: true, expectBatchBatchResResult: []expect.BatchRes{ { - Idx: 0, - Output: fakeSentinelOutput(), - Match: []string{fakeSentinelOutput()}}, + Idx: 1, + CaseIdx: 0, + Output: fakeSentinelOutput(fakeOutput()), + Match: []string{fakeSentinelOutput(fakeOutput())}}, }, expectBatchBatchResErr: nil, reelMatchIsCalled: true, reelMatchPattern: "", reelMatchBefore: "", - reelMatchMatch: fakeSentinelOutput(), + reelMatchMatch: fakeOutput(), reelMatchResult: nil, }, + "reel_first_only": { + testCommandArgs: defaultTestCommand, + reelFirstResult: &reel.Step{ + Execute: "ls", + Expect: []string{fakeOutput()}, + Timeout: testTimeoutDuration, + }, + testerResultResult: tnf.ERROR, + expectBatchIsCalled: true, + expectBatchBatchResResult: []expect.BatchRes{ + { + Idx: 1, + CaseIdx: 1, + Output: fakeSentinelOutput(fakeWrongOutput()), + Match: []string{fakeSentinelOutput(fakeWrongOutput())}}, + }, + expectBatchBatchResErr: nil, + reelMatchIsCalled: false, + }, + "reel_first_only_with_error_code": { + testCommandArgs: defaultTestCommand, + reelFirstResult: &reel.Step{ + Execute: "ls", + Expect: []string{fakeOutput()}, + Timeout: testTimeoutDuration, + }, + testerResultResult: tnf.ERROR, + expectBatchIsCalled: true, + expectBatchBatchResResult: []expect.BatchRes{ + { + Idx: 1, + CaseIdx: 1, + Output: fakeSentinelOutputWithReturnCode(fakeWrongOutput(), fakeErrorCode()), + Match: []string{fakeSentinelOutputWithReturnCode(fakeWrongOutput(), fakeErrorCode())}}, + }, + expectBatchBatchResErr: nil, + reelMatchIsCalled: false, + testRunErr: fmt.Errorf("error executing command exit code:%d", fakeErrorCode()), + }, } // Also covers ReelFirst() and ReelMatch(). Tests those state transitions. @@ -163,7 +220,7 @@ func TestTest_Run(t *testing.T) { for _, testCase := range testRunTestCases { mockExpecter := mock_interactive.NewMockExpecter(ctrl) - testCommand := strings.Join(testCase.testCommandArgs, " ") + "\n" + testCommand := fmt.Sprintf("%s ; echo %s %s$?\n", strings.Join(testCase.testCommandArgs, " "), reel.EndOfTestSentinel, reel.ExitKeyword) mockExpecter.EXPECT().Send(testCommand).AnyTimes() // Only for test cases where ReelMatch(...) is encountered. @@ -183,13 +240,12 @@ func TestTest_Run(t *testing.T) { var expecter expect.Expecter = mockExpecter var errorChannel <-chan error - test, err := tnf.NewTest(&expecter, mockTester, []reel.Handler{mockHandler}, errorChannel, reel.DisableTerminalPromptEmulation()) + test, err := tnf.NewTest(&expecter, mockTester, []reel.Handler{mockHandler}, errorChannel) assert.Nil(t, err) assert.NotNil(t, test) result, err := test.Run() - // Since we have no control over the t.runner, just make the assertion that err is nil. In these cases, it - // always should be nil, as it is mocked. - assert.Nil(t, err) + + assert.Equal(t, err, testCase.testRunErr) assert.Equal(t, result, testCase.testerResultResult) } } diff --git a/pkg/tnf/testcases/base.go b/pkg/tnf/testcases/base.go index 254c5725e..adb1810a5 100644 --- a/pkg/tnf/testcases/base.go +++ b/pkg/tnf/testcases/base.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,11 +18,11 @@ package testcases import ( "encoding/json" - "io/ioutil" "os" "regexp" "strings" + log "github.com/sirupsen/logrus" "github.com/test-network-function/test-network-function/pkg/tnf/testcases/data/cnf" "github.com/test-network-function/test-network-function/pkg/tnf/testcases/data/operator" "gopkg.in/yaml.v2" @@ -34,6 +34,8 @@ type StatusFunctionType string const ( // ServiceAccountFn function name to be called to replace expected status ServiceAccountFn StatusFunctionType = "FN_SERVICE_ACCOUNT_NAME" + // ConfiguredTestFile the name for the default "container" test list file + ConfiguredTestFile = "testconfigure.yml" ) // TestResultType Defines Test Result Type @@ -90,40 +92,38 @@ const ( OperatorStatus = "OPERATOR_STATUS" ) -// ContainerFactType type to hold container fact types -type ContainerFactType string +// PodFactType type to hold container fact types +type PodFactType string const ( // ServiceAccountName - for k8s service account name - ServiceAccountName ContainerFactType = "SERVICE_ACCOUNT_NAME" + ServiceAccountName PodFactType = "SERVICE_ACCOUNT_NAME" // Name for pod name - Name ContainerFactType = "NAME" + Name PodFactType = "NAME" // NameSpace for pod namespace - NameSpace ContainerFactType = "NAMESPACE" + NameSpace PodFactType = "NAMESPACE" // ClusterRole for cluster roles - ClusterRole ContainerFactType = "CLUSTER_ROLE" + ClusterRole PodFactType = "CLUSTER_ROLE" // ContainerCount for count of containers in the pod - ContainerCount ContainerFactType = "CONTAINER_COUNT" + ContainerCount PodFactType = "CONTAINER_COUNT" ) -// ContainerFact struct to store pod facts -type ContainerFact struct { +// PodFact struct to store pod facts +type PodFact struct { // Name of the pod under test Name string // Namespace of the pod under test Namespace string // ServiceAccount name used by the pod ServiceAccount string - // HasClusterRole if pod has cluster role - HasClusterRole bool // ContainerCount is the count of containers inside the pod ContainerCount int // Exists if the pod is found in the cluster Exists bool } -// CnfTestTemplateDataMap is map of available json data test case templates -var CnfTestTemplateDataMap = map[string]string{ +// PodTestTemplateDataMap is map of available json data test case templates +var PodTestTemplateDataMap = map[string]string{ GatherFacts: cnf.GatherPodFactsJSON, PrivilegedPod: cnf.PrivilegedPodJSON, PrivilegedRoles: cnf.RolesJSON, @@ -232,7 +232,7 @@ type ConfiguredTest struct { // LoadConfiguredTestFile loads configured test cases to struct func LoadConfiguredTestFile(filepath string) (c *ConfiguredTestCase, err error) { - yamlFile, err := ioutil.ReadFile(filepath) + yamlFile, err := os.ReadFile(filepath) if err != nil { return } @@ -273,7 +273,7 @@ func ContainsConfiguredTest(a []ConfiguredTest, testType string) ConfiguredTest // LoadCnfTestCaseSpecs loads base test template data into a struct func LoadCnfTestCaseSpecs(name string) (*BaseTestCaseConfigSpec, error) { var testCaseConfigSpec BaseTestCaseConfigSpec - err := json.Unmarshal([]byte(CnfTestTemplateDataMap[name]), &testCaseConfigSpec) + err := json.Unmarshal([]byte(PodTestTemplateDataMap[name]), &testCaseConfigSpec) if err != nil { return nil, err } @@ -341,7 +341,37 @@ func IsInFocus(focus []string, desc string) bool { focusFilter = regexp.MustCompile(strings.Join(focus, "|")) } if focusFilter != nil { - matchesFocus = focusFilter.Match([]byte(desc)) + matchesFocus = focusFilter.MatchString(desc) } return matchesFocus } + +// GetConfiguredPodTests loads the `configuredTestFile` and extracts +// the names of test groups from it. +func GetConfiguredPodTests() (cnfTests []string) { + configuredTests, err := LoadConfiguredTestFile(ConfiguredTestFile) + if err != nil { + log.Errorf("failed to load %s, continuing with no tests", ConfiguredTestFile) + return []string{} + } + for _, configuredTest := range configuredTests.CnfTest { + cnfTests = append(cnfTests, configuredTest.Name) + } + log.WithField("cnfTests", cnfTests).Infof("got all tests from %s.", ConfiguredTestFile) + return cnfTests +} + +// GetConfiguredOperatorTests loads the `configuredTestFile` and extracts +// the names of test groups from it. +func GetConfiguredOperatorTests() (operatorTests []string) { + configuredTests, err := LoadConfiguredTestFile(ConfiguredTestFile) + if err != nil { + log.Errorf("failed to load %s, continuing with no tests", ConfiguredTestFile) + return []string{} + } + for _, configuredTest := range configuredTests.OperatorTest { + operatorTests = append(operatorTests, configuredTest.Name) + } + log.WithField("operatorTests", operatorTests).Infof("got all tests from %s.", ConfiguredTestFile) + return operatorTests +} diff --git a/pkg/tnf/testcases/base_test.go b/pkg/tnf/testcases/base_test.go index c022dab4e..fd1fb8289 100644 --- a/pkg/tnf/testcases/base_test.go +++ b/pkg/tnf/testcases/base_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,7 +17,6 @@ package testcases_test import ( - "io/ioutil" "log" "os" "reflect" @@ -53,12 +52,12 @@ func setup() { configuredTest.Tests = []string{"HOST_NETWORK_CHECK", "HOST_PORT_CHECK", "HOST_IPC_CHECK"} testConfigure.CnfTest = append(testConfigure.CnfTest, configuredTest) - file, err = ioutil.TempFile(".", testTempFile) + file, err = os.CreateTemp(".", testTempFile) if err != nil { log.Fatal(err) } bytes, _ := yaml.Marshal(testConfigure) - err = ioutil.WriteFile(file.Name(), bytes, filePerm) + err = os.WriteFile(file.Name(), bytes, filePerm) if err != nil { log.Fatal(err) } @@ -122,7 +121,7 @@ func TestLoadInvalidPathCNFTestCaseSpecsFromFile(t *testing.T) { } func TestBaseTestCase_CNFExpectedStatusFn(t *testing.T) { - var facts = testcases.ContainerFact{} + var facts = testcases.PodFact{} facts.Name = name facts.ServiceAccount = "TEST_SERVICE_ACCOUNT_NAME" testCase, err := testcases.LoadTestCaseSpecsFromFile(testcases.PrivilegedRoles, cnfFilePath, testcases.Cnf) diff --git a/pkg/tnf/testcases/data/cnf/doc.go b/pkg/tnf/testcases/data/cnf/doc.go index 28c56958d..a49df38e8 100644 --- a/pkg/tnf/testcases/data/cnf/doc.go +++ b/pkg/tnf/testcases/data/cnf/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/testcases/data/cnf/gatherfacts.go b/pkg/tnf/testcases/data/cnf/gatherfacts.go index 9623bbecf..7d9e49140 100644 --- a/pkg/tnf/testcases/data/cnf/gatherfacts.go +++ b/pkg/tnf/testcases/data/cnf/gatherfacts.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/testcases/data/cnf/privilegedpod.go b/pkg/tnf/testcases/data/cnf/privilegedpod.go index e326d78c1..ba75b956d 100644 --- a/pkg/tnf/testcases/data/cnf/privilegedpod.go +++ b/pkg/tnf/testcases/data/cnf/privilegedpod.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -33,20 +33,20 @@ var PrivilegedPodJSON = string(`{ "name": "HOST_PORT_CHECK", "skiptest": true, "loop": 1, - "command": "oc get pod %s -n %s -o json | jq -r '.spec.containers[%d].ports.hostPort'", + "command": "oc get pod %s -n %s -o go-template='{{range (index .spec.containers %d).ports }}{{.hostPort}}{{end}}'", "action": "allow", "expectedstatus": [ - "NULL_FALSE" + "^()*$" ] }, { "name": "HOST_PATH_CHECK", "skiptest": true, "loop": 0, - "command": "oc get pod %s -n %s -o json | jq -r '.spec.hostpath.path'", + "command": "oc get pods %s -n %s -o go-template='{{range .spec.volumes}}{{.hostPath.path}}{{end}}'", "action": "allow", "expectedstatus": [ - "NULL_FALSE" + "^()*$" ] }, { @@ -78,7 +78,9 @@ var PrivilegedPodJSON = string(`{ "action": "deny", "expectedstatus": [ "NET_ADMIN", - "SYS_ADMIN" + "SYS_ADMIN", + "NET_RAW", + "IPC_LOCK" ] }, { diff --git a/pkg/tnf/testcases/data/cnf/privilegedroles.go b/pkg/tnf/testcases/data/cnf/privilegedroles.go index 1a65c172a..16b49f952 100644 --- a/pkg/tnf/testcases/data/cnf/privilegedroles.go +++ b/pkg/tnf/testcases/data/cnf/privilegedroles.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/testcases/data/operator/doc.go b/pkg/tnf/testcases/data/operator/doc.go index d9a132398..718f76ee4 100644 --- a/pkg/tnf/testcases/data/operator/doc.go +++ b/pkg/tnf/testcases/data/operator/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/testcases/data/operator/operator.go b/pkg/tnf/testcases/data/operator/operator.go index 20d9d1ae8..eadc35380 100644 --- a/pkg/tnf/testcases/data/operator/operator.go +++ b/pkg/tnf/testcases/data/operator/operator.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/testcases/doc.go b/pkg/tnf/testcases/doc.go index f4e08a691..639ec0687 100644 --- a/pkg/tnf/testcases/doc.go +++ b/pkg/tnf/testcases/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/pkg/tnf/testcases/files/cnf/gatherpodfacts.yml b/pkg/tnf/testcases/files/cnf/gatherpodfacts.yml index cc8b7552b..4632b2a41 100644 --- a/pkg/tnf/testcases/files/cnf/gatherpodfacts.yml +++ b/pkg/tnf/testcases/files/cnf/gatherpodfacts.yml @@ -1,12 +1,12 @@ testcase: - name: "NAME" - skiptest: false - command: "oc get pod %s -n %s -o json | jq -r '.metadata.name'" - action: "allow" - resulttype: "string" - expectedType: "regex" - expectedstatus: - - "ALLOW_ALL" + skiptest: false + command: "oc get pod %s -n %s -o json | jq -r '.metadata.name'" + action: "allow" + resulttype: "string" + expectedType: "regex" + expectedstatus: + - "ALLOW_ALL" - name: "CONTAINER_COUNT" skiptest: false command: "oc get pod %s -n %s -o json | jq -r '.spec.containers | length'" @@ -16,10 +16,10 @@ testcase: expectedstatus: - "DIGIT" - name: "SERVICE_ACCOUNT_NAME" - skiptest: false - command: "oc get pod %s -n %s -o json | jq -r '.spec.serviceAccountName'" - action: "allow" - resulttype: "string" - expectedType: "regex" - expectedstatus: - - "ALLOW_ALL" \ No newline at end of file + skiptest: false + command: "oc get pod %s -n %s -o json | jq -r '.spec.serviceAccountName'" + action: "allow" + resulttype: "string" + expectedType: "regex" + expectedstatus: + - "ALLOW_ALL" diff --git a/pkg/tnf/testcases/files/cnf/privilegedpod.yml b/pkg/tnf/testcases/files/cnf/privilegedpod.yml index 8019bc2e5..0515b0b3d 100644 --- a/pkg/tnf/testcases/files/cnf/privilegedpod.yml +++ b/pkg/tnf/testcases/files/cnf/privilegedpod.yml @@ -10,11 +10,11 @@ testcase: - name: HOST_PORT_CHECK skiptest: true loop: 0 - command: "oc get pod %s -n %s -o json | jq -r '.spec.containers[%d].ports.hostPort'" + command: "oc get pod %s -n %s -o go-template='{{$putName := .metadata.name}}{{$cut := (index .spec.containers %d)}}{{range $cut.ports }}{{if .hostPort}}PUT {{$putName}} - CUT {{$cut.name}} has declared hostPort {{.hostPort}}{{\"\\n\"}}{{end}}{{end}}'" action: allow expectedType: "regex" expectedstatus: - - NULL_FALSE + - "^()*$" - name: HOST_PATH_CHECK skiptest: true loop: 0 @@ -49,6 +49,8 @@ testcase: expectedstatus: - NET_ADMIN - SYS_ADMIN + - NET_RAW + - IPC_LOCK - name: ROOT_CHECK skiptest: true loop: 0 @@ -65,4 +67,4 @@ testcase: action: allow expectedType: "regex" expectedstatus: - - NULL_FALSE \ No newline at end of file + - NULL_FALSE diff --git a/pkg/tnf/testcases/files/cnf/privilegedroles.yml b/pkg/tnf/testcases/files/cnf/privilegedroles.yml index ffb99ac30..f59870647 100644 --- a/pkg/tnf/testcases/files/cnf/privilegedroles.yml +++ b/pkg/tnf/testcases/files/cnf/privilegedroles.yml @@ -15,4 +15,4 @@ testcase: expectedType: "function" expectedstatus: - "FN_SERVICE_ACCOUNT_NAME" - - "null" \ No newline at end of file + - "null" diff --git a/pkg/tnf/testcases/files/operator/operatorstatus.yml b/pkg/tnf/testcases/files/operator/operatorstatus.yml index 2be8fa47c..2e7a958ec 100644 --- a/pkg/tnf/testcases/files/operator/operatorstatus.yml +++ b/pkg/tnf/testcases/files/operator/operatorstatus.yml @@ -15,4 +15,3 @@ testcase: expectedtype: "string" expectedstatus: - "EMPTY" - diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 9d7ff8385..60cacf350 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -1,6 +1,53 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + package utils -import "strings" +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + log "github.com/sirupsen/logrus" + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/generic" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/nodedebug" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" +) + +var ( + // pathRelativeToRoot is used to calculate relative filepaths to the tnf folder. + pathRelativeToRoot = path.Join("..") + // commandHandlerFilePath is the file location of the command handler. + commandHandlerFilePath = path.Join(pathRelativeToRoot, "pkg", "tnf", "handlers", "command", "command.json") + // handlerJSONSchemaFilePath is the file location of the json handlers generic schema. + handlerJSONSchemaFilePath = path.Join(pathRelativeToRoot, "schemas", "generic-test.schema.json") +) + +const ( + timeoutPid = 5 * time.Second +) // ArgListToMap takes a list of strings of the form "key=value" and translate it into a map // of the form {key: value} @@ -27,3 +74,160 @@ func FilterArray(vs []string, f func(string) bool) []string { } return vsf } + +func CheckFileExists(filePath, name string) { + fullPath, _ := filepath.Abs(filePath) + if _, err := os.Stat(fullPath); err == nil { + log.Infof("Path to %s file found and valid: %s ", name, fullPath) + } else if errors.Is(err, os.ErrNotExist) { + log.Fatalf("Path to %s file not found: %s , Exiting", name, fullPath) + } else { + log.Fatalf("Path to %s file not valid: %s , err=%s, exiting", name, fullPath, err) + } +} + +func escapeToJSONstringFormat(line string) (string, error) { + // Newlines need manual escaping. + line = strings.ReplaceAll(line, "\n", "\\n") + marshalled, err := json.Marshal(line) + if err != nil { + return "", err + } + s := string(marshalled) + // Remove double quotes and return marshalled string. + return s[1 : len(s)-1], nil +} + +// ExecuteCommand uses the generic command handler to execute an arbitrary interactive command, returning +// its output wihout any filtering/matching if the command is successfully executed +var ExecuteCommand = func(command string, timeout time.Duration, context *interactive.Context) (string, error) { + tester, test := newGenericCommandTester(command, timeout, context) + result, err := test.Run() + if result == tnf.SUCCESS && err == nil { + genericTest := (*tester).(*generic.Generic) + if genericTest != nil { + matches := genericTest.Matches + if len(matches) == 1 { + return genericTest.GetMatches()[0].Match, nil + } + } + } + return "", err +} + +// ExecuteCommandAndValidate uses the generic command handler to execute an arbitrary interactive command, returning +// its output wihout any filtering/matching +var ExecuteCommandAndValidate = func(command string, timeout time.Duration, context *interactive.Context, failureCallbackFun func()) string { + tester, test := newGenericCommandTester(command, timeout, context) + test.RunAndValidateWithFailureCallback(failureCallbackFun) + genericTest := (*tester).(*generic.Generic) + gomega.Expect(genericTest).ToNot(gomega.BeNil()) + + matches := genericTest.Matches + gomega.Expect(len(matches)).To(gomega.Equal(1)) + match := genericTest.GetMatches()[0] + return match.Match +} + +func newGenericCommandTester(command string, timeout time.Duration, context *interactive.Context) (*tnf.Tester, *tnf.Test) { + log.Debugf("Executing command: %s", command) + + values := make(map[string]interface{}) + // Escapes the double quote and new line chars to make a valid json string for the command to be executed by the handler. + var err error + values["COMMAND"], err = escapeToJSONstringFormat(command) + gomega.Expect(err).To(gomega.BeNil()) + values["TIMEOUT"] = timeout.Nanoseconds() + + log.Debugf("Command handler's COMMAND string value: %s", values["COMMAND"]) + + tester, handlers := NewGenericTesterAndValidate(commandHandlerFilePath, handlerJSONSchemaFilePath, values) + test, err := tnf.NewTest(context.GetExpecter(), *tester, handlers, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(tester).ToNot(gomega.BeNil()) + return tester, test +} + +// NewGenericTesterAndValidate creates a generic handler from the json template with the var map and validate the outcome +func NewGenericTesterAndValidate(templateFile, schemaPath string, values map[string]interface{}) (*tnf.Tester, []reel.Handler) { + tester, handlers, result, err := generic.NewGenericFromMap(templateFile, handlerJSONSchemaFilePath, values) + + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(result).ToNot(gomega.BeNil()) + gomega.Expect(result.Valid()).To(gomega.BeTrue()) + gomega.Expect(handlers).ToNot(gomega.BeNil()) + gomega.Expect(tester).ToNot(gomega.BeNil()) + + return tester, handlers +} + +// GetContainerPID gets the container PID from a kubernetes node, Oc and container PID +func GetContainerPID(nodeName string, nodeOc *interactive.Oc, containerID, runtime string) string { + command := "" + switch runtime { + case "docker": //nolint:goconst // used only once + command = "chroot /host docker inspect -f '{{.State.Pid}}' " + containerID + " 2>/dev/null" + case "docker-pullable": //nolint:goconst // used only once + command = "chroot /host docker inspect -f '{{.State.Pid}}' " + containerID + " 2>/dev/null" + case "cri-o", "containerd": //nolint:goconst // used only once + command = "chroot /host crictl inspect --output go-template --template '{{.info.pid}}' " + containerID + " 2>/dev/null" + default: + ginkgo.Skip(fmt.Sprintf("Container runtime %s not supported yet for this test, skipping", runtime)) + } + return RunCommandInNode(nodeName, nodeOc, command, timeoutPid) +} + +func GetModulesFromNode(nodeName string, nodeOc *interactive.Oc) []string { + // Get the 1st column list of the modules running on the node. + // Split on the return/newline and get the list of the modules back. + //nolint:goconst // used only once + command := `chroot /host lsmod | awk '{ print $1 }' | grep -v Module` + output := RunCommandInNode(nodeName, nodeOc, command, timeoutPid) + output = strings.ReplaceAll(output, "\t", "") + return strings.Split(strings.ReplaceAll(output, "\r\n", "\n"), "\n") +} + +// ModuleInTree returns true if the module hasn't tainted the kernel with the +// out-of-tree ("O") bit. The /sys/module//taint file has the stringified +// values of all the taints it is adding to the kernel. Each bit is one letter. +// Refs: +// 1. https://www.kernel.org/doc/html/latest/admin-guide/tainted-kernels.html +// 2. https://github.com/torvalds/linux/blob/master/kernel/panic.c#L369 +func ModuleInTree(nodeName, moduleName string, nodeOc *interactive.Oc) bool { + command := `chroot /host cat /sys/module/` + moduleName + `/taint` + cmdOutput := RunCommandInNode(nodeName, nodeOc, command, timeoutPid) + return !strings.Contains(cmdOutput, "O") +} + +// RunCommandInNode runs a command on a remote kubernetes node +// takes the node name, node oc and command +// returns the command raw output +var RunCommandInNode = func(nodeName string, nodeOc *interactive.Oc, command string, timeout time.Duration) string { + context := nodeOc + tester := nodedebug.NewNodeDebug(timeout, nodeName, command, true, true) + test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + test.RunAndValidate() + return tester.Raw +} + +// AddNsenterPrefix adds the nsenter command prefix to run inside a container namespace +func AddNsenterPrefix(containerPID string) string { + return "nsenter -t " + containerPID + " -n " +} + +// StringInSlice checks a slice for a given string. +func StringInSlice(s []string, str string, contains bool) bool { + for _, v := range s { + if !contains { + if strings.TrimSpace(v) == str { + return true + } + } else { + if strings.Contains(strings.TrimSpace(v), str) { + return true + } + } + } + return false +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go new file mode 100644 index 000000000..1002913e9 --- /dev/null +++ b/pkg/utils/utils_test.go @@ -0,0 +1,237 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package utils + +import ( + "reflect" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" +) + +const ( + testString1 = "{{\"Quoted line with new line\n char and also some others \b chars not commonly used like \f, \t, \\ and \r.\"}}" + testEscapedString1 = `{{\"Quoted line with new line\\n char and also some others \u0008 chars not commonly used like \u000c, \t, \\ and \r.\"}}` +) + +func TestEscapeToJSONstringFormat(t *testing.T) { + escapedString, err := escapeToJSONstringFormat(testString1) + assert.Nil(t, err) + assert.Equal(t, testEscapedString1, escapedString) +} + +func TestArgListToMap(t *testing.T) { + testCases := []struct { + argList []string + expectedMap map[string]string + }{ + { + argList: []string{"key1=value1", "key2=value2"}, + expectedMap: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + { + argList: []string{}, + expectedMap: map[string]string{}, + }, + { + argList: []string{"key1=value1", "key2=value2", "key3"}, + expectedMap: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "", + }, + }, + } + + for _, tc := range testCases { + assert.True(t, reflect.DeepEqual(tc.expectedMap, ArgListToMap(tc.argList))) + } +} + +func TestFilterArray(t *testing.T) { + stringFilter := func(incomingVar string) bool { + return strings.Contains(incomingVar, "test") + } + + testCases := []struct { + arrayToFilter []string + expectedArray []string + }{ + { + arrayToFilter: []string{"test1", "test2"}, + expectedArray: []string{"test1", "test2"}, + }, + { + arrayToFilter: []string{"apples", "oranges"}, + expectedArray: []string{}, + }, + } + + for _, tc := range testCases { + assert.Equal(t, tc.expectedArray, FilterArray(tc.arrayToFilter, stringFilter)) + } +} + +func TestAddNsenterPrefix(t *testing.T) { + testCases := []struct { + containerID string + expectedString string + }{ + { + containerID: "1337", + expectedString: `nsenter -t 1337 -n `, + }, + { + containerID: "", + expectedString: `nsenter -t -n `, + }, + } + + for _, tc := range testCases { + assert.Equal(t, tc.expectedString, AddNsenterPrefix(tc.containerID)) + } +} + +func TestModuleInTree(t *testing.T) { + testCases := []struct { + fakeOutput string + isInTree bool + }{ + { + fakeOutput: "", + isInTree: true, + }, + { + // "K": kernel has been live patched + fakeOutput: "K", + isInTree: true, + }, + { + // "O": externally-built (“out-of-tree”) module was loaded + // "E": unsigned module was loaded + fakeOutput: "OE", + isInTree: false, + }, + { + fakeOutput: "O", + isInTree: false, + }, + } + + origFunc := RunCommandInNode + defer func() { + RunCommandInNode = origFunc + }() + for _, tc := range testCases { + RunCommandInNode = func(nodeName string, nodeOc *interactive.Oc, command string, timeout time.Duration) string { + return tc.fakeOutput + } + assert.Equal(t, tc.isInTree, ModuleInTree("testNode", "testModule", nil)) + } +} + +func TestGetModulesFromNode(t *testing.T) { + testCases := []struct { + fakeOutput string + expectedOutput []string + }{ + { + fakeOutput: `xt_nat + ip_vs_sh + vboxsf + vboxguest`, + expectedOutput: []string{ + "xt_nat", + "ip_vs_sh", + "vboxsf", + "vboxguest", + }, + }, + } + + origFunc := RunCommandInNode + defer func() { + RunCommandInNode = origFunc + }() + for _, tc := range testCases { + RunCommandInNode = func(nodeName string, nodeOc *interactive.Oc, command string, timeout time.Duration) string { + return strings.TrimSpace(tc.fakeOutput) + } + assert.Equal(t, tc.expectedOutput, GetModulesFromNode("testNode", nil)) + } +} + +//nolint:funlen +func TestStringInSlice(t *testing.T) { + testCases := []struct { + testSlice []string + testString string + containsFeature bool + expected bool + }{ + { + testSlice: []string{ + "apples", + "bananas", + "oranges", + }, + testString: "apples", + containsFeature: false, + expected: true, + }, + { + testSlice: []string{ + "apples", + "bananas", + "oranges", + }, + testString: "tacos", + containsFeature: false, + expected: false, + }, + { + testSlice: []string{ + "intree: Y", + "intree: N", + "outoftree: Y", + }, + testString: "intree:", + containsFeature: true, // Note: Turn 'on' the contains check + expected: true, + }, + { + testSlice: []string{ + "intree: Y", + "intree: N", + "outoftree: Y", + }, + testString: "intree:", + containsFeature: false, // Note: Turn 'off' the contains check + expected: false, + }, + } + + for _, tc := range testCases { + assert.Equal(t, tc.expected, StringInSlice(tc.testSlice, tc.testString, tc.containsFeature)) + } +} diff --git a/run-cnf-suites.sh b/run-cnf-suites.sh index 46b709b7a..8d258054d 100755 --- a/run-cnf-suites.sh +++ b/run-cnf-suites.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash - +set -x # defaults export OUTPUT_LOC="$PWD/test-network-function" usage() { - echo "$0 [-o OUTPUT_LOC] SUITE [... SUITE]" + echo "$0 [-o OUTPUT_LOC] [-f SUITE...] -s [SUITE...] [-l LABEL...]" echo "Call the script and list the test suites to run" echo " e.g." - echo " $0 [ARGS] generic container" - echo " will run the generic and container suites" + echo " $0 [ARGS] -f access-control lifecycle" + echo " will run the access-control and lifecycle suites" echo "" echo "Allowed suites are listed in the README." } @@ -18,46 +18,63 @@ usage_error() { exit 1 } +FOCUS="" +SKIP="" +LABEL="" +BASEDIR=$(dirname $(realpath $0)) # Parge args beginning with "-" while [[ $1 == -* ]]; do case "$1" in -h|--help|-\?) usage; exit 0;; -o) if (($# > 1)); then - OUTPUT_LOC=$2; shift 2 + OUTPUT_LOC=$2; shift else echo "-o requires an argument" 1>&2 exit 1 fi ;; - --) shift; break;; - -*) echo "invalid option: $1" 1>&2; usage_error;; + -s|--skip) + while (( "$#" >= 2 )) && ! [[ $2 = --* ]] && ! [[ $2 = -* ]] ; do + SKIP="$2|$SKIP" + shift + done;; + -f|--focus) + while (( "$#" >= 2 )) && ! [[ $2 = --* ]] && ! [[ $2 = -* ]] ; do + FOCUS="$2|$FOCUS" + shift + done;; + -l|--label) + while (( "$#" >= 2 )) && ! [[ $2 = --* ]] && ! [[ $2 = -* ]] ; do + LABEL="$2|$LABEL" + shift + done;; + -*) echo "invalid option: $1" 1>&2; usage_error;; esac + shift done # specify Junit report file name. -GINKGO_ARGS="-ginkgo.v -junit $OUTPUT_LOC -claimloc $OUTPUT_LOC -ginkgo.reportFile $OUTPUT_LOC/cnf-certification-tests_junit.xml" -FOCUS="" +GINKGO_ARGS="-junit $OUTPUT_LOC -claimloc $OUTPUT_LOC --ginkgo.junit-report $OUTPUT_LOC/cnf-certification-tests_junit.xml -ginkgo.v -test.v" -for var in "$@" -do - FOCUS="$var|$FOCUS" - # case "$var" in - # diagnostic) FOCUS="diagnostic|$FOCUS";; - # access-control) FOCUS="ac|$FOCUS";; - # affiliated-certification) FOCUS="affiliated-certification|$FOCUS";; - # lifecycle) FOCUS="lifecycle|$FOCUS";; - # platform-alteration) FOCUS="platform-alteration|$FOCUS";; - # generic) FOCUS="generic|$FOCUS";; - # observability) FOCUS="observability|$FOCUS";; - # operator) FOCUS="operator|$FOCUS";; - # networking) FOCUS="networking|$FOCUS";; - # *) usage_error;; - # esac -done - -# If no focus is set then display usage and quit with a non-zero exit code. -[ -z "$FOCUS" ] && usage_error +# Make sure the HTML output is copied to the output directory, +# even in case of a test failure +function html_output() { + if [ -f ${OUTPUT_LOC}/claim.json ]; then + echo -n "var initialjson=" > ${OUTPUT_LOC}/claimjson.js + cat ${OUTPUT_LOC}/claim.json >> ${OUTPUT_LOC}/claimjson.js + fi + cp ${BASEDIR}/script/results.html ${OUTPUT_LOC} +} +trap html_output EXIT FOCUS=${FOCUS%?} # strip the trailing "|" from the concatenation +SKIP=${SKIP%?} # strip the trailing "|" from the concatenation +LABEL=${LABEL%?} # strip the trailing "|" from the concatenation +res=`oc version | grep Server` +if [ -z "$res" ] +then + echo "Minikube or similar detected" + export TNF_NON_OCP_CLUSTER=true +fi # Run cnf-feature-deploy test container if not running inside a container # cgroup file doesn't exist on MacOS. Consider that as not running in container as well if [[ ! -f "/proc/1/cgroup" ]] || grep -q init\.scope /proc/1/cgroup; then @@ -66,5 +83,30 @@ if [[ ! -f "/proc/1/cgroup" ]] || grep -q init\.scope /proc/1/cgroup; then cd .. fi -echo "Running with focus '$FOCUS'. Report will be output to '$OUTPUT_LOC'" -cd ./test-network-function && ./test-network-function.test -ginkgo.focus="$FOCUS" ${GINKGO_ARGS} +if [[ -z "${TNF_PARTNER_SRC_DIR}" ]]; then + echo "env var \"TNF_PARTNER_SRC_DIR\" not set, running the script without updating infra" +else + make -C $TNF_PARTNER_SRC_DIR install-partner-pods +fi + +echo "Running with focus '$FOCUS'" +echo "Running with skip '$SKIP'" +echo "Running with label filter '$LABEL'" +echo "Report will be output to '$OUTPUT_LOC'" +echo "ginkgo arguments '${GINKGO_ARGS}'" +FOCUS_STRING="" +SKIP_STRING="" +LABEL_STRING="" +if [ -n "$FOCUS" ]; then + FOCUS_STRING=-ginkgo.focus="$FOCUS" + if [ -n "$SKIP" ]; then + SKIP_STRING=-ginkgo.skip="$SKIP" + fi + if [ -n "$LABEL" ]; then + LABEL_STRING=-ginkgo.label-filter="$LABEL" + fi +else + echo "No test suite (-f) was set, so only diagnostic functions will run. Skip patterns (-s) and labels (-l) will be ignored". +fi + +cd ./test-network-function && ./test-network-function.test $FOCUS_STRING $SKIP_STRING $LABEL_STRING ${GINKGO_ARGS} diff --git a/run-tnf-container.sh b/run-tnf-container.sh index e0d3dd00c..559a1837d 100755 --- a/run-tnf-container.sh +++ b/run-tnf-container.sh @@ -27,10 +27,13 @@ export TNF_OFFICIAL_ORG=quay.io/testnetworkfunction/ export TNF_OFFICIAL_IMAGE="${TNF_OFFICIAL_ORG}${TNF_IMAGE_NAME}:${TNF_IMAGE_TAG}" export TNF_CMD="./run-cnf-suites.sh" export OUTPUT_ARG="-o" +export FOCUS_ARG="-f" +export SKIP_ARG="-s" +export CONTAINER_NETWORK_MODE='host' usage() { read -d '' usage_prompt <<- EOF - Usage: $0 -t TNFCONFIG -o OUTPUT_LOC [-i IMAGE] [-k KUBECONFIG] [-n NETWORK_MODE] [-d DNS_RESOLVER_ADDRESS] SUITE [... SUITE] + Usage: $0 -t TNFCONFIG -o OUTPUT_LOC [-i IMAGE] [-k KUBECONFIG] [-n NETWORK_MODE] [-d DNS_RESOLVER_ADDRESS] -f SUITE [... SUITE] Configure and run the containerised TNF test offering. @@ -46,6 +49,8 @@ usage() { -n: set the network mode of the container. -d: set the DNS resolver address for the test containers started by docker, may be required with certain docker version if the kubeconfig contains host names + -f: Set the suites that should be tested, multiple suites can be supplied + -s: Set the test cases that should be skipped Kubeconfig lookup order 1. If -k is specified, use the paths provided with the -k option. @@ -54,21 +59,21 @@ usage() { (currently: $HOME/.kube/config). Examples - $0 -t ~/tnf/config -o ~/tnf/output diagnostic generic + $0 -t ~/tnf/config -o ~/tnf/output -f networking access-control -s access-control-host-resource-PRIVILEGED_POD Because -k is omitted, $(basename $0) will first try to autodiscover local kubeconfig files. - If it succeeds, the diagnostic and generic tests will be run using the autodiscovered configuration. + If it succeeds, the networking and access-control tests will be run using the autodiscovered configuration. The test results will be saved to the '~/tnf/output' directory on the host. - $0 -k ~/.kube/ABC:~/.kube/DEF -t ~/tnf/config -o ~/tnf/output diagnostic generic + $0 -k ~/.kube/ABC:~/.kube/DEF -t ~/tnf/config -o ~/tnf/output -f access-control networking The command will bind two kubeconfig files (~/.kube/ABC and ~/.kube/DEF) to the TNF container, - run the diagnostic and generic tests, and save the test results into the '~/tnf/output' directory + run the access-control and networking tests, and save the test results into the '~/tnf/output' directory on the host. - $0 -i custom-tnf-image:v1.2-dev -t ~/tnf/config -o ~/tnf/output diagnostic generic + $0 -i custom-tnf-image:v1.2-dev -t ~/tnf/config -o ~/tnf/output -f access-control networking - The command will run the diagnostic and generic tests as implemented in the custom-tnf-image:v1.2-dev + The command will run the access-control and networking tests as implemented in the custom-tnf-image:v1.2-dev local image set by the -i parameter. The test results will be saved to the '~/tnf/output' directory. Test suites @@ -132,50 +137,80 @@ perform_kubeconfig_autodiscovery # Parge args beginning with - while [[ $1 == -* ]]; do - echo "$1 $2" case "$1" in -h|--help|-\?) usage; exit 0;; -k) if (($# > 1)); then export LOCAL_KUBECONFIG=$2 unset kubeconfig_autodiscovery_source - shift 2 + shift else echo "-k requires an argument" 1>&2 exit 1 - fi ;; + fi + echo "-k $LOCAL_KUBECONFIG" + ;; -t) if (($# > 1)); then - export LOCAL_TNF_CONFIG=$2; shift 2 + export LOCAL_TNF_CONFIG=$2; shift else echo "-t requires an argument" 1>&2 exit 1 - fi ;; + fi + echo "-t $LOCAL_TNF_CONFIG" + ;; -o) if (($# > 1)); then - export OUTPUT_LOC=$2; shift 2 + export OUTPUT_LOC=$2; shift else echo "-o requires an argument" 1>&2 exit 1 - fi ;; + fi + echo "-o $OUTPUT_LOC" + ;; -i) if (($# > 1)); then - export TNF_IMAGE=$2; shift 2 + export TNF_IMAGE=$2; shift else echo "-i requires an argument" 1>&2 exit 1 - fi ;; + fi + echo "-i $TNF_IMAGE" + ;; -n) if (($# > 1)); then - export CONTAINER_NETWORK_MODE=$2; shift 2 + export CONTAINER_NETWORK_MODE=$2; shift else echo "-n requires an argument" 1>&2 exit 1 - fi ;; + fi + echo "-n $CONTAINER_NETWORK_MODE" + ;; -d) if (($# > 1)); then export DNS_ARG=$2; shift 2 else echo "-d requires an argument" 1>&2 exit 1 - fi ;; + fi + echo "-d $DNS_ARGS" + ;; + -s) + TNF_SKIP_SUITES="" + while (( "$#" >= 2 )) && ! [[ $2 = --* ]] && ! [[ $2 = -* ]] ; do + TNF_SKIP_SUITES="$2 $TNF_SKIP_SUITES" + shift + done + export TNF_SKIP_SUITES + echo "-s $TNF_SKIP_SUITES" + ;; + -f) + TNF_FOCUS_SUITES="" + while (( "$#" >= 2 )) && ! [[ $2 = --* ]] && ! [[ $2 = -* ]] ; do + TNF_FOCUS_SUITES="$2 $TNF_FOCUS_SUITES" + shift + done + export TNF_FOCUS_SUITES + echo "-f $TNF_FOCUS_SUITES" + ;; --) shift; break;; -*) echo "invalid option: $1" 1>&2; usage_error;; esac + shift done display_kubeconfig_autodiscovery_summary @@ -185,4 +220,4 @@ cd script ./run-cfd-container.sh -./run-container.sh "$@" \ No newline at end of file +./run-container.sh "$@" \ No newline at end of file diff --git a/script/exec-container.sh b/script/exec-container.sh new file mode 100755 index 000000000..312b4f425 --- /dev/null +++ b/script/exec-container.sh @@ -0,0 +1,10 @@ +# proof of concept bash script to execute any commands in running pods +set -x +NAMESPACE=$1 +POD_NAME=$2 +COMMAND=$3 +NODE_NAME=$(oc get pods -n $NAMESPACE $POD_NAME --no-headers=true -ocustom-columns=node:.spec.nodeName) +CONTAINER_ID=$(oc get pods -ojsonpath={.status.containerStatuses[0].containerID} -n $NAMESPACE $POD_NAME | awk -F "//" '{print $2}') +CONTAINER_PID=$(oc debug node/$NODE_NAME -- chroot /host crictl inspect --output go-template --template '{{.info.pid}}' $CONTAINER_ID 2>/dev/null) +oc debug node/$NODE_NAME -- nsenter nsenter -t $CONTAINER_PID -n $COMMAND + diff --git a/script/results.html b/script/results.html new file mode 100644 index 000000000..7444b5c10 --- /dev/null +++ b/script/results.html @@ -0,0 +1,271 @@ + + + + + TNF claim.json parser + + + + + + + +
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + + + + + + + + + + + + diff --git a/script/run-cfd-container.sh b/script/run-cfd-container.sh index 6d5b45fa3..1d92d7ce3 100755 --- a/script/run-cfd-container.sh +++ b/script/run-cfd-container.sh @@ -1,11 +1,12 @@ #!/usr/bin/env bash -if [ "$VERIFY_CNF_FEATURES" == "true" ] && [ "$TNF_MINIKUBE_ONLY" != "true" ]; then +if [ "$TNF_RUN_CFD_TEST" == "true" ]; then export TNF_IMAGE_NAME=cnf-tests - export TNF_IMAGE_TAG=4.6 + export TNF_IMAGE_TAG="${TNF_CFD_IMAGE_TAG:-4.6}" export TNF_OFFICIAL_ORG=quay.io/openshift-kni/ export TNF_OFFICIAL_IMAGE="${TNF_OFFICIAL_ORG}${TNF_IMAGE_NAME}:${TNF_IMAGE_TAG}" + export TNF_CFD_SKIP="${TNF_CFD_SKIP:-performance|sriov|ptp|sctp|xt_u32|dpdk|ovn}" export TNF_CMD="/usr/bin/test-run.sh" export OUTPUT_ARG="--junit" export CONTAINER_NETWORK_MODE="host" @@ -19,7 +20,7 @@ if [ "$VERIFY_CNF_FEATURES" == "true" ] && [ "$TNF_MINIKUBE_ONLY" != "true" ]; t # For older verions of docker, dns server may need to be set explicitly, e.g. # # export DNS_ARG=172.0.0.53 - ./run-container.sh -ginkgo.v -ginkgo.skip="performance|sriov|ptp|sctp|xt_u32|dpdk|ovn" + ./run-container.sh -ginkgo.v -ginkgo.skip=${TNF_CFD_SKIP} else # removing report if not running, so the final claim won't include stale reports rm -f ${OUTPUT_LOC}/validation_junit.xml diff --git a/script/run-container.sh b/script/run-container.sh index 5e9c9f6b1..666f47109 100755 --- a/script/run-container.sh +++ b/script/run-container.sh @@ -22,9 +22,9 @@ configure_tnf_container_client CONTAINER_TNF_DIR=/usr/tnf CONTAINER_TNF_KUBECONFIG_FILE_BASE_PATH="$CONTAINER_TNF_DIR/kubeconfig/config" CONTAINER_DEFAULT_NETWORK_MODE=bridge -CONTAINER_DEFAULT_TNF_MINIKUBE_ONLY=false CONTAINER_DEFAULT_TNF_NON_INTRUSIVE_ONLY=true CONTAINER_DEFAULT_TNF_DISABLE_CONFIG_AUTODISCOVER=false +LOG_LEVEL_DEFAULT=info get_container_tnf_kubeconfig_path_from_index() { local local_path_index="$1" @@ -71,15 +71,14 @@ for local_path_index in "${!local_kubeconfig_paths[@]}"; do container_path=$(get_container_tnf_kubeconfig_path_from_index $local_path_index) container_tnf_kubeconfig_paths+=($container_path) - container_tnf_kubeconfig_volume_bindings+=("$local_path:$container_path:ro") + container_tnf_kubeconfig_volume_bindings+=("$local_path:$container_path:Z") done TNF_IMAGE="${TNF_IMAGE:-$TNF_OFFICIAL_IMAGE}" CONTAINER_NETWORK_MODE="${CONTAINER_NETWORK_MODE:-$CONTAINER_DEFAULT_NETWORK_MODE}" -CONTAINER_TNF_MINIKUBE_ONLY="${TNF_MINIKUBE_ONLY:-$CONTAINER_DEFAULT_TNF_MINIKUBE_ONLY}" CONTAINER_TNF_NON_INTRUSIVE_ONLY="${TNF_NON_INTRUSIVE_ONLY:-$CONTAINER_DEFAULT_TNF_NON_INTRUSIVE_ONLY}" CONTAINER_TNF_DISABLE_CONFIG_AUTODISCOVER="${TNF_DISABLE_CONFIG_AUTODISCOVER:-$CONTAINER_DEFAULT_TNF_DISABLE_CONFIG_AUTODISCOVER}" - +LOG_LEVEL="${LOG_LEVEL:-$LOG_LEVEL_DEFAULT}" display_config_summary # Construct new $KUBECONFIG env variable containing all paths to kubeconfigs mounted to the container. @@ -103,9 +102,11 @@ ${TNF_CONTAINER_CLIENT} run --rm $DNS_ARG \ $CONFIG_VOLUME_MOUNT_ARG \ -v $OUTPUT_LOC:$CONTAINER_TNF_DIR/claim:Z \ -e KUBECONFIG=$CONTAINER_TNF_KUBECONFIG \ - -e TNF_MINIKUBE_ONLY=$CONTAINER_TNF_MINIKUBE_ONLY \ -e TNF_NON_INTRUSIVE_ONLY=$CONTAINER_TNF_NON_INTRUSIVE_ONLY \ -e TNF_DISABLE_CONFIG_AUTODISCOVER=$CONTAINER_TNF_DISABLE_CONFIG_AUTODISCOVER \ + -e TNF_PARTNER_REPO=$TNF_PARTNER_REPO \ + -e TNF_DEPLOYMENT_TIMEOUT=$TNF_DEPLOYMENT_TIMEOUT \ + -e LOG_LEVEL=$LOG_LEVEL \ -e PATH=/usr/bin:/usr/local/oc/bin \ $TNF_IMAGE \ - $TNF_CMD $OUTPUT_ARG $CONTAINER_TNF_DIR/claim "$@" + $TNF_CMD $OUTPUT_ARG $CONTAINER_TNF_DIR/claim $FOCUS_ARG $TNF_FOCUS_SUITES $SKIP_ARG $TNF_SKIP_SUITES "$@" diff --git a/test-network-function/accesscontrol/doc.go b/test-network-function/accesscontrol/doc.go index d5053f749..55ef913f1 100644 --- a/test-network-function/accesscontrol/doc.go +++ b/test-network-function/accesscontrol/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/test-network-function/accesscontrol/suite.go b/test-network-function/accesscontrol/suite.go index c11301d76..85e9ad993 100644 --- a/test-network-function/accesscontrol/suite.go +++ b/test-network-function/accesscontrol/suite.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,229 +18,416 @@ package accesscontrol import ( "fmt" - "strconv" "strings" - "github.com/onsi/ginkgo" - ginkgoconfig "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" log "github.com/sirupsen/logrus" - configpkg "github.com/test-network-function/test-network-function/pkg/config" + "github.com/test-network-function/test-network-function/pkg/config" + "github.com/test-network-function/test-network-function/pkg/config/configsections" "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/automountservice" "github.com/test-network-function/test-network-function/pkg/tnf/handlers/clusterrolebinding" containerpkg "github.com/test-network-function/test-network-function/pkg/tnf/handlers/container" "github.com/test-network-function/test-network-function/pkg/tnf/handlers/rolebinding" - "github.com/test-network-function/test-network-function/pkg/tnf/handlers/serviceaccount" "github.com/test-network-function/test-network-function/pkg/tnf/interactive" "github.com/test-network-function/test-network-function/pkg/tnf/reel" "github.com/test-network-function/test-network-function/pkg/tnf/testcases" + "github.com/test-network-function/test-network-function/pkg/utils" "github.com/test-network-function/test-network-function/test-network-function/common" "github.com/test-network-function/test-network-function/test-network-function/identifiers" "github.com/test-network-function/test-network-function/test-network-function/results" ) +const ( + // ocGetCrPluralNameFormat is the CR name to use with "oc get ". + ocGetCrPluralNameFormat = "oc get crd %s -o jsonpath='{.spec.names.plural}'" + + // ocGetCrNamespaceFormat is the "oc get" format string to get the namespaced-only resources created for a given CRD. + ocGetCrNamespaceFormat = "oc get %s -A -o go-template='{{range .items}}{{if .metadata.namespace}}{{.metadata.name}},{{.metadata.namespace}}{{\"\n\"}}{{end}}{{end}}'" +) + +var ( + invalidNamespacePrefixes = []string{ + "default", + "openshift-", + "istio-", + "aspenmesh-", + } +) + var _ = ginkgo.Describe(common.AccessControlTestKey, func() { - if testcases.IsInFocus(ginkgoconfig.GinkgoConfig.FocusStrings, common.AccessControlTestKey) { - config := common.GetTestConfiguration() - log.Infof("Test Configuration: %s", config) + conf, _ := ginkgo.GinkgoConfiguration() + if testcases.IsInFocus(conf.FocusStrings, common.AccessControlTestKey) { + env := config.GetTestEnvironment() + ginkgo.BeforeEach(func() { + env.LoadAndRefresh() + gomega.Expect(len(env.PodsUnderTest)).ToNot(gomega.Equal(0)) + gomega.Expect(len(env.ContainersUnderTest)).ToNot(gomega.Equal(0)) + }) - containersUnderTest := common.CreateContainersUnderTest(config) - log.Info(containersUnderTest) + ginkgo.ReportAfterEach(results.RecordResult) + ginkgo.AfterEach(env.CloseLocalShellContext) - for _, containerUnderTest := range containersUnderTest { - testNamespace(containerUnderTest.Oc) - } + testNamespace(env) - for _, containerUnderTest := range containersUnderTest { - testRoles(containerUnderTest.Oc.GetPodName(), containerUnderTest.Oc.GetPodNamespace()) - } + testRoles(env) - // Former "container" tests defer ginkgo.GinkgoRecover() - // Run the tests that interact with the containers + // Run the tests that interact with the pods ginkgo.When("under test", func() { - conf := configpkg.GetConfigInstance() - cnfsInTest := conf.CNFs - gomega.Expect(cnfsInTest).ToNot(gomega.BeNil()) - for _, cnf := range cnfsInTest { - cnf := cnf - var containerFact = testcases.ContainerFact{Namespace: cnf.Namespace, Name: cnf.Name, ContainerCount: 0, HasClusterRole: false, Exists: true} - // Gather facts for containers - podFacts, err := testcases.LoadCnfTestCaseSpecs(testcases.GatherFacts) + allTests := testcases.GetConfiguredPodTests() + for _, testType := range allTests { + testFile, err := testcases.LoadConfiguredTestFile(common.ConfiguredTestFile) + gomega.Expect(testFile).ToNot(gomega.BeNil()) gomega.Expect(err).To(gomega.BeNil()) - context := common.GetContext() - // Collect container facts - for _, factsTest := range podFacts.TestCase { - args := strings.Split(fmt.Sprintf(factsTest.Command, cnf.Name, cnf.Namespace), " ") - cnfInTest := containerpkg.NewPod(args, cnf.Name, cnf.Namespace, factsTest.ExpectedStatus, factsTest.ResultType, factsTest.Action, common.DefaultTimeout) - test, err := tnf.NewTest(context.GetExpecter(), cnfInTest, []reel.Handler{cnfInTest}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(test).ToNot(gomega.BeNil()) - _, err = test.Run() - gomega.Expect(err).To(gomega.BeNil()) - if factsTest.Name == string(testcases.ContainerCount) { - containerFact.ContainerCount, _ = strconv.Atoi(cnfInTest.Facts()) - } else if factsTest.Name == string(testcases.ServiceAccountName) { - containerFact.ServiceAccount = cnfInTest.Facts() - } else if factsTest.Name == string(testcases.Name) { - containerFact.Name = cnfInTest.Facts() - gomega.Expect(containerFact.Name).To(gomega.Equal(cnf.Name)) - if strings.Compare(containerFact.Name, cnf.Name) > 0 { - containerFact.Exists = true - } - } - } - // loop through various cnfs test - if !containerFact.Exists { - ginkgo.It(fmt.Sprintf("is running test pod exists : %s/%s for test command : %s", containerFact.Namespace, containerFact.Name, "POD EXISTS"), func() { - gomega.Expect(containerFact.Exists).To(gomega.BeTrue()) - }) - continue - } - for _, testType := range cnf.Tests { - testFile, err := testcases.LoadConfiguredTestFile(common.ConfiguredTestFile) - gomega.Expect(testFile).ToNot(gomega.BeNil()) - gomega.Expect(err).To(gomega.BeNil()) - testConfigure := testcases.ContainsConfiguredTest(testFile.CnfTest, testType) - renderedTestCase, err := testConfigure.RenderTestCaseSpec(testcases.Cnf, testType) - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(renderedTestCase).ToNot(gomega.BeNil()) - for _, testCase := range renderedTestCase.TestCase { - if !testCase.SkipTest { - if testCase.ExpectedType == testcases.Function { - for _, val := range testCase.ExpectedStatus { - testCase.ExpectedStatusFn(cnf.Name, testcases.StatusFunctionType(val)) - } - } - if testCase.Loop > 0 { - runTestsOnCNF(containerFact.ContainerCount, testCase, testType, containerFact, context) - } else { - runTestsOnCNF(testCase.Loop, testCase, testType, containerFact, context) - } - } + testConfigure := testcases.ContainsConfiguredTest(testFile.CnfTest, testType) + renderedTestCase, err := testConfigure.RenderTestCaseSpec(testcases.Cnf, testType) + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(renderedTestCase).ToNot(gomega.BeNil()) + + // Loop through test cases + for i := range renderedTestCase.TestCase { + if !renderedTestCase.TestCase[i].SkipTest { + runTestOnPods(env, &renderedTestCase.TestCase[i], testType) } } } }) - } }) -//nolint:gocritic // ignore hugeParam error. Pointers to loop iterator vars are bad and `testCmd` is likely to be such. -func runTestsOnCNF(containerCount int, testCmd testcases.BaseTestCase, - testType string, facts testcases.ContainerFact, context *interactive.Context) { - ginkgo.It(fmt.Sprintf("is running test for : %s/%s for test command : %s", facts.Namespace, facts.Name, testCmd.Name), func() { - defer results.RecordResult(identifiers.TestHostResourceIdentifier) - containerCount := containerCount - testType := testType - facts := facts - testCmd := testCmd - var args []interface{} - if testType == testcases.PrivilegedRoles { - args = []interface{}{facts.Namespace, facts.Namespace, facts.ServiceAccount} - } else { - args = []interface{}{facts.Name, facts.Namespace} - } - if containerCount > 0 { - count := 0 - for count < containerCount { - argsCount := append(args, count) - cmdArgs := strings.Split(fmt.Sprintf(testCmd.Command, argsCount...), " ") - cnfInTest := containerpkg.NewPod(cmdArgs, facts.Name, facts.Namespace, testCmd.ExpectedStatus, testCmd.ResultType, testCmd.Action, common.DefaultTimeout) - gomega.Expect(cnfInTest).ToNot(gomega.BeNil()) - test, err := tnf.NewTest(context.GetExpecter(), cnfInTest, []reel.Handler{cnfInTest}, context.GetErrorChannel()) +type failedTcInfo struct { + tc string + containerIdx int + ns string +} + +func addFailedTcInfo(failedTcs map[string][]failedTcInfo, tc, pod, ns string, containerIdx int) { + if tcs, exists := failedTcs[pod]; exists { + tcs = append(tcs, failedTcInfo{tc: tc, containerIdx: containerIdx, ns: ns}) + failedTcs[pod] = tcs + } else { + failedTcs[pod] = []failedTcInfo{{tc: tc, containerIdx: containerIdx, ns: ns}} + } +} + +//nolint:funlen // ignore hugeParam error. Pointers to loop iterator vars are bad and `testCmd` is likely to be such. +func runTestOnPods(env *config.TestEnvironment, testCmd *testcases.BaseTestCase, testType string) { + const noContainerIdx = -1 + testID := identifiers.XformToGinkgoItIdentifierExtended(identifiers.TestHostResourceIdentifier, testCmd.Name) + ginkgo.It(testID, ginkgo.Label(testID), func() { + context := env.GetLocalShellContext() + failedTcs := map[string][]failedTcInfo{} // maps a pod name to a slice of failed TCs + for _, podUnderTest := range env.PodsUnderTest { + if testCmd.ExpectedType == testcases.Function { + for _, val := range testCmd.ExpectedStatus { + testCmd.ExpectedStatusFn(podUnderTest.Name, testcases.StatusFunctionType(val)) + } + } + var args []interface{} + if testType == testcases.PrivilegedRoles { + args = []interface{}{podUnderTest.Namespace, podUnderTest.Namespace, podUnderTest.ServiceAccount} + } else { + args = []interface{}{podUnderTest.Name, podUnderTest.Namespace} + } + var count int + if testCmd.Loop > 0 { + count = podUnderTest.ContainerCount + } else { + count = testCmd.Loop + } + + if count > 0 { + count := 0 + for count < podUnderTest.ContainerCount { + ginkgo.By(fmt.Sprintf("Executing TC %s on pod %s (ns %s), container index %d", testCmd.Name, podUnderTest.Namespace, podUnderTest.Name, count)) + argsCount := append(args, count) //nolint:gocritic + cmd := fmt.Sprintf(testCmd.Command, argsCount...) + cmdArgs := strings.Split(cmd, " ") + cnfInTest := containerpkg.NewPod(cmdArgs, podUnderTest.Name, podUnderTest.Namespace, testCmd.ExpectedStatus, testCmd.ResultType, testCmd.Action, common.DefaultTimeout) + gomega.Expect(cnfInTest).ToNot(gomega.BeNil()) + test, err := tnf.NewTest(context.GetExpecter(), cnfInTest, []reel.Handler{cnfInTest}, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(test).ToNot(gomega.BeNil()) + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("FAILURE: Command sent: %s, Expectations: %v", cmd, testCmd.ExpectedStatus) + addFailedTcInfo(failedTcs, testCmd.Name, podUnderTest.Name, podUnderTest.Namespace, count) + }, func(e error) { + tnf.ClaimFilePrintf("ERROR: Command sent: %s, Expectations: %v, Error: %v", cmd, testCmd.ExpectedStatus, e) + addFailedTcInfo(failedTcs, testCmd.Name, podUnderTest.Name, podUnderTest.Namespace, count) + }) + count++ + } + } else { + ginkgo.By(fmt.Sprintf("Executing TC %s on pod %s (ns %s)", testCmd.Name, podUnderTest.Namespace, podUnderTest.Name)) + cmd := fmt.Sprintf(testCmd.Command, args...) + cmdArgs := strings.Split(cmd, " ") + podTest := containerpkg.NewPod(cmdArgs, podUnderTest.Name, podUnderTest.Namespace, testCmd.ExpectedStatus, testCmd.ResultType, testCmd.Action, common.DefaultTimeout) + gomega.Expect(podTest).ToNot(gomega.BeNil()) + test, err := tnf.NewTest(context.GetExpecter(), podTest, []reel.Handler{podTest}, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) gomega.Expect(test).ToNot(gomega.BeNil()) - testResult, err := test.Run() - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) - count++ - } - } else { - cmdArgs := strings.Split(fmt.Sprintf(testCmd.Command, args...), " ") - cnfInTest := containerpkg.NewPod(cmdArgs, facts.Name, facts.Namespace, testCmd.ExpectedStatus, testCmd.ResultType, testCmd.Action, common.DefaultTimeout) - gomega.Expect(cnfInTest).ToNot(gomega.BeNil()) - test, err := tnf.NewTest(context.GetExpecter(), cnfInTest, []reel.Handler{cnfInTest}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(test).ToNot(gomega.BeNil()) - testResult, err := test.Run() - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("FAILURE: Command sent: %s, Expectations: %v", cmd, testCmd.ExpectedStatus) + addFailedTcInfo(failedTcs, testCmd.Name, podUnderTest.Name, podUnderTest.Namespace, noContainerIdx) + }, func(e error) { + tnf.ClaimFilePrintf("ERROR: Command sent: %s, Expectations: %v, Error: %v", cmd, testCmd.ExpectedStatus, e) + addFailedTcInfo(failedTcs, testCmd.Name, podUnderTest.Name, podUnderTest.Namespace, noContainerIdx) + }) + } + } + + if n := len(failedTcs); n > 0 { + log.Debugf("Failed TCs: %+v", failedTcs) + ginkgo.Fail(fmt.Sprintf("%d pods failed the test.", n)) } }) } -func testNamespace(oc *interactive.Oc) { - pod := oc.GetPodName() - container := oc.GetPodContainerName() - ginkgo.When(fmt.Sprintf("Reading namespace of %s/%s", pod, container), func() { - ginkgo.It("Should not be 'default' and should not begin with 'openshift-'", func() { - defer results.RecordResult(identifiers.TestNamespaceBestPracticesIdentifier) - gomega.Expect(oc.GetPodNamespace()).To(gomega.Not(gomega.Equal("default"))) - gomega.Expect(oc.GetPodNamespace()).To(gomega.Not(gomega.HavePrefix("openshift-"))) +func getCrsNamespaces(crdName, crdKind string, context *interactive.Context) (map[string]string, error) { + gomega.Expect(crdKind).NotTo(gomega.BeEmpty()) + getCrNamespaceCommand := fmt.Sprintf(ocGetCrNamespaceFormat, crdKind) + cmdOut := utils.ExecuteCommandAndValidate(getCrNamespaceCommand, common.DefaultTimeout, context, func() { + common.TcClaimLogPrintf("CRD %s: Failed to get CRs (kind=%s)", crdName, crdKind) + }) + + return parseCrOutput(cmdOut) +} + +func parseCrOutput(rawOutput string) (map[string]string, error) { + const crNameFieldIdx = 0 + const namespaceFieldIdx = 1 + const expectedNumFields = 2 + crNamespaces := map[string]string{} + if rawOutput == "" { + // Filter out empty (0 CRs) output. + return crNamespaces, nil + } + + lines := strings.Split(rawOutput, "\n") + for _, line := range lines { + lineFields := strings.Split(line, ",") + if len(lineFields) != expectedNumFields { + return crNamespaces, fmt.Errorf("failed to parse output line %s", line) + } + crNamespaces[lineFields[crNameFieldIdx]] = lineFields[namespaceFieldIdx] + } + + return crNamespaces, nil +} + +func testCrsNamespaces(crNames, configNamespaces []string, context *interactive.Context) map[string][]string { + invalidCrs := map[string][]string{} + for _, crdName := range crNames { + getCrPluralNameCommand := fmt.Sprintf(ocGetCrPluralNameFormat, crdName) + crdPluralName := utils.ExecuteCommandAndValidate(getCrPluralNameCommand, common.DefaultTimeout, context, func() { + common.TcClaimLogPrintf("CRD %s: Failed to get CR plural name.", crdName) + }) + + crNamespaces, err := getCrsNamespaces(crdName, crdPluralName, context) + if err != nil { + ginkgo.Fail(fmt.Sprintf("Failed to get CRs for CRD %s - Error: %v", crdName, err)) + } + + ginkgo.By(fmt.Sprintf("CRD %s has %d CRs (plural name: %s).", crdName, len(crNamespaces), crdPluralName)) + for crName, namespace := range crNamespaces { + ginkgo.By(fmt.Sprintf("Checking CR %s - Namespace %s", crName, namespace)) + if !utils.StringInSlice(configNamespaces, namespace, false) { + common.TcClaimLogPrintf("CRD: %s (kind:%s) - CR %s has an invalid namespace (%s)", crdName, crdPluralName, crName, namespace) + if crNames, exists := invalidCrs[crdName]; exists { + invalidCrs[crdName] = append(crNames, crName) + } else { + invalidCrs[crdName] = []string{crName} + } + } + } + } + return invalidCrs +} + +func testNamespace(env *config.TestEnvironment) { + ginkgo.When("test CNF namespaces", func() { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestNamespaceBestPracticesIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + ginkgo.By(fmt.Sprintf("CNF resources' namespaces should not have any of the following prefixes: %v", invalidNamespacePrefixes)) + var failedNamespaces []string + for _, namespace := range env.NameSpacesUnderTest { + ginkgo.By(fmt.Sprintf("Checking namespace %s", namespace)) + for _, invalidPrefix := range invalidNamespacePrefixes { + if strings.HasPrefix(namespace, invalidPrefix) { + common.TcClaimLogPrintf("Namespace %s has invalid prefix %s", namespace, invalidPrefix) + failedNamespaces = append(failedNamespaces, namespace) + } + } + } + + if failedNamespacesNum := len(failedNamespaces); failedNamespacesNum > 0 { + ginkgo.Fail(fmt.Sprintf("Found %d namespaces with an invalid prefix.", failedNamespacesNum)) + } + + ginkgo.By(fmt.Sprintf("CNF pods' should belong to any of the configured namespaces: %v", env.NameSpacesUnderTest)) + + if nonValidPodsNum := len(env.Config.NonValidPods); nonValidPodsNum > 0 { + for _, invalidPod := range env.Config.NonValidPods { + common.TcClaimLogPrintf("Pod %s has invalid namespace %s", invalidPod.Name, invalidPod.Namespace) + } + + ginkgo.Fail(fmt.Sprintf("Found %d pods under test belonging to invalid namespaces.", nonValidPodsNum)) + } + + ginkgo.By(fmt.Sprintf("CRs from autodiscovered CRDs should belong to the configured namespaces: %v", env.NameSpacesUnderTest)) + invalidCrs := testCrsNamespaces(env.CrdNames, env.NameSpacesUnderTest, env.GetLocalShellContext()) + + if invalidCrsNum := len(invalidCrs); invalidCrsNum > 0 { + for crdName, crs := range invalidCrs { + for _, crName := range crs { + common.TcClaimLogPrintf("CRD %s - CR %s has an invalid namespace.", crdName, crName) + } + } + ginkgo.Fail(fmt.Sprintf("Found %d CRs belonging to invalid namespaces.", invalidCrsNum)) + } }) }) } -func testRoles(podName, podNamespace string) { - var serviceAccountName string - ginkgo.When(fmt.Sprintf("Testing roles and privileges of %s/%s", podNamespace, podName), func() { - testServiceAccount(podName, podNamespace, &serviceAccountName) - testRoleBindings(podNamespace, &serviceAccountName) - testClusterRoleBindings(podNamespace, &serviceAccountName) +func testRoles(env *config.TestEnvironment) { + testServiceAccount(env) + testRoleBindings(env) + testClusterRoleBindings(env) + testAutomountService(env) +} + +func testServiceAccount(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestPodServiceAccountBestPracticesIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + ginkgo.By("Should have a valid ServiceAccount name") + failedPods := []*configsections.Pod{} + for _, podUnderTest := range env.PodsUnderTest { + ginkgo.By(fmt.Sprintf("Testing service account for pod %s (ns: %s)", podUnderTest.Name, podUnderTest.Namespace)) + if podUnderTest.ServiceAccount == "" { + tnf.ClaimFilePrintf("Pod %s (ns: %s) doesn't have a service account name.", podUnderTest.Name, podUnderTest.Namespace) + failedPods = append(failedPods, podUnderTest) + } + } + if n := len(failedPods); n > 0 { + log.Debugf("Pods without service account: %+v", failedPods) + ginkgo.Fail(fmt.Sprintf("%d pods don't have a service account name.", n)) + } }) } -func testServiceAccount(podName, podNamespace string, serviceAccountName *string) { - ginkgo.It("Should have a valid ServiceAccount name", func() { - defer results.RecordResult(identifiers.TestPodServiceAccountBestPracticesIdentifier) - context := common.GetContext() - tester := serviceaccount.NewServiceAccount(common.DefaultTimeout, podName, podNamespace) - test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - testResult, err := test.Run() - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) - gomega.Expect(err).To(gomega.BeNil()) - *serviceAccountName = tester.GetServiceAccountName() - gomega.Expect(*serviceAccountName).ToNot(gomega.BeEmpty()) +//nolint:funlen +func testAutomountService(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestPodAutomountServiceAccountIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + ginkgo.By("Should have automountServiceAccountToken set to false") + msg := []string{} + for _, podUnderTest := range env.PodsUnderTest { + ginkgo.By(fmt.Sprintf("check the existence of pod service account %s (ns= %s )", podUnderTest.Namespace, podUnderTest.Name)) + gomega.Expect(podUnderTest.ServiceAccount).ToNot(gomega.BeEmpty()) + context := env.GetLocalShellContext() + tester := automountservice.NewAutomountService(automountservice.WithNamespace(podUnderTest.Namespace), automountservice.WithServiceAccount(podUnderTest.ServiceAccount)) + test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + test.RunAndValidate() + serviceAccountToken := tester.Token() + tester = automountservice.NewAutomountService(automountservice.WithNamespace(podUnderTest.Namespace), automountservice.WithPodname(podUnderTest.Name)) + test, err = tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + test.RunAndValidate() + podToken := tester.Token() + // The token can be specified in the pod directly + // or it can be specified in the service account of the pod + // if no service account is configured, then the pod will use the configuration + // of the default service account in that namespace + // the token defined in the pod has takes precedence + // the test would pass iif token is explicitly set to false + // if the token is set to true in the pod, the test would fail right away + if podToken == automountservice.TokenIsTrue { + msg = append(msg, fmt.Sprintf("Pod %s:%s is configured with automountServiceAccountToken set to true ", podUnderTest.Namespace, podUnderTest.Name)) + continue + } + // The pod token is false means the pod is configured properly + // The pod is not configured and the service account is configured with false means + // the pod will inherit the behavior `false` and the test would pass + if podToken == automountservice.TokenIsFalse || serviceAccountToken == automountservice.TokenIsFalse { + continue + } + // the service account is configured with true means all the pods + // using this service account are not configured properly, register the error + // message and fail + if serviceAccountToken == automountservice.TokenIsTrue { + msg = append(msg, fmt.Sprintf("serviceaccount %s:%s is configured with automountServiceAccountToken set to true, impacting pod %s ", podUnderTest.Namespace, podUnderTest.ServiceAccount, podUnderTest.Name)) + } + // the token should be set explicitly to false, otherwise, it's a failure + // register the error message and check the next pod + if serviceAccountToken == automountservice.TokenNotSet { + msg = append(msg, fmt.Sprintf("serviceaccount %s:%s is not configured with automountServiceAccountToken set to false, impacting pod %s ", podUnderTest.Namespace, podUnderTest.ServiceAccount, podUnderTest.Name)) + } + } + if len(msg) > 0 { + tnf.ClaimFilePrintf(strings.Join(msg, "")) + } + gomega.Expect(msg).To(gomega.BeEmpty()) }) } -func testRoleBindings(podNamespace string, serviceAccountName *string) { - ginkgo.It("Should not have RoleBinding in other namespaces", func() { - defer results.RecordResult(identifiers.TestPodRoleBindingsBestPracticesIdentifier) - if *serviceAccountName == "" { - ginkgo.Skip("Can not test when serviceAccountName is empty. Please check previous tests for failures") +func testRoleBindings(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestPodRoleBindingsBestPracticesIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + failedPods := []*configsections.Pod{} + ginkgo.By("Should not have RoleBinding in other namespaces") + for _, podUnderTest := range env.PodsUnderTest { + context := env.GetLocalShellContext() + ginkgo.By(fmt.Sprintf("Testing role binding %s %s", podUnderTest.Namespace, podUnderTest.Name)) + if podUnderTest.ServiceAccount == "" { + ginkgo.Skip("Can not test when serviceAccountName is empty. Please check previous tests for failures") + } + rbTester := rolebinding.NewRoleBinding(common.DefaultTimeout, podUnderTest.ServiceAccount, podUnderTest.Namespace) + test, err := tnf.NewTest(context.GetExpecter(), rbTester, []reel.Handler{rbTester}, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("FAILURE: Pod %s (ns: %s) roleBindings: %v", podUnderTest.Name, podUnderTest.Namespace, rbTester.GetRoleBindings()) + failedPods = append(failedPods, podUnderTest) + }, func(err error) { + tnf.ClaimFilePrintf("ERROR: Pod %s (ns: %s) roleBindings: %v, error: %v", podUnderTest.Name, podUnderTest.Namespace, rbTester.GetRoleBindings(), err) + failedPods = append(failedPods, podUnderTest) + }) } - context := common.GetContext() - rbTester := rolebinding.NewRoleBinding(common.DefaultTimeout, *serviceAccountName, podNamespace) - test, err := tnf.NewTest(context.GetExpecter(), rbTester, []reel.Handler{rbTester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - testResult, err := test.Run() - if rbTester.Result() == tnf.FAILURE { - log.Info("RoleBindings: ", rbTester.GetRoleBindings()) + if n := len(failedPods); n > 0 { + log.Debugf("Pods with role bindings: %+v", failedPods) + ginkgo.Fail(fmt.Sprintf("%d pods have role bindings in other namespaces.", n)) } - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) - gomega.Expect(err).To(gomega.BeNil()) }) } -func testClusterRoleBindings(podNamespace string, serviceAccountName *string) { - ginkgo.It("Should not have ClusterRoleBindings", func() { - defer results.RecordResult(identifiers.TestPodClusterRoleBindingsBestPracticesIdentifier) - if *serviceAccountName == "" { - ginkgo.Skip("Can not test when serviceAccountName is empty. Please check previous tests for failures") +func testClusterRoleBindings(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestPodClusterRoleBindingsBestPracticesIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + ginkgo.By("Should not have ClusterRoleBindings") + failedPods := []*configsections.Pod{} + for _, podUnderTest := range env.PodsUnderTest { + context := env.GetLocalShellContext() + ginkgo.By(fmt.Sprintf("Testing cluster role binding %s %s", podUnderTest.Namespace, podUnderTest.Name)) + if podUnderTest.ServiceAccount == "" { + ginkgo.Skip("Can not test when serviceAccountName is empty. Please check previous tests for failures") + } + crbTester := clusterrolebinding.NewClusterRoleBinding(common.DefaultTimeout, podUnderTest.ServiceAccount, podUnderTest.Namespace) + test, err := tnf.NewTest(context.GetExpecter(), crbTester, []reel.Handler{crbTester}, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("FAILURE: Pod: %s (ns: %s) SA: %s clusterRoleBindings: %v", podUnderTest.Name, podUnderTest.Namespace, podUnderTest.ServiceAccount, crbTester.GetClusterRoleBindings()) + failedPods = append(failedPods, podUnderTest) + }, func(err error) { + tnf.ClaimFilePrintf("ERROR: Pod: %s (ns: %s) SA: %s clusterRoleBindings: %v, error: %v", podUnderTest.Name, podUnderTest.Namespace, podUnderTest.ServiceAccount, crbTester.GetClusterRoleBindings(), err) + failedPods = append(failedPods, podUnderTest) + }) } - context := common.GetContext() - crbTester := clusterrolebinding.NewClusterRoleBinding(common.DefaultTimeout, *serviceAccountName, podNamespace) - test, err := tnf.NewTest(context.GetExpecter(), crbTester, []reel.Handler{crbTester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - testResult, err := test.Run() - if crbTester.Result() == tnf.FAILURE { - log.Info("ClusterRoleBindings: ", crbTester.GetClusterRoleBindings()) + if n := len(failedPods); n > 0 { + log.Debugf("Pods with cluster role bindings: %+v", failedPods) + ginkgo.Fail(fmt.Sprintf("%d pods have cluster role bindings.", n)) } - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) }) } diff --git a/test-network-function/accesscontrol/suite_test.go b/test-network-function/accesscontrol/suite_test.go new file mode 100644 index 000000000..d83b4c583 --- /dev/null +++ b/test-network-function/accesscontrol/suite_test.go @@ -0,0 +1,151 @@ +// Copyright (C) 2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package accesscontrol + +import ( + "errors" + "testing" + "time" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" + "github.com/test-network-function/test-network-function/pkg/utils" +) + +func TestParseCrOutput(t *testing.T) { + testCases := []struct { + rawOutput string + expectedOutput map[string]string + expectedErr error + }{ + { + rawOutput: "aws-ebs-csi-driver-operator,openshift-cloud-credential-operator", + expectedOutput: map[string]string{ + "aws-ebs-csi-driver-operator": "openshift-cloud-credential-operator", + }, + expectedErr: nil, + }, + { + rawOutput: "abcd1234,openshift-cloud-credential-operator", + expectedOutput: map[string]string{ + "abcd1234": "openshift-cloud-credential-operator", + }, + expectedErr: nil, + }, + { + rawOutput: "openshift-cloud-credential-operator", + expectedOutput: map[string]string{}, + expectedErr: errors.New("failed to parse output line openshift-cloud-credential-operator"), + }, + { + rawOutput: "", + expectedOutput: map[string]string{}, + expectedErr: nil, + }, + } + + for _, tc := range testCases { + crMap, err := parseCrOutput(tc.rawOutput) + assert.Equal(t, tc.expectedErr, err) + if err == nil { + assert.Equal(t, tc.expectedOutput, crMap) + } + } +} + +func TestGetCrsNamespaces(t *testing.T) { + gomega.RegisterFailHandler(ginkgo.Fail) + origFunc := utils.ExecuteCommandAndValidate + defer func() { + utils.ExecuteCommandAndValidate = origFunc + }() + + utils.ExecuteCommandAndValidate = func(command string, timeout time.Duration, context *interactive.Context, failureCallbackFun func()) string { + return "aws-ebs-csi-driver-operator,openshift-cloud-credential-operator" + } + + crsNamespaces, err := getCrsNamespaces("test123", "testCRD", nil) + assert.Nil(t, err) + assert.Equal(t, map[string]string{ + "aws-ebs-csi-driver-operator": "openshift-cloud-credential-operator", + }, crsNamespaces) +} + +//nolint:funlen +func TestAddFailedTcInfo(t *testing.T) { + testCases := []struct { + tc string + pod string + namespace string + contID int + existingMap map[string][]failedTcInfo + expectedMap map[string][]failedTcInfo + }{ + { + tc: "tc1", + pod: "pod1", + namespace: "ns1", + contID: 1, + existingMap: map[string][]failedTcInfo{}, + expectedMap: map[string][]failedTcInfo{ + "pod1": { + { + tc: "tc1", + containerIdx: 1, + ns: "ns1", + }, + }, + }, + }, + { + tc: "tc1", + pod: "pod1", + namespace: "ns1", + contID: 1, + existingMap: map[string][]failedTcInfo{ + "pod1": { + { + tc: "tc1", + containerIdx: 1, + ns: "ns1", + }, + }, + }, + expectedMap: map[string][]failedTcInfo{ + "pod1": { + { + tc: "tc1", + containerIdx: 1, + ns: "ns1", + }, + { + tc: "tc1", + containerIdx: 1, + ns: "ns1", + }, + }, + }, + }, + } + + for _, tc := range testCases { + addFailedTcInfo(tc.existingMap, tc.tc, tc.pod, tc.namespace, tc.contID) + assert.Equal(t, tc.expectedMap, tc.existingMap) + } +} diff --git a/test-network-function/certification/doc.go b/test-network-function/certification/doc.go index 363b4520f..1225d7094 100644 --- a/test-network-function/certification/doc.go +++ b/test-network-function/certification/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/test-network-function/certification/suite.go b/test-network-function/certification/suite.go index 2e23a9b39..4da4d7c26 100644 --- a/test-network-function/certification/suite.go +++ b/test-network-function/certification/suite.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,13 +18,19 @@ package certification import ( "fmt" + "strings" + "time" - "github.com/onsi/ginkgo" - ginkgoconfig "github.com/onsi/ginkgo/config" - "github.com/onsi/gomega" + version "github.com/hashicorp/go-version" + "github.com/onsi/ginkgo/v2" + log "github.com/sirupsen/logrus" "github.com/test-network-function/test-network-function/internal/api" configpkg "github.com/test-network-function/test-network-function/pkg/config" + "github.com/test-network-function/test-network-function/pkg/config/configsections" + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" "github.com/test-network-function/test-network-function/pkg/tnf/testcases" + "github.com/test-network-function/test-network-function/pkg/utils" "github.com/test-network-function/test-network-function/test-network-function/common" "github.com/test-network-function/test-network-function/test-network-function/identifiers" "github.com/test-network-function/test-network-function/test-network-function/results" @@ -32,52 +38,251 @@ import ( const ( // timeout for eventually call - eventuallyTimeoutSeconds = 30 - // interval of time - interval = 1 + apiRequestTimeout = 40 * time.Second + expectersVerboseModeEnabled = false + CertifiedOperator = "certified-operators" + outMinikubeVersion = "null" ) -var certAPIClient api.CertAPIClient +var ( + ocpVersionCommand = "oc version -o json | jq '.openshiftVersion'" + kubernetesVersionCommand = "oc version -o json | jq '.serverVersion.gitVersion'" + execCommandOutput = func(command string) string { + return utils.ExecuteCommandAndValidate(command, apiRequestTimeout, interactive.GetContext(expectersVerboseModeEnabled), func() { + log.Error("can't run command: ", command) + }) + } + + certAPIClient api.CertAPIClient +) var _ = ginkgo.Describe(common.AffiliatedCertTestKey, func() { - if testcases.IsInFocus(ginkgoconfig.GinkgoConfig.FocusStrings, common.AffiliatedCertTestKey) { - - // Query API for certification status of listed containers - ginkgo.When("getting certification status", func() { - conf := configpkg.GetConfigInstance() - cnfsToQuery := conf.CertifiedContainerInfo - if len(cnfsToQuery) > 0 { - certAPIClient = api.NewHTTPClient() - for _, cnfRequestInfo := range cnfsToQuery { - cnf := cnfRequestInfo - // Care: this test takes some time to run, failures at later points while before this has finished may be reported as a failure here. Read the failure reason carefully. - ginkgo.It(fmt.Sprintf("container %s/%s should eventually be verified as certified", cnf.Repository, cnf.Name), func() { - defer results.RecordResult(identifiers.TestContainerIsCertifiedIdentifier) - cnf := cnf // pin - gomega.Eventually(func() bool { - isCertified := certAPIClient.IsContainerCertified(cnf.Repository, cnf.Name) - return isCertified - }, eventuallyTimeoutSeconds, interval).Should(gomega.BeTrue()) - }) + conf, _ := ginkgo.GinkgoConfiguration() + if testcases.IsInFocus(conf.FocusStrings, common.AffiliatedCertTestKey) { + env := configpkg.GetTestEnvironment() + ginkgo.BeforeEach(func() { + env.LoadAndRefresh() + }) + + ginkgo.ReportAfterEach(results.RecordResult) + ginkgo.AfterEach(env.CloseLocalShellContext) + + testContainerCertificationStatus() + testAllOperatorCertified(env) + testHelmCertified(env) + } +}) + +func testHelmCertified(env *configpkg.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestHelmIsCertifiedIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + certAPIClient = api.NewHTTPClient() + helmcharts := env.HelmchartsUnderTest + if len(helmcharts) == 0 { + ginkgo.Skip("No helm charts to check") + } + out, err := certAPIClient.GetYamlFile() + if err != nil { + ginkgo.Fail(fmt.Sprintf("error while reading the helm yaml file from the api %s", err)) + } + if out.Entries == nil { + ginkgo.Skip("No helm charts from the api") + } + ourKubeVersion := GetKubeVersion()[1:] + failedHelmCharts := []configsections.HelmChart{} + for _, helm := range helmcharts { + certified := false + for _, entryList := range out.Entries { + for _, entry := range entryList { + if entry.Name == helm.Name && entry.Version == helm.Version { + if entry.KubeVersion != "" { + if CompareVersion(ourKubeVersion, entry.KubeVersion) { + certified = true + break + } + } else { + certified = true + break + } + } + } + if certified { + log.Info(fmt.Sprintf("Helm %s with version %s is certified", helm.Name, helm.Version)) + break } } - }) + if !certified { + failedHelmCharts = append(failedHelmCharts, helm) + } + } + if len(failedHelmCharts) > 0 { + log.Errorf("Helms that are not certified: %+v", failedHelmCharts) + tnf.ClaimFilePrintf("Helms that are not certified: %+v", failedHelmCharts) + ginkgo.Fail(fmt.Sprintf("%d helms chart are not certified.", len(failedHelmCharts))) + } + }) +} + +// getContainerCertificationRequestFunction returns function that will try to get the certification status (CCP) for a container. +func getContainerCertificationRequestFunction(id configsections.ContainerImageIdentifier) func() (interface{}, error) { + return func() (interface{}, error) { + return certAPIClient.GetContainerCatalogEntry(id) + } +} + +// getOperatorCertificationRequestFunction returns function that will try to get the certification status (OCP) for an operator. +func getOperatorCertificationRequestFunction(organization, operatorName, ocpversion string) func() (interface{}, error) { + return func() (interface{}, error) { + return certAPIClient.IsOperatorCertified(organization, operatorName, ocpversion) + } +} + +// waitForCertificationRequestToSuccess calls to certificationRequestFunc until it returns true. +func waitForCertificationRequestToSuccess(certificationRequestFunc func() (interface{}, error), timeout time.Duration) interface{} { + const pollingPeriod = 1 * time.Second + var elapsed time.Duration + var err error + var result interface{} + + for elapsed < timeout { + result, err = certificationRequestFunc() - operatorsToQuery := configpkg.GetConfigInstance().CertifiedOperatorInfo - if len(operatorsToQuery) > 0 { - certAPIClient := api.NewHTTPClient() - for _, certified := range operatorsToQuery { - // Care: this test takes some time to run, failures at later points while before this has finished may be reported as a failure here. Read the failure reason carefully. - ginkgo.It(fmt.Sprintf("should eventually be verified as certified (operator %s/%s)", certified.Organization, certified.Name), func() { - defer results.RecordResult(identifiers.TestOperatorIsCertifiedIdentifier) - certified := certified // pin - gomega.Eventually(func() bool { - isCertified := certAPIClient.IsOperatorCertified(certified.Organization, certified.Name) - return isCertified - }, eventuallyTimeoutSeconds, interval).Should(gomega.BeTrue()) - }) + if err == nil { + break + } + time.Sleep(pollingPeriod) + elapsed += pollingPeriod + } + return result +} + +func testContainerCertificationStatus() { + // Query API for certification status of listed containers + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestContainerIsCertifiedIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + env := configpkg.GetTestEnvironment() + containersToQuery := make(map[configsections.ContainerImageIdentifier]bool) + for _, c := range env.Config.CertifiedContainerInfo { + containersToQuery[c] = true + } + if env.Config.CheckDiscoveredContainerCertificationStatus { + for _, cut := range env.ContainersUnderTest { + containersToQuery[cut.ImageSource.ContainerImageIdentifier] = true + } + } + if len(containersToQuery) == 0 { + ginkgo.Skip("No containers to check configured in tnf_config.yml") + } + ginkgo.By(fmt.Sprintf("Getting certification status. Number of containers to check: %d", len(containersToQuery))) + if len(containersToQuery) > 0 { + certAPIClient = api.NewHTTPClient() + failedContainers := []configsections.ContainerImageIdentifier{} + allContainersToQueryEmpty := true + for c := range containersToQuery { + if c.Name == "" || c.Repository == "" { + tnf.ClaimFilePrintf("Container name = \"%s\" or repository = \"%s\" is missing, skipping this container to query", c.Name, c.Repository) + continue + } + allContainersToQueryEmpty = false + ginkgo.By(fmt.Sprintf("Container %s/%s should eventually be verified as certified", c.Repository, c.Name)) + entry := waitForCertificationRequestToSuccess(getContainerCertificationRequestFunction(c), apiRequestTimeout).(*api.ContainerCatalogEntry) + if entry == nil { + tnf.ClaimFilePrintf("Container %s (repository %s) is not found in the certified container catalog.", c.Name, c.Repository) + failedContainers = append(failedContainers, c) + } else { + if entry.GetBestFreshnessGrade() > "C" { + tnf.ClaimFilePrintf("Container %s (repository %s) is found in the certified container catalog but with low health index '%s'.", c.Name, c.Repository, entry.GetBestFreshnessGrade()) + failedContainers = append(failedContainers, c) + } + log.Info(fmt.Sprintf("Container %s (repository %s) is certified.", c.Name, c.Repository)) + } + } + if allContainersToQueryEmpty { + ginkgo.Skip("No containers to check because either container name or repository is empty for all containers in tnf_config.yml") + } + + if n := len(failedContainers); n > 0 { + log.Warnf("Containers that are not certified: %+v", failedContainers) + ginkgo.Fail(fmt.Sprintf("%d container images are not certified.", n)) } } + }) +} +func testAllOperatorCertified(env *configpkg.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestOperatorIsCertifiedIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + operatorsToQuery := env.OperatorsUnderTest + + if len(operatorsToQuery) == 0 { + ginkgo.Skip("No operators to check configured ") + } + certAPIClient = api.NewHTTPClient() + ginkgo.By(fmt.Sprintf("Verify operator as certified. Number of operators to check: %d", len(operatorsToQuery))) + testFailed := false + for _, op := range operatorsToQuery { + ocpversion := GetOcpVersion() + majorDotMinorVersion := "" + if ocpversion != "" { + // Converts major.minor.patch version format to major.minor + const majorMinorPatchCount = 3 + splitVersion := strings.SplitN(ocpversion, ".", majorMinorPatchCount) + majorDotMinorVersion = splitVersion[0] + "." + splitVersion[1] + } + pack := op.Name + isCertified := waitForCertificationRequestToSuccess(getOperatorCertificationRequestFunction(CertifiedOperator, pack, majorDotMinorVersion), apiRequestTimeout).(bool) + if !isCertified { + testFailed = true + log.Info(fmt.Sprintf("Operator %s not certified for OpenShift %s .", pack, majorDotMinorVersion)) + tnf.ClaimFilePrintf("Operator %s failed to be certified for OpenShift %s", pack, majorDotMinorVersion) + } else { + log.Info(fmt.Sprintf("Operator %s certified OK.", pack)) + } + } + if testFailed { + ginkgo.Fail("At least one operator was not certified to run on this version of OpenShift. Check Claim.json file for details.") + } + }) +} + +func GetOcpVersion() string { + ocCmd := ocpVersionCommand + ocVersion := execCommandOutput(ocCmd) + if ocVersion != outMinikubeVersion { + nums := strings.Split(strings.ReplaceAll(ocVersion, "\"", ""), ".") + ocVersion = nums[0] + "." + nums[1] + } else { + ocVersion = "" } -}) + return ocVersion +} +func GetKubeVersion() string { + ocCmd := kubernetesVersionCommand + kubeVersion := execCommandOutput(ocCmd) + if kubeVersion != outMinikubeVersion { + kubeVersion = strings.Split(kubeVersion, "+")[0] + kubeVersion = kubeVersion[1:] + } else { + kubeVersion = "" + } + return kubeVersion +} +func CompareVersion(ver1, ver2 string) bool { + ourKubeVersion, _ := version.NewVersion(ver1) + kubeVersion := strings.ReplaceAll(ver2, " ", "")[2:] + if strings.Contains(kubeVersion, "<") { + kubever := strings.Split(kubeVersion, "<") + minVersion, _ := version.NewVersion(kubever[0]) + maxVersion, _ := version.NewVersion(kubever[1]) + if ourKubeVersion.GreaterThanOrEqual(minVersion) && ourKubeVersion.LessThan(maxVersion) { + return true + } + } else { + kubever := strings.Split(kubeVersion, "-") + minVersion, _ := version.NewVersion(kubever[0]) + if ourKubeVersion.GreaterThanOrEqual(minVersion) { + return true + } + } + return false +} diff --git a/test-network-function/common/constant.go b/test-network-function/common/constant.go index a6183b6d3..74b9325c1 100644 --- a/test-network-function/common/constant.go +++ b/test-network-function/common/constant.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -28,4 +28,5 @@ const ( ObservabilityTestKey = "observability" OperatorTestKey = "operator" PlatformAlterationTestKey = "platform-alteration" + CommonTestKey = "common" ) diff --git a/test-network-function/common/doc.go b/test-network-function/common/doc.go index 3e43e77e3..c306011ef 100644 --- a/test-network-function/common/doc.go +++ b/test-network-function/common/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/test-network-function/common/env.go b/test-network-function/common/env.go index e43fbf50d..e1f5a0920 100644 --- a/test-network-function/common/env.go +++ b/test-network-function/common/env.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,19 +17,15 @@ package common import ( + "fmt" "os" "path" + "runtime" "strconv" "time" - "github.com/onsi/gomega" + "github.com/onsi/ginkgo/v2" log "github.com/sirupsen/logrus" - "github.com/test-network-function/test-network-function/pkg/config" - "github.com/test-network-function/test-network-function/pkg/config/configsections" - "github.com/test-network-function/test-network-function/pkg/tnf" - "github.com/test-network-function/test-network-function/pkg/tnf/handlers/ipaddr" - "github.com/test-network-function/test-network-function/pkg/tnf/interactive" - "github.com/test-network-function/test-network-function/pkg/tnf/reel" ) var ( @@ -46,173 +42,70 @@ var ( // DefaultTimeout for creating new interactive sessions (oc, ssh, tty) var DefaultTimeout = time.Duration(defaultTimeoutSeconds) * time.Second -// ContainersToExcludeFromConnectivityTests is a set used for storing the containers that should be excluded from -// connectivity testing. -var ContainersToExcludeFromConnectivityTests = make(map[configsections.ContainerIdentifier]interface{}) +// LogLevelTraceEnabled is saved to filter some debug trace logs (e.g. expecters Sent/Match) +var LogLevelTraceEnabled = false -// Helper used to instantiate an OpenShift Client Session. -func getOcSession(pod, container, namespace string, timeout time.Duration, options ...interactive.Option) *interactive.Oc { - // Spawn an interactive OC shell using a goroutine (needed to avoid cross expect.Expecter interaction). Extract the - // Oc reference from the goroutine through a channel. Performs basic sanity checking that the Oc session is set up - // correctly. - var containerOc *interactive.Oc - ocChan := make(chan *interactive.Oc) - var chOut <-chan error - - goExpectSpawner := interactive.NewGoExpectSpawner() - var spawner interactive.Spawner = goExpectSpawner - - go func() { - oc, outCh, err := interactive.SpawnOc(&spawner, pod, container, namespace, timeout, options...) - gomega.Expect(outCh).ToNot(gomega.BeNil()) - gomega.Expect(err).To(gomega.BeNil()) - ocChan <- oc - }() - - // Set up a go routine which reads from the error channel - go func() { - err := <-chOut - gomega.Expect(err).To(gomega.BeNil()) - }() - - containerOc = <-ocChan - - gomega.Expect(containerOc).ToNot(gomega.BeNil()) - - return containerOc -} - -// Container is an internal construct which follows the Container design pattern. Essentially, a Container holds the -// pertinent information to perform a test against or using an Operating System Container. This includes facets such -// as the reference to the interactive.Oc instance, the reference to the test configuration, and the default network -// IP address. -type Container struct { - ContainerConfiguration configsections.Container - Oc *interactive.Oc - DefaultNetworkIPAddress string - ContainerIdentifier configsections.ContainerIdentifier -} - -// createContainers contains the general steps involved in creating "oc" sessions and other configuration. A map of the -// aggregate information is returned. -func createContainers(containerDefinitions []configsections.Container) map[configsections.ContainerIdentifier]*Container { - createdContainers := make(map[configsections.ContainerIdentifier]*Container) - for _, c := range containerDefinitions { - oc := getOcSession(c.PodName, c.ContainerName, c.Namespace, DefaultTimeout, interactive.Verbose(true)) - var defaultIPAddress = "UNKNOWN" - if _, ok := ContainersToExcludeFromConnectivityTests[c.ContainerIdentifier]; !ok { - defaultIPAddress = getContainerDefaultNetworkIPAddress(oc, c.DefaultNetworkDevice) - } - createdContainers[c.ContainerIdentifier] = &Container{ - ContainerConfiguration: c, - Oc: oc, - DefaultNetworkIPAddress: defaultIPAddress, - ContainerIdentifier: c.ContainerIdentifier, - } +var TcClaimLogPrintf = func(format string, args ...interface{}) { + message := fmt.Sprintf(format+"\n", args...) + _, err := ginkgo.GinkgoWriter.Write([]byte(message)) + if err != nil { + log.Errorf("Ginkgo writer could not write msg '%s' because: %s", message, err) } - return createdContainers -} - -// Extract a container IP address for a particular device. This is needed since container default network IP address -// is served by dhcp, and thus is ephemeral. -func getContainerDefaultNetworkIPAddress(oc *interactive.Oc, dev string) string { - log.Infof("Getting IP Information for: %s(%s) in ns=%s", oc.GetPodName(), oc.GetPodContainerName(), oc.GetPodNamespace()) - ipTester := ipaddr.NewIPAddr(DefaultTimeout, dev) - test, err := tnf.NewTest(oc.GetExpecter(), ipTester, []reel.Handler{ipTester}, oc.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - RunAndValidateTest(test) - return ipTester.GetIPv4Address() -} - -// CreateContainersUnderTest sets up the test containers. -func CreateContainersUnderTest(conf *configsections.TestConfiguration) map[configsections.ContainerIdentifier]*Container { - return createContainers(conf.ContainersUnderTest) -} - -// CreatePartnerContainers sets up the partner containers. -func CreatePartnerContainers(conf *configsections.TestConfiguration) map[configsections.ContainerIdentifier]*Container { - return createContainers(conf.PartnerContainers) -} - -// GetContext spawns a new shell session and returns its context -func GetContext() *interactive.Context { - context, err := interactive.SpawnShell(interactive.CreateGoExpectSpawner(), DefaultTimeout, interactive.Verbose(true)) - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(context).ToNot(gomega.BeNil()) - gomega.Expect(context.GetExpecter()).ToNot(gomega.BeNil()) - return context -} - -// RunAndValidateTest runs the test and checks the result -func RunAndValidateTest(test *tnf.Test) { - testResult, err := test.Run() - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) - gomega.Expect(err).To(gomega.BeNil()) -} - -// GetTestConfiguration returns the cnf-certification-generic-tests test configuration. -func GetTestConfiguration() *configsections.TestConfiguration { - conf := config.GetConfigInstance() - return &conf.Generic } -// IsMinikube returns true when the env var is set, OCP only test would be skipped based on this flag -func IsMinikube() bool { - b, _ := strconv.ParseBool(os.Getenv("TNF_MINIKUBE_ONLY")) +// IsNonOcpCluster returns true when the env var is set, OCP only test would be skipped based on this flag +func IsNonOcpCluster() bool { + b, _ := strconv.ParseBool(os.Getenv("TNF_NON_OCP_CLUSTER")) return b } -// NonIntrusive is for skipping tests that would impact the CNF or test environment in an intrusive way -func NonIntrusive() bool { +// Intrusive is for running tests that can impact the CNF or test environment in an intrusive way +func Intrusive() bool { b, _ := strconv.ParseBool(os.Getenv("TNF_NON_INTRUSIVE_ONLY")) - return b + return !b } -// ConfigurationData is used to host test configuration -type ConfigurationData struct { - ContainersUnderTest map[configsections.ContainerIdentifier]*Container - PartnerContainers map[configsections.ContainerIdentifier]*Container - TestOrchestrator *Container - FsDiffContainer *Container - needsRefresh bool -} - -// createContainersUnderTest sets up the test containers. -func createContainersUnderTest(conf *configsections.TestConfiguration) map[configsections.ContainerIdentifier]*Container { - return createContainers(conf.ContainersUnderTest) -} +// logLevel retrieves the LOG_LEVEL environment variable +func logLevel() string { + logLevel := os.Getenv("LOG_LEVEL") + if logLevel == "" { + log.Info("LOG_LEVEL environment is not set, defaulting to DEBUG") + logLevel = "debug" //nolint:goconst + } -// createPartnerContainers sets up the partner containers. -func createPartnerContainers(conf *configsections.TestConfiguration) map[configsections.ContainerIdentifier]*Container { - return createContainers(conf.PartnerContainers) + return logLevel } -// Loadconfiguration the configuration into ConfigurationData -func Loadconfiguration(configData *ConfigurationData) { - conf := GetTestConfiguration() - log.Infof("Test Configuration: %s", conf) +// SetLogLevel sets the log level for logrus based on the "LOG_LEVEL" environment variable +func SetLogLevel() { + var aLogLevel, err = log.ParseLevel(logLevel()) - for _, cid := range conf.ExcludeContainersFromConnectivityTests { - ContainersToExcludeFromConnectivityTests[cid] = "" + if err != nil { + log.Error("LOG_LEVEL environment set with an invalid value, defaulting to DEBUG \n Valid values are: trace, debug, info, warn, error, fatal, panic") + aLogLevel = log.DebugLevel } - configData.ContainersUnderTest = createContainersUnderTest(conf) - configData.PartnerContainers = createPartnerContainers(conf) - configData.TestOrchestrator = configData.PartnerContainers[conf.TestOrchestrator] - configData.FsDiffContainer = configData.PartnerContainers[conf.FsDiffMasterContainer] - log.Info(configData.TestOrchestrator) - log.Info(configData.ContainersUnderTest) -} -// ReloadConfiguration force the autodiscovery to run again -func ReloadConfiguration(configData *ConfigurationData) { - if configData.needsRefresh { - config.SetNeedsRefresh() - Loadconfiguration(configData) + if aLogLevel == log.TraceLevel { + LogLevelTraceEnabled = true } - configData.needsRefresh = false -} -// SetNeedsRefresh indicate the config should be reloaded after this test -func (configData *ConfigurationData) SetNeedsRefresh() { - configData.needsRefresh = true + log.Info("Log level set to:", aLogLevel) + log.SetLevel(aLogLevel) +} + +// SetLogFormat sets the log format for logrus +func SetLogFormat() { + log.Info("debug format initialization: start") + customFormatter := new(log.TextFormatter) + customFormatter.TimestampFormat = time.StampMilli + customFormatter.PadLevelText = true + customFormatter.FullTimestamp = true + customFormatter.ForceColors = true + log.SetReportCaller(true) + customFormatter.CallerPrettyfier = func(f *runtime.Frame) (string, string) { + _, filename := path.Split(f.File) + return strconv.Itoa(f.Line) + "]", fmt.Sprintf("[%s:", filename) + } + log.SetFormatter(customFormatter) + log.Info("debug format initialization: done") } diff --git a/test-network-function/common/env_test.go b/test-network-function/common/env_test.go new file mode 100644 index 000000000..9cb8fd95b --- /dev/null +++ b/test-network-function/common/env_test.go @@ -0,0 +1,122 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package common + +import ( + "os" + "testing" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestIsNonOcpCluster(t *testing.T) { + testCases := []struct { + isNonOCPCluster bool + }{ + { + isNonOCPCluster: true, + }, + { + isNonOCPCluster: false, + }, + } + + defer os.Unsetenv("TNF_NON_OCP_CLUSTER") + for _, tc := range testCases { + if tc.isNonOCPCluster { + os.Setenv("TNF_NON_OCP_CLUSTER", "true") + assert.Equal(t, tc.isNonOCPCluster, IsNonOcpCluster()) + } else { + os.Setenv("TNF_NON_OCP_CLUSTER", "false") + assert.Equal(t, tc.isNonOCPCluster, IsNonOcpCluster()) + } + } +} + +func TestIntrusive(t *testing.T) { + testCases := []struct { + isIntrusive bool + }{ + { + isIntrusive: true, + }, + { + isIntrusive: false, + }, + } + + defer os.Unsetenv("TNF_NON_INTRUSIVE_ONLY") + for _, tc := range testCases { + if tc.isIntrusive { + os.Setenv("TNF_NON_INTRUSIVE_ONLY", "false") + assert.Equal(t, tc.isIntrusive, Intrusive()) + } else { + os.Setenv("TNF_NON_INTRUSIVE_ONLY", "true") + assert.Equal(t, tc.isIntrusive, Intrusive()) + } + } +} + +func TestLogLevel(t *testing.T) { + testCases := []struct { + logLevel string + expectedLogLevel string + }{ + { + logLevel: "high", + expectedLogLevel: "high", + }, + { + logLevel: "", + expectedLogLevel: "debug", + }, + } + + defer os.Unsetenv("LOG_LEVEL") + for _, tc := range testCases { + os.Setenv("LOG_LEVEL", tc.logLevel) + assert.Equal(t, tc.expectedLogLevel, logLevel()) + } +} + +func TestSetLogLevel(t *testing.T) { + testCases := []struct { + logLevel string + expectedLogLevel log.Level + }{ + { + logLevel: "high", + expectedLogLevel: log.DebugLevel, + }, + { + logLevel: "", + expectedLogLevel: log.DebugLevel, + }, + { + logLevel: "trace", + expectedLogLevel: log.TraceLevel, + }, + } + + defer os.Unsetenv("LOG_LEVEL") + for _, tc := range testCases { + os.Setenv("LOG_LEVEL", tc.logLevel) + SetLogLevel() + assert.Equal(t, tc.expectedLogLevel, log.GetLevel()) + } +} diff --git a/test-network-function/common/suite.go b/test-network-function/common/suite.go new file mode 100644 index 000000000..2186f1ced --- /dev/null +++ b/test-network-function/common/suite.go @@ -0,0 +1,53 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package common + +import ( + "github.com/onsi/ginkgo/v2" + log "github.com/sirupsen/logrus" + configpkg "github.com/test-network-function/test-network-function/pkg/config" + "github.com/test-network-function/test-network-function/pkg/config/autodiscover" +) + +var env *configpkg.TestEnvironment + +func RemoveLabelsFromAllNodes() { + for name := range autodiscover.GetNodesList() { + autodiscover.DeleteDebugLabel(name) + } +} + +func RemoveDebugPods() { + env = configpkg.GetTestEnvironment() + env.LoadAndRefresh() + for name, node := range env.NodesUnderTest { + if !(node.HasDebugPod()) { + continue + } + node.DebugContainer.CloseOc() + autodiscover.DeleteDebugLabel(name) + } +} + +var _ = ginkgo.BeforeSuite(func() { +}) + +var _ = ginkgo.AfterSuite(func() { + // clean up added label to nodes + log.Info("Clean up added labels to nodes") + RemoveDebugPods() +}) diff --git a/test-network-function/diagnostic/diagnostic.go b/test-network-function/diagnostic/diagnostic.go new file mode 100644 index 000000000..6465caa78 --- /dev/null +++ b/test-network-function/diagnostic/diagnostic.go @@ -0,0 +1,461 @@ +package diagnostic + +import ( + "encoding/json" + "errors" + "fmt" + "path" + "strings" + "time" + + log "github.com/sirupsen/logrus" + "github.com/test-network-function/test-network-function/pkg/config" + "github.com/test-network-function/test-network-function/test-network-function/common" + + "github.com/test-network-function/test-network-function/pkg/tnf" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/clusterversion" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/generic" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/nodedebug" + "github.com/test-network-function/test-network-function/pkg/tnf/reel" +) + +const ( + // defaultTimeoutSeconds contains the default timeout in seconds. + defaultTimeoutSeconds = 20 +) + +var ( + // defaultTestTimeout is the timeout for the test. + defaultTestTimeout = time.Duration(defaultTimeoutSeconds) * time.Second + + // nodeSummary stores the raw JSON output of `oc get nodes -o json` + nodeSummary = make(map[string]interface{}) + + cniPlugins = make([]CniPlugin, 0) + + versionsOcp clusterversion.ClusterVersion + + nodesHwInfo = NodesHwInfo{} + + // csiDriver stores the csi driver JSON output of `oc get csidriver -o json` + csiDriver = make(map[string]interface{}) + + // nodesTestPath is the file location of the nodes.json test case relative to the project root. + nodesTestPath = path.Join("pkg", "tnf", "handlers", "node", "nodes.json") + + // csiDriverTestPath is the file location of the csidriver.json test case relative to the project root. + csiDriverTestPath = path.Join("pkg", "tnf", "handlers", "csidriver", "csidriver.json") + + // relativeCsiDriverTestPath is the relative path to the csidriver.json test case. + relativeCsiDriverTestPath = path.Join(pathRelativeToRoot, csiDriverTestPath) + + // pathRelativeToRoot is used to calculate relative filepaths for the `test-network-function` executable entrypoint. + pathRelativeToRoot = path.Join("..") + + // relativeNodesTestPath is the relative path to the nodes.json test case. + relativeNodesTestPath = path.Join(pathRelativeToRoot, nodesTestPath) + + // relativeSchemaPath is the relative path to the generic-test.schema.json JSON schema. + relativeSchemaPath = path.Join(pathRelativeToRoot, schemaPath) + + // schemaPath is the path to the generic-test.schema.json JSON schema relative to the project root. + schemaPath = path.Join("schemas", "generic-test.schema.json") + + // retrieve the singleton instance of test environment + env *config.TestEnvironment = config.GetTestEnvironment() +) + +// CniPlugin holds info about a CNI plugin +// The JSON fields come from the jq output +type CniPlugin struct { + Name string `json:"name"` + Type string `json:"type"` + Version string `json:"version"` + Plugins interface{} `json:"plugins"` +} + +// NodeHwInfo node HW info +type NodeHwInfo struct { + NodeName string + Lscpu map[string]string // lscpu output parsed as entry to value map + IPconfig map[string][]string // 'ip a' output parsed as interface name to output lines map + Lsblk interface{} // 'lsblk -J' output un-marshaled into an unknown type + Lspci []string // lspci output parsed to individual lines +} + +// NodesHwInfo one master one worker +type NodesHwInfo struct { + Master NodeHwInfo + Worker NodeHwInfo +} + +func GetDiagnosticData() []error { + errs := []error{} + if len(env.PodsUnderTest) == 0 { + errs = append(errs, errors.New("nod pods under test found")) + } + if len(env.ContainersUnderTest) == 0 { + errs = append(errs, errors.New("no containers under test found")) + } + + if err := getOcpVersions(); err != nil { + errs = append(errs, fmt.Errorf("failed to get ocp version. Error: %v", err)) + } + + if err := getNodes(); err != nil { + errs = append(errs, fmt.Errorf("failed to get nodes info. Error: %v", err)) + } + + if err := getCniPlugins(); err != nil { + errs = append(errs, fmt.Errorf("failed to get CNI plugins info. Error: %v", err)) + } + + if err := getClusterCSIInfo(); err != nil { + errs = append(errs, fmt.Errorf("failed to get cluster CSI info. Error: %v", err)) + } + + if hwErrs := getNodesHwInfo(); len(hwErrs) > 0 { + errs = append(errs, hwErrs...) + } + + return errs +} + +func getNodes() error { + log.Infof("Getting Nodes information.") + + context := env.GetLocalShellContext() + defer env.CloseLocalShellContext() + + tester, handlers, jsonParseResult, err := generic.NewGenericFromJSONFile(relativeNodesTestPath, relativeSchemaPath) + if validParseResult := jsonParseResult.Valid(); err != nil || !validParseResult { + return fmt.Errorf("failed to create handler to get nodes information (validParseResult: %v, error: %v)", validParseResult, err) + } + + test, err := tnf.NewTest(context.GetExpecter(), *tester, handlers, context.GetErrorChannel()) + if err != nil { + return fmt.Errorf("failed to create tester to get nodes information (error: %v)", err) + } + + test.RunWithCallbacks(func() { + genericTest := (*tester).(*generic.Generic) + matches := genericTest.Matches + if n := len(matches); n != 1 { + err = fmt.Errorf("failed to parse console output for %s (len=%d)", strings.Join(genericTest.Args(), " "), n) + return + } + + match := matches[0].Match + err = json.Unmarshal([]byte(match), &nodeSummary) + }, func() { + err = errors.New("failed to execute tester to get nodes information") + }, func(handlerError error) { + err = fmt.Errorf("failed to execute tester to get nodes information (error: %v)", handlerError) + }) + + return err +} + +// GetNodeSummary returns the result of running `oc get nodes -o json`. +func GetNodeSummary() map[string]interface{} { + return nodeSummary +} + +// GetCniPlugins return the found plugins +func GetCniPlugins() []CniPlugin { + return cniPlugins +} + +// GetVersionsOcp return OCP versions +func GetVersionsOcp() clusterversion.ClusterVersion { + return versionsOcp +} + +// GetNodesHwInfo returns an object with HW info of one master and one worker +func GetNodesHwInfo() NodesHwInfo { + return nodesHwInfo +} + +// GetCsiDriverInfo returns the CSI driver info of running `oc get csidriver -o json`. +func GetCsiDriverInfo() map[string]interface{} { + return csiDriver +} + +func getMasterNodeName(env *config.TestEnvironment) string { + for _, node := range env.NodesUnderTest { + if node.IsMaster() && node.HasDebugPod() { + return node.Name + } + } + return "" +} + +func getWorkerNodeName(env *config.TestEnvironment) string { + for _, node := range env.NodesUnderTest { + if node.IsWorker() && node.HasDebugPod() { + return node.Name + } + } + return "" +} + +func listNodeCniPlugins(nodeName string) ([]CniPlugin, error) { + // This command will return a JSON array, with the name, cniVersion and plugins fields from the cat output + const command = "cat /host/etc/cni/net.d/[0-999]* | jq -s '[ .[] | {name:.name, type:.type, version:.cniVersion, plugins: .plugins}]'" + + nodes := config.GetTestEnvironment().NodesUnderTest + context := nodes[nodeName].DebugContainer.GetOc() + tester := nodedebug.NewNodeDebug(defaultTestTimeout, nodeName, command, true, true) + test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) + if err != nil { + return nil, fmt.Errorf("failed to create cni plugins handler (error: %v)", err) + } + + result := []CniPlugin{} + test.RunWithCallbacks(func() { + err = json.Unmarshal([]byte(tester.Raw), &result) + }, func() { + err = errors.New("failed to execute tester to get CNI plugins") + }, func(handlerError error) { + err = fmt.Errorf("failed to execute tester to get CNI plugins (error: %v)", handlerError) + }) + + return result, err +} + +func getOcpVersions() error { + log.Infof("Getting Openshift versions.") + + context := env.GetLocalShellContext() + defer env.CloseLocalShellContext() + + tester := clusterversion.NewClusterVersion(defaultTestTimeout) + test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) + if err != nil { + return fmt.Errorf("failed to create ocp versions handler test. Error: %v", err) + } + + test.RunWithCallbacks(func() { + versionsOcp = tester.GetVersions() + }, func() { + err = errors.New("ocp versions handler failed") + }, func(handlerError error) { + err = fmt.Errorf(" maxIndex { - maxIndex = entryIndex - gomega.Expect(err2).To(gomega.BeNil()) - maxIndexEntryName = bootEntry - } + ginkgo.ReportAfterEach(results.RecordResult) + ginkgo.AfterEach(env.CloseLocalShellContext) } - - return maxIndexEntryName -} - -func getGrubKernelArgs(context *interactive.Context, nodeName string) map[string]string { - bootConfigEntriesTester := bootconfigentries.NewBootConfigEntries(common.DefaultTimeout, nodeName) - test, err := tnf.NewTest(context.GetExpecter(), bootConfigEntriesTester, []reel.Handler{bootConfigEntriesTester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - common.RunAndValidateTest(test) - bootConfigEntries := bootConfigEntriesTester.GetBootConfigEntries() - - maxIndexEntryName := getMaxIndexEntry(bootConfigEntries) - - readBootConfigTester := readbootconfig.NewReadBootConfig(common.DefaultTimeout, nodeName, maxIndexEntryName) - test, err = tnf.NewTest(context.GetExpecter(), readBootConfigTester, []reel.Handler{readBootConfigTester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - common.RunAndValidateTest(test) - bootConfig := readBootConfigTester.GetBootConfig() - - splitBootConfig := strings.Split(bootConfig, "\n") - filteredBootConfig := utils.FilterArray(splitBootConfig, func(line string) bool { - return strings.HasPrefix(line, "options") - }) - gomega.Expect(len(filteredBootConfig)).To(gomega.Equal(1)) - grubKernelConfig := filteredBootConfig[0] - grubSplitKernelConfig := strings.Split(grubKernelConfig, " ") - grubSplitKernelConfig = grubSplitKernelConfig[1:] - return utils.ArgListToMap(grubSplitKernelConfig) -} - -func testBootParams(context *interactive.Context, podName, podNamespace string, targetPodOc *interactive.Oc) { - ginkgo.It(fmt.Sprintf("Testing boot params for the pod's node %s/%s", podNamespace, podName), func() { - defer results.RecordResult(identifiers.TestUnalteredStartupBootParamsIdentifier) - nodeName := getPodNodeName(context, podName, podNamespace) - mcName := getMcName(context, nodeName) - mcKernelArgumentsMap := getMcKernelArguments(context, mcName) - currentKernelArgsMap := getCurrentKernelCmdlineArgs(targetPodOc) - grubKernelConfigMap := getGrubKernelArgs(context, nodeName) - - for key, mcVal := range mcKernelArgumentsMap { - if currentVal, ok := currentKernelArgsMap[key]; ok { - gomega.Expect(currentVal).To(gomega.Equal(mcVal)) - } - if grubVal, ok := grubKernelConfigMap[key]; ok { - gomega.Expect(grubVal).To(gomega.Equal(mcVal)) - } - } - }) -} +}) diff --git a/test-network-function/identifiers/identifier_test.go b/test-network-function/identifiers/identifier_test.go new file mode 100644 index 000000000..541e5b988 --- /dev/null +++ b/test-network-function/identifiers/identifier_test.go @@ -0,0 +1,117 @@ +// Copyright (C) 2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package identifiers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function-claim/pkg/claim" +) + +func TestGetSuiteAndTestFromIdentifier(t *testing.T) { + testCases := []struct { + testIdentifier claim.Identifier + expectedResult []string + }{ + { + testIdentifier: claim.Identifier{ + Url: "http://test-network-function.com/testcases/SuiteName/TestName", + }, + expectedResult: []string{"SuiteName", "TestName"}, + }, + { // extra 'MyTest' added. The function should only return the Suite and Test names + testIdentifier: claim.Identifier{ + Url: "http://test-network-function.com/testcases/SuiteName/TestName/MyTest", + }, + expectedResult: []string{"SuiteName", "TestName"}, + }, + { // invalid formatting + testIdentifier: claim.Identifier{ + Url: "testURL", + Version: "testVersion", + }, + expectedResult: nil, + }, + } + + for _, tc := range testCases { + assert.Equal(t, tc.expectedResult, GetSuiteAndTestFromIdentifier(tc.testIdentifier)) + } +} + +func TestXformToGinkgoItIdentifier(t *testing.T) { + testCases := []struct { + testIdentifier claim.Identifier + expectedResult string + }{ + { + testIdentifier: claim.Identifier{ + Url: "http://test-network-function.com/testcases/SuiteName/TestName", + }, + expectedResult: "SuiteName-TestName", + }, + { // extra 'MyTest' added + testIdentifier: claim.Identifier{ + Url: "http://test-network-function.com/testcases/SuiteName/TestName/MyTest", + }, + expectedResult: "SuiteName-TestName", + }, + { // invalid formatting + testIdentifier: claim.Identifier{ + Url: "testURL", + Version: "testVersion", + }, + expectedResult: "", + }, + } + + for _, tc := range testCases { + assert.Equal(t, tc.expectedResult, XformToGinkgoItIdentifier(tc.testIdentifier)) + } +} + +func TestXformToGinkgoItIdentifierExtended(t *testing.T) { + testCases := []struct { + testIdentifier claim.Identifier + expectedResult string + }{ + { + testIdentifier: claim.Identifier{ + Url: "http://test-network-function.com/testcases/SuiteName/TestName", + }, + expectedResult: "SuiteName-TestName-extra", + }, + { // extra 'MyTest' added and subsequently removed + testIdentifier: claim.Identifier{ + Url: "http://test-network-function.com/testcases/SuiteName/TestName/MyTest", + }, + expectedResult: "SuiteName-TestName-extra", + }, + { // invalid formatting + testIdentifier: claim.Identifier{ + Url: "testURL", + Version: "testVersion", + }, + expectedResult: "-extra", + }, + } + + for _, tc := range testCases { + assert.Equal(t, tc.expectedResult, XformToGinkgoItIdentifierExtended(tc.testIdentifier, "extra")) + } +} diff --git a/test-network-function/identifiers/identifiers.go b/test-network-function/identifiers/identifiers.go index d37eb7e42..8b2b98049 100644 --- a/test-network-function/identifiers/identifiers.go +++ b/test-network-function/identifiers/identifiers.go @@ -1,5 +1,4 @@ // Copyright (C) 2021 Red Hat, Inc. -// Copyright (C) 2021 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -19,16 +18,19 @@ package identifiers import ( "fmt" + "strings" "github.com/test-network-function/test-network-function-claim/pkg/claim" "github.com/test-network-function/test-network-function/test-network-function/common" ) const ( - informativeResult = "informative" - normativeResult = "normative" - url = "http://test-network-function.com/testcases" - versionOne = "v1.0.0" + bestPracticeDocV1dot2URL = "[CNF Best Practice V1.2](https://connect.redhat.com/sites/default/files/2021-03/Cloud%20Native%20Network%20Function%20Requirements.pdf)" + informativeResult = "informative" + normativeResult = "normative" + url = "http://test-network-function.com/testcases" + versionOne = "v1.0.0" + bestPracticeDocV1dot3URL = "https://docs.google.com/document/d/1wRHMk1ZYUSVmgp_4kxvqjVOKwolsZ5hDXjr5MLy-wbg/edit#" ) // TestCaseDescription describes a JUnit test case. @@ -44,6 +46,9 @@ type TestCaseDescription struct { // Type is the type of the test (i.e., normative). Type string `json:"type" yaml:"type"` + + // BestPracticeReference is a helpful best practice references of the test case. + BestPracticeReference string `json:"BestPracticeReference" yaml:"BestPracticeReference"` } func formTestURL(suite, name string) string { @@ -51,6 +56,11 @@ func formTestURL(suite, name string) string { } var ( + // TestIdToClaimId converts the testcase short ID to the claim identifier + TestIDToClaimID = map[string]claim.Identifier{} + + // BaseDomain for the test cases + TestIDBaseDomain = url // TestHostResourceIdentifier tests container best practices. TestHostResourceIdentifier = claim.Identifier{ Url: formTestURL(common.AccessControlTestKey, "host-resource"), @@ -61,21 +71,6 @@ var ( Url: formTestURL(common.AffiliatedCertTestKey, "container-is-certified"), Version: versionOne, } - // TestExtractNodeInformationIdentifier is a test which extracts Node information. - TestExtractNodeInformationIdentifier = claim.Identifier{ - Url: formTestURL(common.DiagnosticTestKey, "extract-node-information"), - Version: versionOne, - } - // TestListCniPluginsIdentifier retrieves list of CNI plugins. - TestListCniPluginsIdentifier = claim.Identifier{ - Url: formTestURL(common.DiagnosticTestKey, "list-cni-plugins"), - Version: versionOne, - } - // TestNodesHwInfoIdentifier retrieves nodes HW info. - TestNodesHwInfoIdentifier = claim.Identifier{ - Url: formTestURL(common.DiagnosticTestKey, "nodes-hw-info"), - Version: versionOne, - } // TestHugepagesNotManuallyManipulated represents the test identifier testing hugepages have not been manipulated. TestHugepagesNotManuallyManipulated = claim.Identifier{ Url: formTestURL(common.PlatformAlterationTestKey, "hugepages-config"), @@ -86,16 +81,26 @@ var ( Url: formTestURL(common.NetworkingTestKey, "icmpv4-connectivity"), Version: versionOne, } + // TestICMPv6ConnectivityIdentifier tests icmpv6 connectivity. + TestICMPv6ConnectivityIdentifier = claim.Identifier{ + Url: formTestURL(common.NetworkingTestKey, "icmpv6-connectivity"), + Version: versionOne, + } + // TestICMPv4ConnectivityIdentifier tests icmpv4 Multus connectivity. + TestICMPv4ConnectivityMultusIdentifier = claim.Identifier{ + Url: formTestURL(common.NetworkingTestKey, "icmpv4-connectivity-multus"), + Version: versionOne, + } + // TestICMPv6ConnectivityIdentifier tests icmpv6 Multus connectivity. + TestICMPv6ConnectivityMultusIdentifier = claim.Identifier{ + Url: formTestURL(common.NetworkingTestKey, "icmpv6-connectivity-multus"), + Version: versionOne, + } // TestNamespaceBestPracticesIdentifier ensures the namespace has followed best namespace practices. TestNamespaceBestPracticesIdentifier = claim.Identifier{ Url: formTestURL(common.AccessControlTestKey, "namespace"), Version: versionOne, } - // TestNonDefaultGracePeriodIdentifier tests best grace period practices. - TestNonDefaultGracePeriodIdentifier = claim.Identifier{ - Url: formTestURL(common.LifecycleTestKey, "pod-termination-grace-period"), - Version: versionOne, - } // TestNonTaintedNodeKernelsIdentifier is the identifier for the test checking tainted nodes. TestNonTaintedNodeKernelsIdentifier = claim.Identifier{ Url: formTestURL(common.PlatformAlterationTestKey, "tainted-node-kernel"), @@ -111,6 +116,11 @@ var ( Url: formTestURL(common.AffiliatedCertTestKey, "operator-is-certified"), Version: versionOne, } + // TestHelmIsCertifiedIdentifier tests that helm chart has passed helm certification. + TestHelmIsCertifiedIdentifier = claim.Identifier{ + Url: formTestURL(common.AffiliatedCertTestKey, "helmchart-is-certified"), + Version: versionOne, + } // TestOperatorIsInstalledViaOLMIdentifier tests that an Operator is installed via OLM. TestOperatorIsInstalledViaOLMIdentifier = claim.Identifier{ Url: formTestURL(common.OperatorTestKey, "install-source"), @@ -139,6 +149,11 @@ var ( Url: formTestURL(common.LifecycleTestKey, "pod-owner-type"), Version: versionOne, } + // TestImagePullPolicyIdentifier ensures represent image pull policy practices. + TestImagePullPolicyIdentifier = claim.Identifier{ + Url: formTestURL(common.LifecycleTestKey, "image-pull-policy"), + Version: versionOne, + } // TestPodRecreationIdentifier ensures recreation best practices. TestPodRecreationIdentifier = claim.Identifier{ Url: formTestURL(common.LifecycleTestKey, "pod-recreation"), @@ -154,6 +169,11 @@ var ( Url: formTestURL(common.AccessControlTestKey, "pod-service-account"), Version: versionOne, } + // + TestPodAutomountServiceAccountIdentifier = claim.Identifier{ + Url: formTestURL(common.AccessControlTestKey, "pod-automount-service-account-token"), + Version: versionOne, + } // TestServicesDoNotUseNodeportsIdentifier ensures Services don't utilize NodePorts. TestServicesDoNotUseNodeportsIdentifier = claim.Identifier{ Url: formTestURL(common.NetworkingTestKey, "service-type"), @@ -174,17 +194,97 @@ var ( Url: formTestURL(common.ObservabilityTestKey, "container-logging"), Version: versionOne, } + // TestCrdsStatusSubresourceIdentifier ensures all CRDs have a valid status subresource + TestCrdsStatusSubresourceIdentifier = claim.Identifier{ + Url: formTestURL(common.ObservabilityTestKey, "crd-status"), + Version: versionOne, + } // TestShudtownIdentifier ensures pre-stop lifecycle is defined TestShudtownIdentifier = claim.Identifier{ Url: formTestURL(common.LifecycleTestKey, "container-shutdown"), Version: versionOne, } + + // TestLivenessIdentifier ensure liveness is defined. + TestLivenessIdentifier = claim.Identifier{ + Url: formTestURL(common.LifecycleTestKey, "liveness"), + Version: versionOne, + } + + // TestReadinessIdentifier ensure readiness is defined. + TestReadinessIdentifier = claim.Identifier{ + Url: formTestURL(common.LifecycleTestKey, "readiness"), + Version: versionOne, + } + + // TestSysctlConfigsIdentifier ensures that the node's sysctl configs are consistent with the MachineConfig CR + TestSysctlConfigsIdentifier = claim.Identifier{ + Url: formTestURL(common.PlatformAlterationTestKey, "sysctl-config"), + Version: versionOne, + } + // TestDeploymentScalingIdentifier ensures deployment scale in/out operations work correctly. + TestDeploymentScalingIdentifier = claim.Identifier{ + Url: formTestURL(common.LifecycleTestKey, "deployment-scaling"), + Version: versionOne, + } + // TestStateFulSetScalingIdentifier ensures statefulset scale in/out operations work correctly. + TestStateFulSetScalingIdentifier = claim.Identifier{ + Url: formTestURL(common.LifecycleTestKey, "statefulset-scaling"), + Version: versionOne, + } + // TestIsRedHatReleaseIdentifier ensures platform is defined + TestIsRedHatReleaseIdentifier = claim.Identifier{ + Url: formTestURL(common.PlatformAlterationTestKey, "isredhat-release"), + Version: versionOne, + } + TestUndeclaredContainerPortsUsage = claim.Identifier{ + Url: formTestURL(common.NetworkingTestKey, "undeclared-container-ports-usage"), + Version: versionOne, + } ) func formDescription(identifier claim.Identifier, description string) string { return fmt.Sprintf("%s %s", identifier.Url, description) } +// XformToGinkgoItIdentifier transform the claim.Identifier into a test Id that can be used to skip +// specific tests +func XformToGinkgoItIdentifier(identifier claim.Identifier) string { + return XformToGinkgoItIdentifierExtended(identifier, "") +} + +// XformToGinkgoItIdentifierExtended transform the claim.Identifier into a test Id that can be used to skip +// specific tests +func XformToGinkgoItIdentifierExtended(identifier claim.Identifier, extra string) string { + itID := strings.ReplaceAll(strings.Join(GetSuiteAndTestFromIdentifier(identifier), "/"), "/", "-") + var key string + if extra != "" { + key = itID + "-" + extra + } else { + key = itID + } + TestIDToClaimID[key] = identifier + return key +} + +// it extracts the suite name and test name from a claim.Identifier based +// on the const url which contains a base domain +// From a claim.Identifier.url: +// http://test-network-function.com/testcases/SuiteName/TestName +// It extracts SuiteName and TestName + +func GetSuiteAndTestFromIdentifier(identifier claim.Identifier) []string { + result := strings.Split(identifier.Url, url+"/") + const SPLITN = 2 + // len 2, the baseDomain can appear only once in the url + // so it returns what you have previous and before basedomain + if len(result) != SPLITN { + return nil + } + // Return only the first two items in the slice. + return strings.Split(result[1], "/")[0:2] +} + // Catalog is the JUnit testcase catalog of tests. var Catalog = map[claim.Identifier]TestCaseDescription{ @@ -205,7 +305,10 @@ cannot be followed.`, 6. The Pod is not granted SYS_ADMIN SCC. 7. The Pod does not run as root. 8. The Pod does not allow privileged escalation. +9. The Pod is not granted NET_RAW SCC. +10. The Pod is not granted IPC_LOCK SCC. `), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", }, TestContainerIsCertifiedIdentifier: { @@ -213,21 +316,15 @@ cannot be followed.`, Type: normativeResult, Remediation: `Ensure that your container has passed the Red Hat Container Certification Program (CCP).`, Description: formDescription(TestContainerIsCertifiedIdentifier, - `tests whether container images have passed the Red Hat Container Certification Program (CCP).`), - }, - - TestExtractNodeInformationIdentifier: { - Identifier: TestExtractNodeInformationIdentifier, - Type: informativeResult, - Description: formDescription(TestExtractNodeInformationIdentifier, - `extracts informational information about the cluster.`), + `tests whether container images listed in the configuration file or used by test target Pods have passed the Red Hat Container + Certification Program (CCP) with a [health index](https://redhat-connect.gitbook.io/catalog-help/container-images/container-health) C or above.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.3.7", }, - TestHugepagesNotManuallyManipulated: { Identifier: TestHugepagesNotManuallyManipulated, Type: normativeResult, Remediation: `HugePage settings should be configured either directly through the MachineConfigOperator or indirectly using the -PeformanceAddonOperator. This ensures that OpenShift is aware of the special MachineConfig requirements, and can +PerformanceAddonOperator. This ensures that OpenShift is aware of the special MachineConfig requirements, and can provision your CNF on a Node that is part of the corresponding MachineConfigSet. Avoid making changes directly to an underlying Node, and let OpenShift handle the heavy lifting of configuring advanced settings.`, Description: formDescription(TestHugepagesNotManuallyManipulated, @@ -236,46 +333,68 @@ underlying Node. This test case applies only to Nodes that are configured with the "worker" MachineConfig is polled, and the Hugepage settings are extracted. Next, the underlying Nodes are polled for configured HugePages through inspection of /proc/meminfo. The results are compared, and the test passes only if they are the same.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", }, TestICMPv4ConnectivityIdentifier: { Identifier: TestICMPv4ConnectivityIdentifier, Type: normativeResult, - Remediation: `Ensure that the CNF is able to communicate via the Default OpenShift network. In some rare cases, -CNFs may require routing table changes in order to communicate over the Default network. In other cases, if the -Container base image does not provide the "ip" or "ping" binaries, this test may not be applicable. For instructions on -how to exclude a particular container from ICMPv4 connectivity tests, consult: -[README.md](https://github.com/test-network-function/test-network-function#issue-161-some-containers-under-test-do-nto-contain-ping-or-ip-binary-utilities).`, + Remediation: `Ensure that the CNF is able to communicate via the Default OpenShift network. In some rare cases, +CNFs may require routing table changes in order to communicate over the Default network. To exclude a particular pod +from ICMPv4 connectivity tests, add the test-network-function.com/skip_connectivity_tests label to it. The label value is not important, only its presence.`, Description: formDescription(TestICMPv4ConnectivityIdentifier, `checks that each CNF Container is able to communicate via ICMPv4 on the Default OpenShift network. This -test case requires the Deployment of the -[CNF Certification Test Partner](https://github.com/test-network-function/cnf-certification-test-partner/blob/main/test-partner/partner.yaml). -The test ensures that all CNF containers respond to ICMPv4 requests from the Partner Pod, and vice-versa. -`), +test case requires the Deployment of the debug daemonset.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", + }, + + TestICMPv6ConnectivityIdentifier: { + Identifier: TestICMPv6ConnectivityIdentifier, + Type: normativeResult, + Remediation: `Ensure that the CNF is able to communicate via the Default OpenShift network. In some rare cases, +CNFs may require routing table changes in order to communicate over the Default network. To exclude a particular pod +from ICMPv6 connectivity tests, add the test-network-function.com/skip_connectivity_tests label to it. The label value is not important, only its presence.`, + Description: formDescription(TestICMPv6ConnectivityIdentifier, + `checks that each CNF Container is able to communicate via ICMPv6 on the Default OpenShift network. This +test case requires the Deployment of the debug daemonset.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", + }, + + TestICMPv4ConnectivityMultusIdentifier: { + Identifier: TestICMPv4ConnectivityMultusIdentifier, + Type: normativeResult, + Remediation: `Ensure that the CNF is able to communicate via the Multus network(s). In some rare cases, +CNFs may require routing table changes in order to communicate over the Multus network(s). To exclude a particular pod +from ICMPv4 connectivity tests, add the test-network-function.com/skip_connectivity_tests label to it. The label value is not important, only its presence.`, + Description: formDescription(TestICMPv4ConnectivityMultusIdentifier, + `checks that each CNF Container is able to communicate via ICMPv4 on the Multus network(s). This +test case requires the Deployment of the debug daemonset.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", + }, + + TestICMPv6ConnectivityMultusIdentifier: { + Identifier: TestICMPv6ConnectivityMultusIdentifier, + Type: normativeResult, + Remediation: `Ensure that the CNF is able to communicate via the Multus network(s). In some rare cases, +CNFs may require routing table changes in order to communicate over the Multus network(s). To exclude a particular pod +from ICMPv6 connectivity tests, add the test-network-function.com/skip_connectivity_tests label to it.The label value is not important, only its presence. +`, + Description: formDescription(TestICMPv6ConnectivityMultusIdentifier, + `checks that each CNF Container is able to communicate via ICMPv6 on the Multus network(s). This +test case requires the Deployment of the debug daemonset.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", }, TestNamespaceBestPracticesIdentifier: { Identifier: TestNamespaceBestPracticesIdentifier, Type: normativeResult, - Remediation: `Ensure that your CNF utilizes a CNF-specific namespace. Additionally, the CNF-specific namespace -should not start with "openshift-", except in rare cases.`, + Remediation: `Ensure that your CNF utilizes namespaces declared in the yaml config file. Additionally, +the namespaces should not start with "default, openshift-, istio- or aspenmesh-", except in rare cases.`, Description: formDescription(TestNamespaceBestPracticesIdentifier, - `tests that CNFs utilize a CNF-specific namespace, and that the namespace does not start with "openshift-". -OpenShift may host a variety of CNF and software applications, and multi-tenancy of such applications is supported -through namespaces. As such, each CNF should be a good neighbor, and utilize an appropriate, unique namespace.`), - }, - - TestNonDefaultGracePeriodIdentifier: { - Identifier: TestNonDefaultGracePeriodIdentifier, - Type: informativeResult, - Remediation: `Choose a terminationGracePeriod that is appropriate for your given CNF. If the default (30s) is appropriate, then feel -free to ignore this informative message. This test is meant to raise awareness around how Pods are terminated, and to -suggest that a CNF is configured based on its requirements. In addition to a terminationGracePeriod, consider utilizing -a termination hook in the case that your application requires special shutdown instructions.`, - Description: formDescription(TestNonDefaultGracePeriodIdentifier, - `tests whether the terminationGracePeriod is CNF-specific, or if the default (30s) is utilized. This test is -informative, and will not affect CNF Certification. In many cases, the default terminationGracePeriod is perfectly -acceptable for a CNF.`), + `tests that all CNF's resources (PUTs and CRs) belong to valid namespaces. A valid namespace meets +the following conditions: (1) It was declared in the yaml config file under the targetNameSpaces +tag. (2) It doesn't have any of the following prefixes: default, openshift-, istio- and aspenmesh-`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2, 16.3.8 & 16.3.9", }, TestNonTaintedNodeKernelsIdentifier: { @@ -287,6 +406,7 @@ Node(s) kernels in order to run the CNF.`, `ensures that the Node(s) hosting CNFs do not utilize tainted kernels. This test case is especially important to support Highly Available CNFs, since when a CNF is re-instantiated on a backup Node, that Node's kernel may not have the same hacks.'`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2.14", }, TestOperatorInstallStatusIdentifier: { @@ -296,7 +416,9 @@ the same hacks.'`), Description: formDescription(TestOperatorInstallStatusIdentifier, `Ensures that CNF Operators abide by best practices. The following is tested: 1. The Operator CSV reports "Installed" status. -2. TODO: Describe operator scc check.`), +2. The operator is not installed with privileged rights. Test passes if clusterPermissions is not present in the CSV manifest or is present +with no resourceNames under its rules.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2.12 and Section 6.3.3", }, TestOperatorIsCertifiedIdentifier: { @@ -304,7 +426,17 @@ the same hacks.'`), Type: normativeResult, Remediation: `Ensure that your Operator has passed Red Hat's Operator Certification Program (OCP).`, Description: formDescription(TestOperatorIsCertifiedIdentifier, - `tests whether CNF Operators have passed the Red Hat Operator Certification Program (OCP).`), + `tests whether CNF Operators listed in the configuration file have passed the Red Hat Operator Certification Program (OCP).`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2.12 and Section 6.3.3", + }, + + TestHelmIsCertifiedIdentifier: { + Identifier: TestHelmIsCertifiedIdentifier, + Type: normativeResult, + Remediation: `Ensure that the helm charts under test passed the Red Hat's helm Certification Program (e.g. listed in https://charts.openshift.io/index.yaml).`, + Description: formDescription(TestHelmIsCertifiedIdentifier, + `tests whether helm charts listed in the cluster passed the Red Hat Helm Certification Program.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2.12 and Section 6.3.3", }, TestOperatorIsInstalledViaOLMIdentifier: { @@ -313,6 +445,7 @@ the same hacks.'`), Remediation: `Ensure that your Operator is installed via OLM.`, Description: formDescription(TestOperatorIsInstalledViaOLMIdentifier, `tests whether a CNF Operator is installed via OLM.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2.12 and Section 6.3.3", }, TestPodNodeSelectorAndAffinityBestPractices: { @@ -325,6 +458,7 @@ to why nodeSelector and/or nodeAffinity is utilized by a CNF.`, Description: formDescription(TestPodNodeSelectorAndAffinityBestPractices, `ensures that CNF Pods do not specify nodeSelector or nodeAffinity. In most cases, Pods should allow for instantiation on any underlying Node.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", }, TestPodHighAvailabilityBestPractices: { @@ -333,6 +467,7 @@ instantiation on any underlying Node.`), Remediation: `In high availability cases, Pod podAntiAffinity rule should be specified for pod scheduling and pod replica value is set to more than 1 .`, Description: formDescription(TestPodHighAvailabilityBestPractices, `ensures that CNF Pods specify podAntiAffinity rules and replica value is set to more than 1.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", }, TestPodClusterRoleBindingsBestPracticesIdentifier: { @@ -342,14 +477,24 @@ instantiation on any underlying Node.`), ClusterRoleBindings, if possible.`, Description: formDescription(TestPodClusterRoleBindingsBestPracticesIdentifier, `tests that a Pod does not specify ClusterRoleBindings.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2.10 and 6.3.6", }, TestPodDeploymentBestPracticesIdentifier: { Identifier: TestPodDeploymentBestPracticesIdentifier, Type: normativeResult, - Remediation: `Deploy the CNF using DaemonSet or ReplicaSet.`, + Remediation: `Deploy the CNF using ReplicaSet/StatefulSet.`, Description: formDescription(TestPodDeploymentBestPracticesIdentifier, - `tests that CNF Pod(s) are deployed as part of a ReplicaSet(s).`), + `tests that CNF Pod(s) are deployed as part of a ReplicaSet(s)/StatefulSet(s).`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.3.3 and 6.3.8", + }, + TestImagePullPolicyIdentifier: { + Identifier: TestImagePullPolicyIdentifier, + Type: normativeResult, + Remediation: `Ensure that the containers under test are using IfNotPresent as Image Pull Policy.`, + Description: formDescription(TestImagePullPolicyIdentifier, + `Ensure that the containers under test are using IfNotPresent as Image Pull Policy..`), + BestPracticeReference: bestPracticeDocV1dot3URL + " Section 15.6", }, TestPodRoleBindingsBestPracticesIdentifier: { @@ -358,6 +503,7 @@ ClusterRoleBindings, if possible.`, Remediation: `Ensure the CNF is not configured to use RoleBinding(s) in a non-CNF Namespace.`, Description: formDescription(TestPodRoleBindingsBestPracticesIdentifier, `ensures that a CNF does not utilize RoleBinding(s) in a non-CNF Namespace.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.3.3 and 6.3.5", }, TestPodServiceAccountBestPracticesIdentifier: { @@ -366,14 +512,16 @@ ClusterRoleBindings, if possible.`, Remediation: `Ensure that the each CNF Pod is configured to use a valid Service Account`, Description: formDescription(TestPodServiceAccountBestPracticesIdentifier, `tests that each CNF Pod utilizes a valid Service Account.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2.3 and 6.2.7", }, TestServicesDoNotUseNodeportsIdentifier: { Identifier: TestServicesDoNotUseNodeportsIdentifier, Type: normativeResult, - Remediation: `Ensure Services are not configured to not use NodePort(s).`, + Remediation: `Ensure Services are not configured to use NodePort(s).`, Description: formDescription(TestServicesDoNotUseNodeportsIdentifier, `tests that each CNF Service does not utilize NodePort(s).`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.3.1", }, TestUnalteredBaseImageIdentifier: { @@ -405,32 +553,18 @@ that there are no changes to the following directories: 8) /usr/sbin 9) /usr/lib 10) /usr/lib64`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2.2", }, TestUnalteredStartupBootParamsIdentifier: { Identifier: TestUnalteredStartupBootParamsIdentifier, Type: normativeResult, - Remediation: `Ensure that boot parameters are set directly through the MachineConfigOperator, or indirectly through the -PerfromanceAddonOperator. Boot parameters should not be changed directly through the Node, as OpenShift should manage + Remediation: `Ensure that boot parameters are set directly through the MachineConfigOperator, or indirectly through the PerformanceAddonOperator. Boot parameters should not be changed directly through the Node, as OpenShift should manage the changes for you.`, Description: formDescription(TestUnalteredStartupBootParamsIdentifier, `tests that boot parameters are set through the MachineConfigOperator, and not set manually on the Node.`), + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2.13 and 6.2.14", }, - TestListCniPluginsIdentifier: { - Identifier: TestListCniPluginsIdentifier, - Type: normativeResult, - Remediation: "", - Description: formDescription(TestListCniPluginsIdentifier, - `lists CNI plugins`), - }, - TestNodesHwInfoIdentifier: { - Identifier: TestNodesHwInfoIdentifier, - Type: normativeResult, - Remediation: "", - Description: formDescription(TestNodesHwInfoIdentifier, - `list nodes HW info`), - }, - TestShudtownIdentifier: { Identifier: TestShudtownIdentifier, Type: normativeResult, @@ -438,7 +572,7 @@ the changes for you.`, `Ensure that the containers lifecycle pre-stop management feature is configured.`), Remediation: ` It's considered best-practices to define prestop for proper management of container lifecycle. - The prestop can be used to gracefully stop the container and clean resources (e.g., DB connexion). + The prestop can be used to gracefully stop the container and clean resources (e.g., DB connection). The prestop can be configured using : 1) Exec : executes the supplied command inside the container @@ -446,9 +580,26 @@ the changes for you.`, When defined. K8s will handle shutdown of the container using the following: 1) K8s first execute the preStop hook inside the container. - 2) K8s will wait for a grace perdiod. + 2) K8s will wait for a grace period. 3) K8s will clean the remaining processes using KILL signal. `, + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", + }, + TestReadinessIdentifier: { + Identifier: TestReadinessIdentifier, + Type: normativeResult, + Description: formDescription(TestReadinessIdentifier, + `Checks that all pods under test have a readiness probe defined.`), + Remediation: `Ensure that all CNF's pods under test have a readiness probe defined.`, + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", + }, + TestLivenessIdentifier: { + Identifier: TestLivenessIdentifier, + Type: normativeResult, + Description: formDescription(TestLivenessIdentifier, + `Checks that all pods under test have a liveness probe defined.`), + Remediation: `Ensure that all CNF's pods under test have a liveness probe defined.`, + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", }, TestPodRecreationIdentifier: { Identifier: TestPodRecreationIdentifier, @@ -460,5 +611,80 @@ the changes for you.`, and that the actual replica count matches the desired replica count.`), Remediation: `Ensure that CNF Pod(s) utilize a configuration that supports High Availability. Additionally, ensure that there are available Nodes in the OpenShift cluster that can be utilized in the event that a host Node fails.`, + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", + }, + TestSysctlConfigsIdentifier: { + Identifier: TestSysctlConfigsIdentifier, + Type: normativeResult, + Description: formDescription(TestSysctlConfigsIdentifier, + `tests that no one has changed the node's sysctl configs after the node + was created, the tests works by checking if the sysctl configs are consistent with the + MachineConfig CR which defines how the node should be configured`), + Remediation: `You should recreate the node or change the sysctls, recreating is recommended because there might be other unknown changes`, + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", + }, + TestDeploymentScalingIdentifier: { + Identifier: TestDeploymentScalingIdentifier, + Type: normativeResult, + Description: formDescription(TestDeploymentScalingIdentifier, + `tests that CNF deployments support scale in/out operations. + First, The test starts getting the current replicaCount (N) of the deployment/s with the Pod Under Test. Then, it executes the + scale-in oc command for (N-1) replicas. Lastly, it executes the scale-out oc command, restoring the original replicaCount of the deployment/s. + In case of deployments that are managed by HPA the test is changing the min and max value to deployment Replica - 1 during scale-in and the + original replicaCount again for both min/max during the scale-out stage. lastly its restoring the original min/max replica of the deployment/s`), + Remediation: `Make sure CNF deployments/replica sets can scale in/out successfully.`, + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", + }, + TestStateFulSetScalingIdentifier: { + Identifier: TestStateFulSetScalingIdentifier, + Type: normativeResult, + Description: formDescription(TestStateFulSetScalingIdentifier, + `tests that CNF statefulsets support scale in/out operations. + First, The test starts getting the current replicaCount (N) of the statefulset/s with the Pod Under Test. Then, it executes the + scale-in oc command for (N-1) replicas. Lastly, it executes the scale-out oc command, restoring the original replicaCount of the statefulset/s. + In case of statefulsets that are managed by HPA the test is changing the min and max value to statefulset Replica - 1 during scale-in and the + original replicaCount again for both min/max during the scale-out stage. lastly its restoring the original min/max replica of the statefulset/s`), + Remediation: `Make sure CNF statefulsets/replica sets can scale in/out successfully.`, + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", + }, + TestIsRedHatReleaseIdentifier: { + Identifier: TestIsRedHatReleaseIdentifier, + Type: normativeResult, + Description: formDescription(TestIsRedHatReleaseIdentifier, + `verifies if the container base image is redhat.`), + Remediation: `build a new docker image that's based on UBI (redhat universal base image).`, + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", + }, + TestCrdsStatusSubresourceIdentifier: { + Identifier: TestCrdsStatusSubresourceIdentifier, + Type: informativeResult, + Description: formDescription(TestCrdsStatusSubresourceIdentifier, + `checks that all CRDs have a status subresource specification.`), + Remediation: `make sure that all the CRDs have a meaningful status specification.`, + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 6.2", + }, + TestLoggingIdentifier: { + Identifier: TestLoggingIdentifier, + Type: informativeResult, + Description: formDescription(TestLoggingIdentifier, + `check that all containers under test use standard input output and standard error when logging`), + Remediation: `make sure containers are not redirecting stdout/stderr`, + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 11.1", + }, + TestPodAutomountServiceAccountIdentifier: { + Identifier: TestPodAutomountServiceAccountIdentifier, + Type: normativeResult, + Description: formDescription(TestPodAutomountServiceAccountIdentifier, + `check that all pods under test have automountServiceAccountToken set to false`), + Remediation: `check that pod has automountServiceAccountToken set to false or pod is attached to service account which has automountServiceAccountToken set to false`, + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 13.7", + }, + TestUndeclaredContainerPortsUsage: { + Identifier: TestUndeclaredContainerPortsUsage, + Type: normativeResult, + Description: formDescription(TestUndeclaredContainerPortsUsage, + `check that containers don't listen on ports that weren't declared in their specification`), + Remediation: `ensure the CNF apps don't listen on undeclared containers' ports`, + BestPracticeReference: bestPracticeDocV1dot2URL + " Section 16.3.1.1", }, } diff --git a/test-network-function/lifecycle/doc.go b/test-network-function/lifecycle/doc.go index 76b93ac91..1d197a6b8 100644 --- a/test-network-function/lifecycle/doc.go +++ b/test-network-function/lifecycle/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -16,6 +16,6 @@ /* Package lifecycle contains k8s resource lifecycle related tests, such as pod -scheduling, scaling, temination etc. +scheduling, scaling, termination etc. */ package lifecycle diff --git a/test-network-function/lifecycle/suite.go b/test-network-function/lifecycle/suite.go index 3d8fe7a40..e83040067 100644 --- a/test-network-function/lifecycle/suite.go +++ b/test-network-function/lifecycle/suite.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -19,34 +19,35 @@ package lifecycle import ( "fmt" "path" - "sort" + "strings" "time" - "github.com/test-network-function/test-network-function/pkg/tnf/handlers/generic" - "github.com/test-network-function/test-network-function/pkg/tnf/testcases" - - "github.com/test-network-function/test-network-function/test-network-function/common" - "github.com/test-network-function/test-network-function/test-network-function/identifiers" - "github.com/test-network-function/test-network-function/test-network-function/results" - - "github.com/onsi/ginkgo" - ginkgoconfig "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" log "github.com/sirupsen/logrus" + "github.com/test-network-function/test-network-function/pkg/config" + "github.com/test-network-function/test-network-function/pkg/config/configsections" "github.com/test-network-function/test-network-function/pkg/tnf" - dp "github.com/test-network-function/test-network-function/pkg/tnf/handlers/deployments" dd "github.com/test-network-function/test-network-function/pkg/tnf/handlers/deploymentsdrain" - dn "github.com/test-network-function/test-network-function/pkg/tnf/handlers/deploymentsnodes" - "github.com/test-network-function/test-network-function/pkg/tnf/handlers/graceperiod" "github.com/test-network-function/test-network-function/pkg/tnf/handlers/nodeselector" "github.com/test-network-function/test-network-function/pkg/tnf/handlers/owners" + ps "github.com/test-network-function/test-network-function/pkg/tnf/handlers/podsets" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/scaling" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" "github.com/test-network-function/test-network-function/pkg/tnf/reel" + "github.com/test-network-function/test-network-function/pkg/tnf/testcases" + "github.com/test-network-function/test-network-function/pkg/utils" + "github.com/test-network-function/test-network-function/test-network-function/common" + "github.com/test-network-function/test-network-function/test-network-function/identifiers" + "github.com/test-network-function/test-network-function/test-network-function/results" ) const ( - defaultTerminationGracePeriod = 30 - drainTimeoutMinutes = 5 - partnerPod = "partner" + baseNodeDrainTimeout = 5 * time.Minute + maxNodeDrainTimeout = 30 * time.Minute + scalingTimeout = 1 * time.Minute + scalingPollingPeriod = 1 * time.Second + postNodeDrainRecoveryTimeOut = 2 * time.Minute ) var ( @@ -56,338 +57,704 @@ var ( // shutdownTestPath is the file location of shutdown.json test case relative to the project root. shutdownTestPath = path.Join("pkg", "tnf", "handlers", "shutdown", "shutdown.json") + // livenessTestPath is the file location of liveness.json test case relative to the project root. + livenessTestPath = path.Join("pkg", "tnf", "handlers", "liveness", "liveness.json") + + // readinessTestPath is the file location of readiness.json test case relative to the project root. + readinessTestPath = path.Join("pkg", "tnf", "handlers", "readiness", "readiness.json") + // shutdownTestDirectoryPath is the directory of the shutdown test shutdownTestDirectoryPath = path.Join("pkg", "tnf", "handlers", "shutdown") + // livenessTestDirectoryPath is the directory of the liveness test + livenessTestDirectoryPath = path.Join("pkg", "tnf", "handlers", "liveness") + + // readinessTestDirectoryPath is the directory of the readiness test + readinessTestDirectoryPath = path.Join("pkg", "tnf", "handlers", "readiness") + // relativeNodesTestPath is the relative path to the nodes.json test case. relativeNodesTestPath = path.Join(common.PathRelativeToRoot, nodeUncordonTestPath) // relativeShutdownTestPath is the relative path to the shutdown.json test case. relativeShutdownTestPath = path.Join(common.PathRelativeToRoot, shutdownTestPath) + // relativeLivenessTestPath is the relative path to the liveness.json test case. + relativeLivenessTestPath = path.Join(common.PathRelativeToRoot, livenessTestPath) + + // relativeReadinessTestPath is the relative path to the readiness.json test case. + relativeReadinessTestPath = path.Join(common.PathRelativeToRoot, readinessTestPath) + // relativeShutdownTestDirectoryPath is the directory of the shutdown directory relativeShutdownTestDirectoryPath = path.Join(common.PathRelativeToRoot, shutdownTestDirectoryPath) + // relativelivenessTestDirectoryPath is the directory of the liveness directory + relativeLivenessTestDirectoryPath = path.Join(common.PathRelativeToRoot, livenessTestDirectoryPath) + + // relativereadinessTestDirectoryPath is the directory of the readiness directory + relativeReadinessTestDirectoryPath = path.Join(common.PathRelativeToRoot, readinessTestDirectoryPath) + // podAntiAffinityTestPath is the file location of the podantiaffinity.json test case relative to the project root. podAntiAffinityTestPath = path.Join("pkg", "tnf", "handlers", "podantiaffinity", "podantiaffinity.json") // relativePodTestPath is the relative path to the podantiaffinity.json test case. relativePodTestPath = path.Join(common.PathRelativeToRoot, podAntiAffinityTestPath) -) -var drainTimeout = time.Duration(drainTimeoutMinutes) * time.Minute + // relativeimagepullpolicyTestPath is the relative path to the imagepullpolicy.json test case. + imagepullpolicyTestPath = path.Join("pkg", "tnf", "handlers", "imagepullpolicy", "imagepullpolicy.json") + relativeimagepullpolicyTestPath = path.Join(common.PathRelativeToRoot, imagepullpolicyTestPath) +) // // All actual test code belongs below here. Utilities belong above. // var _ = ginkgo.Describe(common.LifecycleTestKey, func() { - configData := common.ConfigurationData{} - ginkgo.BeforeSuite(func() { - common.Loadconfiguration(&configData) - log.Info(configData.ContainersUnderTest) - }) - ginkgo.BeforeEach(func() { - common.ReloadConfiguration(&configData) - }) - if testcases.IsInFocus(ginkgoconfig.GinkgoConfig.FocusStrings, common.LifecycleTestKey) { + conf, _ := ginkgo.GinkgoConfiguration() + if testcases.IsInFocus(conf.FocusStrings, common.LifecycleTestKey) { + env := config.GetTestEnvironment() + ginkgo.BeforeEach(func() { + env.LoadAndRefresh() + gomega.Expect(len(env.PodsUnderTest)).ToNot(gomega.Equal(0)) + gomega.Expect(len(env.ContainersUnderTest)).ToNot(gomega.Equal(0)) + + }) - testNodeSelector(&configData) + ginkgo.ReportAfterEach(results.RecordResult) + ginkgo.AfterEach(env.CloseLocalShellContext) - testGracePeriod(&configData) + testImagePolicy(env) - testShutdown(&configData) + testNodeSelector(env) - testPodAntiAffinity(&configData) + testShutdown(env) - if !common.NonIntrusive() { - testPodsRecreation(&configData) + testLiveness(env) + + testReadiness(env) + + testPodAntiAffinity(env) + + if common.Intrusive() { + testPodsRecreation(env) + + testScaling(env) + testStateFulSetScaling(env) } - testOwner(&configData) + testOwner(env) } }) -func testNodeSelector(configData *common.ConfigurationData) { - ginkgo.It("Testing pod nodeSelector", func() { - for _, cut := range configData.ContainersUnderTest { - podName := cut.Oc.GetPodName() - podNamespace := cut.Oc.GetPodNamespace() - ginkgo.By(fmt.Sprintf("Testing pod nodeSelector %s/%s", cut.Oc.GetPodNamespace(), podName)) - defer results.RecordResult(identifiers.TestPodNodeSelectorAndAffinityBestPractices) - infoWriter := tnf.CreateTestExtraInfoWriter() - tester := nodeselector.NewNodeSelector(common.DefaultTimeout, podName, podNamespace) - test, err := tnf.NewTest(cut.Oc.GetExpecter(), tester, []reel.Handler{tester}, cut.Oc.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - testResult, err := test.Run() - gomega.Expect(err).To(gomega.BeNil()) - if testResult != tnf.SUCCESS { - msg := fmt.Sprintf("The pod specifies nodeSelector/nodeAffinity field, you might want to change it, %s %s", podNamespace, podName) - log.Warn(msg) - infoWriter(msg) - } +func waitForAllPodSetsReady(namespace string, timeout, pollingPeriod time.Duration, resourceType configsections.PodSetType, context *interactive.Context) int { //nolint:unparam // it is fine to use always the same value for timeout + var elapsed time.Duration + var notReadyPodSets []string + + for elapsed < timeout { + _, notReadyPodSets = GetPodSets(namespace, resourceType, context) + log.Debugf("Waiting for %s to get ready, remaining: %d PodSets", string(resourceType), len(notReadyPodSets)) + if len(notReadyPodSets) == 0 { + break + } + time.Sleep(pollingPeriod) + elapsed += pollingPeriod + } + return len(notReadyPodSets) +} + +// restoreDeployments is the last attempt to restore the original test deployments' replicaCount +func restoreDeployments(env *config.TestEnvironment) { + for i := range env.DeploymentsUnderTest { + // For each test deployment in the namespace, refresh the current replicas and compare. + refreshReplicas(&env.DeploymentsUnderTest[i], env) + } +} + +// restoreStateFulSet is the last attempt to restore the original test PodSets' replicaCount +func restoreStateFulSet(env *config.TestEnvironment) { + for i := range env.StateFulSetUnderTest { + // For each test StateFulSet in the namespace, refresh the current replicas and compare. + refreshReplicas(&env.StateFulSetUnderTest[i], env) + } +} + +func refreshReplicas(podset *configsections.PodSet, env *config.TestEnvironment) { + podsets, notReadyPodsets := GetPodSets(podset.Namespace, podset.Type, env.GetLocalShellContext()) + + if len(notReadyPodsets) > 0 { + // Wait until the deployment/replicaset is ready + notReady := waitForAllPodSetsReady(podset.Namespace, scalingTimeout, scalingPollingPeriod, podset.Type, env.GetLocalShellContext()) + if notReady != 0 { + collectNodeAndPendingPodInfo(podset.Namespace, env.GetLocalShellContext()) + ginkgo.AbortSuite(fmt.Sprintf("Could not restore %s replicaCount for namespace %s.", string(podset.Type), podset.Namespace)) + } + } + if podset.Hpa.HpaName != "" { // it have hpa and need to update the max min + runHpaScalingTest(podset, env.GetLocalShellContext()) + } + key := podset.Namespace + ":" + podset.Name + dep, ok := podsets[key] + if ok { + if dep.Replicas != podset.Replicas { + log.Warn(string(podset.Type), podset.Name, " replicaCount (", podset.Replicas, ") needs to be restored.") + + // Try to scale to the original deployments/statefulsets replicaCount. + runScalingTest(podset, env.GetLocalShellContext()) + + env.SetNeedsRefresh() + } + } +} + +func closeOcSessionsByPodset(containers map[configsections.ContainerIdentifier]*configsections.Container, podset *configsections.PodSet) { + log.Debug("close session for", string(podset.Type), "=", podset.Name, " start") + defer log.Debug("close session for", string(podset.Type), "=", podset.Name, " done") + for cid, c := range containers { + if cid.Namespace == podset.Namespace && strings.HasPrefix(cid.PodName, podset.Name+"-") { + log.Infof("Closing session to %s %s", cid.PodName, cid.ContainerName) + c.CloseOc() + delete(containers, cid) + } + } +} + +// runScalingTest Runs a Scaling handler TC and waits for all the deployments/statefulset to be ready. +func runScalingTest(podset *configsections.PodSet, context *interactive.Context) { + handler := scaling.NewScaling(common.DefaultTimeout, podset.Namespace, podset.Name, string(podset.Type), podset.Replicas) + test, err := tnf.NewTest(context.GetExpecter(), handler, []reel.Handler{handler}, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + test.RunAndValidate() + + // Wait until the deployment/statefulset is ready + notReady := waitForAllPodSetsReady(podset.Namespace, scalingTimeout, scalingPollingPeriod, podset.Type, context) + if notReady != 0 { + collectNodeAndPendingPodInfo(podset.Namespace, context) + ginkgo.Fail(fmt.Sprintf("Failed to scale deployment for namespace %s.", podset.Namespace)) + } +} + +func runHpaScalingTest(podset *configsections.PodSet, context *interactive.Context) { + handler := scaling.NewHpaScaling(common.DefaultTimeout, podset.Namespace, podset.Hpa.HpaName, podset.Hpa.MinReplicas, podset.Hpa.MaxReplicas) + test, err := tnf.NewTest(context.GetExpecter(), handler, []reel.Handler{handler}, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + test.RunAndValidate() + + // Wait until the deployment/statefulset is ready + notReady := waitForAllPodSetsReady(podset.Namespace, scalingTimeout, scalingPollingPeriod, podset.Type, context) + if notReady != 0 { + collectNodeAndPendingPodInfo(podset.Namespace, context) + ginkgo.Fail(fmt.Sprintf("Failed to auto-scale %s for namespace %s.", string(podset.Type), podset.Namespace)) + } +} + +func testScaling(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestDeploymentScalingIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + ginkgo.By("Testing deployment scaling") + defer restoreDeployments(env) + defer env.SetNeedsRefresh() + + if len(env.DeploymentsUnderTest) == 0 { + ginkgo.Skip("No test deployments found.") + } + for i := range env.DeploymentsUnderTest { + runScalingfunc(&env.DeploymentsUnderTest[i], env) + } + }) +} +func testStateFulSetScaling(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestStateFulSetScalingIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + ginkgo.By("Testing StatefulSet scaling") + defer restoreStateFulSet(env) + defer env.SetNeedsRefresh() + + if len(env.StateFulSetUnderTest) == 0 { + ginkgo.Skip("No test StatefulSet found.") + } + for i := range env.StateFulSetUnderTest { + runScalingfunc(&env.StateFulSetUnderTest[i], env) } }) } -func testGracePeriod(configData *common.ConfigurationData) { - ginkgo.When("Test terminationGracePeriod ", func() { - ginkgo.It("Testing pod terminationGracePeriod", func() { - for _, cut := range configData.ContainersUnderTest { - context := common.GetContext() - podName := cut.Oc.GetPodName() - podNamespace := cut.Oc.GetPodNamespace() - ginkgo.By(fmt.Sprintf("Testing pod terminationGracePeriod %s %s", podNamespace, podName)) - defer results.RecordResult(identifiers.TestNonDefaultGracePeriodIdentifier) - infoWriter := tnf.CreateTestExtraInfoWriter() - tester := graceperiod.NewGracePeriod(common.DefaultTimeout, podName, podNamespace) - test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - testResult, err := test.Run() - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) - gomega.Expect(err).To(gomega.BeNil()) - gracePeriod := tester.GetGracePeriod() - if gracePeriod == defaultTerminationGracePeriod { - msg := fmt.Sprintf("%s %s has terminationGracePeriod set to %d, you might want to change it", podNamespace, podName, defaultTerminationGracePeriod) - log.Warn(msg) - infoWriter(msg) - } - } - }) +func runScalingfunc(podset *configsections.PodSet, env *config.TestEnvironment) { + ginkgo.By(fmt.Sprintf("Scaling %s=%s, Replicas=%d (ns=%s)", string(podset.Type), podset.Name, podset.Replicas, podset.Namespace)) + + closeOcSessionsByPodset(env.ContainersUnderTest, podset) + replicaCount := podset.Replicas + podsetscale := *podset + if podsetscale.Hpa.HpaName != "" { + podsetscale.Hpa.MinReplicas = replicaCount - 1 + podsetscale.Hpa.MaxReplicas = replicaCount - 1 + runHpaScalingTest(&podsetscale, env.GetLocalShellContext()) // scale in + podsetscale.Hpa.MinReplicas = replicaCount + podsetscale.Hpa.MaxReplicas = replicaCount + runHpaScalingTest(&podsetscale, env.GetLocalShellContext()) // scale out + } else { + // ScaleIn, removing one pod from the replicaCount + podsetscale.Replicas = replicaCount - 1 + runScalingTest(&podsetscale, env.GetLocalShellContext()) + + // Scaleout, restoring the original replicaCount number + podsetscale.Replicas = replicaCount + runScalingTest(&podsetscale, env.GetLocalShellContext()) + } +} + +func testNodeSelector(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestPodNodeSelectorAndAffinityBestPractices) + ginkgo.It(testID, ginkgo.Label(testID), func() { + ginkgo.By("Testing pod nodeSelector") + context := env.GetLocalShellContext() + badPods := []configsections.Pod{} + for _, podUnderTest := range env.PodsUnderTest { + ginkgo.By(fmt.Sprintf("Testing pod nodeSelector %s/%s", podUnderTest.Namespace, podUnderTest.Name)) + tester := nodeselector.NewNodeSelector(common.DefaultTimeout, podUnderTest.Name, podUnderTest.Namespace) + test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("FAILURE: Pod %s/%s has nodeSelector/nodeAffinity rule", podUnderTest.Namespace, podUnderTest.Name) + badPods = append(badPods, *podUnderTest) + }, func(err error) { + tnf.ClaimFilePrintf("ERROR: Pod %s/%s, error: %v", podUnderTest.Namespace, podUnderTest.Name, err) + badPods = append(badPods, *podUnderTest) + }) + } + + if n := len(badPods); n > 0 { + log.Debugf("Pods with nodeSelector/nodeAffinity: %+v", badPods) + ginkgo.Fail(fmt.Sprintf("%d pods found with nodeSelector/nodeAffinity rules", n)) + } }) } -func testShutdown(configData *common.ConfigurationData) { - ginkgo.When("Testing PUTs are configured with pre-stop lifecycle", func() { - ginkgo.It("should have pre-stop configured", func() { - for _, cut := range configData.ContainersUnderTest { - podName := cut.Oc.GetPodName() - podNamespace := cut.Oc.GetPodNamespace() - ginkgo.By(fmt.Sprintf("should have pre-stop configured %s/%s", podNamespace, podName)) - defer results.RecordResult(identifiers.TestShudtownIdentifier) - shutdownTest(podNamespace, podName) +//nolint:dupl +func testShutdown(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestShudtownIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + failedPods := []*configsections.Pod{} + ginkgo.By("Testing PUTs are configured with pre-stop lifecycle") + for _, podUnderTest := range env.PodsUnderTest { + ginkgo.By(fmt.Sprintf("should have pre-stop configured %s/%s", podUnderTest.Namespace, podUnderTest.Name)) + passed := shutdownTest(podUnderTest.Namespace, podUnderTest.Name, env.GetLocalShellContext()) + if !passed { + failedPods = append(failedPods, podUnderTest) } - }) + } + if n := len(failedPods); n > 0 { + log.Debugf("Pods without pre-stop configured: %+v", failedPods) + ginkgo.Fail(fmt.Sprintf("%d pods do not have pre-stop configured.", n)) + } }) } -func shutdownTest(podNamespace, podName string) { - context := common.GetContext() +func shutdownTest(podNamespace, podName string, context *interactive.Context) bool { + passed := true values := make(map[string]interface{}) values["POD_NAMESPACE"] = podNamespace values["POD_NAME"] = podName values["GO_TEMPLATE_PATH"] = relativeShutdownTestDirectoryPath - test, handlers, result, err := generic.NewGenericFromMap(relativeShutdownTestPath, common.RelativeSchemaPath, values) + tester, handlers := utils.NewGenericTesterAndValidate(relativeShutdownTestPath, common.RelativeSchemaPath, values) + test, err := tnf.NewTest(context.GetExpecter(), *tester, handlers, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(result).ToNot(gomega.BeNil()) - gomega.Expect(result.Valid()).To(gomega.BeTrue()) - gomega.Expect(handlers).ToNot(gomega.BeNil()) - gomega.Expect(handlers).ToNot(gomega.BeNil()) gomega.Expect(test).ToNot(gomega.BeNil()) - tester, err := tnf.NewTest(context.GetExpecter(), *test, handlers, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(tester).ToNot(gomega.BeNil()) - testResult, err := tester.Run() - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("FAILURE: Pod %s/%s does not have pre-stop configured", podNamespace, podName) + passed = false + }, func(err error) { + tnf.ClaimFilePrintf("ERROR: Pod %s/%s, error: %v", podNamespace, podName, err) + passed = false + }) + return passed } -func testPodsRecreation(configData *common.ConfigurationData) { - var deployments dp.DeploymentMap - var notReadyDeployments []string - var nodesSorted []node // A slice version of nodes sorted by number of deployments descending - ginkgo.It("Testing node draining effect of deployment", func() { - configData.SetNeedsRefresh() - for _, cut := range configData.ContainersUnderTest { - namespace := cut.Oc.GetPodNamespace() - ginkgo.By(fmt.Sprintf("test deployment in namespace %s", namespace)) - deployments, notReadyDeployments = getDeployments(namespace) - if len(deployments) == 0 { - return +//nolint:dupl +func testLiveness(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestLivenessIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + failedPods := []*configsections.Pod{} + ginkgo.By("Testing PUTs are configured with liveness lifecycle") + for _, podUnderTest := range env.PodsUnderTest { + ginkgo.By(fmt.Sprintf("should have liveness configured %s/%s", podUnderTest.Namespace, podUnderTest.Name)) + passed := livenessTest(podUnderTest.Namespace, podUnderTest.Name, env.GetLocalShellContext()) + if !passed { + failedPods = append(failedPods, podUnderTest) } - // We require that all deployments have the desired number of replicas and are all up to date - if len(notReadyDeployments) != 0 { - ginkgo.Skip("Can not test when deployments are not ready") - } - gomega.Expect(notReadyDeployments).To(gomega.BeEmpty()) - ginkgo.By("Should return map of nodes to deployments") - nodesSorted = getDeploymentsNodes(namespace) - ginkgo.By("should create new replicas when node is drained") - defer results.RecordResult(identifiers.TestPodRecreationIdentifier) - testedDeployments := map[string]bool{} - for _, n := range nodesSorted { - oldLen := len(testedDeployments) // this starts with zero - // mark tested deployments - for d := range n.deployments { - testedDeployments[d] = true - } - if oldLen == len(testedDeployments) { - // If node does not add new deployments then skip it - continue - } - // drain node - drainNode(n.name) // should go in this - // verify deployments are ready again - _, notReadyDeployments = getDeployments(namespace) - gomega.Expect(notReadyDeployments).To(gomega.BeEmpty()) // this is to make sure pods are created again - uncordonNode(n.name) - if len(testedDeployments) == len(deployments) { - break - } + } + if n := len(failedPods); n > 0 { + log.Debugf("Pods without liveness: %+v", failedPods) + ginkgo.Fail(fmt.Sprintf("%d pods do not have liveness configured.", n)) + } + }) +} + +func livenessTest(podNamespace, podName string, context *interactive.Context) bool { + passed := true + values := make(map[string]interface{}) + values["POD_NAMESPACE"] = podNamespace + values["POD_NAME"] = podName + values["GO_TEMPLATE_PATH"] = relativeLivenessTestDirectoryPath + tester, handlers := utils.NewGenericTesterAndValidate(relativeLivenessTestPath, common.RelativeSchemaPath, values) + test, err := tnf.NewTest(context.GetExpecter(), *tester, handlers, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(test).ToNot(gomega.BeNil()) + + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("FAILURE: Pod %s/%s does not have liveness defined", podNamespace, podName) + passed = false + }, func(err error) { + tnf.ClaimFilePrintf("ERROR: Pod %s/%s, error: %v", podNamespace, podName, err) + passed = false + }) + return passed +} + +//nolint:dupl +func testReadiness(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestReadinessIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + failedPods := []*configsections.Pod{} + ginkgo.By("Testing PUTs are configured with readiness lifecycle") + for _, podUnderTest := range env.PodsUnderTest { + ginkgo.By(fmt.Sprintf("should have readiness configured %s/%s", podUnderTest.Namespace, podUnderTest.Name)) + passed := readinessTest(podUnderTest.Namespace, podUnderTest.Name, env.GetLocalShellContext()) + if !passed { + failedPods = append(failedPods, podUnderTest) } } + if n := len(failedPods); n > 0 { + log.Debugf("Pods without readiness: %+v", failedPods) + ginkgo.Fail(fmt.Sprintf("%d pods do not have readiness configured.", n)) + } }) } -type node struct { - name string - deployments map[string]bool +func readinessTest(podNamespace, podName string, context *interactive.Context) bool { + passed := true + values := make(map[string]interface{}) + values["POD_NAMESPACE"] = podNamespace + values["POD_NAME"] = podName + values["GO_TEMPLATE_PATH"] = relativeReadinessTestDirectoryPath + tester, handlers := utils.NewGenericTesterAndValidate(relativeReadinessTestPath, common.RelativeSchemaPath, values) + test, err := tnf.NewTest(context.GetExpecter(), *tester, handlers, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(test).ToNot(gomega.BeNil()) + + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("FAILURE: Pod %s/%s does not have readiness defined", podNamespace, podName) + passed = false + }, func(err error) { + tnf.ClaimFilePrintf("ERROR: Pod %s/%s, error: %v", podNamespace, podName, err) + passed = false + }) + return passed } -func sortNodesMap(nodesMap dn.NodesMap) []node { - nodes := make([]node, 0, len(nodesMap)) - for n, d := range nodesMap { - nodes = append(nodes, node{n, d}) +func cleanupNodeDrain(env *config.TestEnvironment, nodeName string) { + uncordonNode(nodeName, env.GetLocalShellContext()) + for _, ns := range env.NameSpacesUnderTest { + notReady := waitForAllPodSetsReady(ns, postNodeDrainRecoveryTimeOut, scalingPollingPeriod, configsections.Deployment, env.GetLocalShellContext()) + if notReady != 0 { + collectNodeAndPendingPodInfo(ns, env.GetLocalShellContext()) + ginkgo.AbortSuite(fmt.Sprintf("Cleanup after node drain for %s failed, stopping tests to ensure cluster integrity", nodeName)) + } + notReadyStateFulSets := waitForAllPodSetsReady(ns, postNodeDrainRecoveryTimeOut, scalingPollingPeriod, configsections.StateFulSet, env.GetLocalShellContext()) + if notReadyStateFulSets != 0 { + collectNodeAndPendingPodInfo(ns, env.GetLocalShellContext()) + ginkgo.AbortSuite(fmt.Sprintf("Cleanup after node drain for %s failed, stopping tests to ensure cluster integrity", nodeName)) + } } - sort.Slice(nodes, func(i, j int) bool { return len(nodes[i].deployments) > len(nodes[j].deployments) }) - return nodes } -func getDeploymentsNodes(namespace string) []node { - context := common.GetContext() - tester := dn.NewDeploymentsNodes(common.DefaultTimeout, namespace) - test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - common.RunAndValidateTest(test) - nodes := tester.GetNodes() - gomega.Expect(nodes).NotTo(gomega.BeEmpty()) - return sortNodesMap(nodes) +func testNodeDrain(env *config.TestEnvironment, nodeName string) { + ginkgo.By(fmt.Sprintf("Testing node drain for %s\n", nodeName)) + // Ensure the node is uncordoned before exiting the function, + // and all podsets(deployments/statefulset) are ready + defer cleanupNodeDrain(env, nodeName) + + // drain node + if err := drainNode(nodeName, env.GetLocalShellContext()); err != nil { + ginkgo.Fail(fmt.Sprintf("Draining node %s failed: %s", nodeName, err)) + } + + for _, ns := range env.NameSpacesUnderTest { + notReadyDeployments := waitForAllPodSetsReady(ns, postNodeDrainRecoveryTimeOut, scalingPollingPeriod, configsections.Deployment, env.GetLocalShellContext()) + if notReadyDeployments != 0 { + collectNodeAndPendingPodInfo(ns, env.GetLocalShellContext()) + ginkgo.Fail(fmt.Sprintf("Failed to recover deployments on namespace %s after draining node %s.", ns, nodeName)) + } + notReadyStateFulSets := waitForAllPodSetsReady(ns, postNodeDrainRecoveryTimeOut, scalingPollingPeriod, configsections.StateFulSet, env.GetLocalShellContext()) + if notReadyStateFulSets != 0 { + collectNodeAndPendingPodInfo(ns, env.GetLocalShellContext()) + ginkgo.Fail(fmt.Sprintf("Failed to recover statefulsets on namespace %s after draining node %s.", ns, nodeName)) + } + } + // If we got this far, all deployments/statefulsets are ready after draining the node + tnf.ClaimFilePrintf("Node drain for %s succeeded", nodeName) } -// getDeployments returns map of deployments and names of not-ready deployments -func getDeployments(namespace string) (deployments dp.DeploymentMap, notReadyDeployments []string) { - context := common.GetContext() - tester := dp.NewDeployments(common.DefaultTimeout, namespace) +func testPodsRecreation(env *config.TestEnvironment) { + deployments := make(ps.PodSetMap) + var notReadyDeployments []string + statefulsets := make(ps.PodSetMap) + var notReadyStatefulsets []string + + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestPodRecreationIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + ginkgo.By("Testing node draining effect of deployment") + ginkgo.By(fmt.Sprintf("test deployment in namespace %s", env.NameSpacesUnderTest)) + for _, ns := range env.NameSpacesUnderTest { + var dps ps.PodSetMap + var sfs ps.PodSetMap + dps, notReadyDeployments = GetPodSets(ns, configsections.Deployment, env.GetLocalShellContext()) + for dpKey, dp := range dps { + deployments[dpKey] = dp + } + sfs, notReadyStatefulsets = GetPodSets(ns, configsections.StateFulSet, env.GetLocalShellContext()) + for sfKey, sf := range sfs { + statefulsets[sfKey] = sf + } + // We require that all deployments/statefulset have the desired number of replicas and are all up to date + if len(notReadyDeployments) != 0 && len(notReadyStatefulsets) != 0 { + ginkgo.Skip("Can not test when podsets are not ready") + } + } + if len(deployments) == 0 && len(statefulsets) == 0 { + ginkgo.Skip("no valid deployment or statefulset") + } + defer env.SetNeedsRefresh() + ginkgo.By("should create new replicas when node is drained") + // We need to delete all Oc sessions because the drain operation is often deleting oauth-openshift pod + // This results in lost connectivity for oc sessions + env.ResetOc() + for _, n := range env.NodesUnderTest { + if !n.HasPodset() { + log.Debug("node ", n.Name, " has no podset, skip draining") + continue + } + testNodeDrain(env, n.Name) + } + }) +} + +// GetPodSets returns map of podsets(deployments/statefulset) and names of not-ready podsets +func GetPodSets(namespace string, resourceType configsections.PodSetType, context *interactive.Context) (podsets ps.PodSetMap, notReadypodsets []string) { + tester := ps.NewPodSets(common.DefaultTimeout, namespace, string(resourceType)) test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) - common.RunAndValidateTest(test) + test.RunAndValidate() - deployments = tester.GetDeployments() - - for name, d := range deployments { - if d.Unavailable != 0 || d.Ready != d.Replicas || d.Available != d.Replicas || d.UpToDate != d.Replicas { - notReadyDeployments = append(notReadyDeployments, name) + podsets = tester.GetPodSets() + for name, d := range podsets { + if d.Unavailable != 0 || d.Ready != d.Replicas || (d.Available != d.Replicas && d.Current != d.Replicas) || d.UpToDate != d.Replicas { + notReadypodsets = append(notReadypodsets, name) + log.Tracef("%s %s: not ready", string(resourceType), name) + } else { + log.Tracef("%s %s: ready", string(resourceType), name) } } - return deployments, notReadyDeployments + return podsets, notReadypodsets } -func drainNode(node string) { - context := common.GetContext() - tester := dd.NewDeploymentsDrain(drainTimeout, node) +func collectNodeAndPendingPodInfo(ns string, context *interactive.Context) { + nodeStatus, _ := utils.ExecuteCommand("oc get nodes -o json | jq '.items[]|{name:.metadata.name, taints:.spec.taints}'", common.DefaultTimeout, context) + common.TcClaimLogPrintf("Namespace: %s\nNode status:\n%s", ns, nodeStatus) + + cmd := fmt.Sprintf("oc get pods -n %s --field-selector=status.phase!=Running,status.phase!=Succeeded -o json | jq '.items[]|{name:.metadata.name, status:.status}'", ns) + podStatus, _ := utils.ExecuteCommand(cmd, common.DefaultTimeout, context) + common.TcClaimLogPrintf("Pending Pods:\n%s", podStatus) + + cmd = fmt.Sprintf("oc get events -n %s --field-selector type!=Normal -o json --sort-by='.lastTimestamp' | jq '.items[]|{object:.involvedObject, reason:.reason, type:.type, message:.message, lastSeen:.lastTimestamp}'", ns) + events, _ := utils.ExecuteCommand(cmd, common.DefaultTimeout, context) + common.TcClaimLogPrintf("Events:\n%s", events) +} + +// getNumPodsDeployedOnNode is a helper function that returns the number of all pods +// deployed in a given node. +func getNumPodsDeployedOnNode(nodeName string, context *interactive.Context) (int, error) { + const cmdFmt = "oc get pods --all-namespaces -o wide --field-selector spec.nodeName=%s -l pod-template-hash" + cmd := fmt.Sprintf(cmdFmt, nodeName) + out, err := utils.ExecuteCommand(cmd, common.DefaultTimeout, context) + if err != nil { + return 0, fmt.Errorf("failed to get a pod list of pods deployed on the node: %s", err) + } + + // The ouptut should be a table, should we expect at list one line for the columns description. + numPodsDeployed := len(strings.Split(out, "\n")) + if numPodsDeployed == 0 { + return 0, fmt.Errorf("empty output from cmd: %q", cmd) + } + + return numPodsDeployed - 1, nil +} + +func drainNode(node string, context *interactive.Context) error { + // Before draining, we'll get the number of pods currently deployed on it. + numPodsDeployed, err := getNumPodsDeployedOnNode(node, context) + if err != nil { + return fmt.Errorf("failed to get number of pods deployed in the node: %s", err) + } + + // We'll add one minute per pod to the base timeout for the node drain operation. + nodeDrainTimeout := baseNodeDrainTimeout + (time.Duration(numPodsDeployed) * time.Minute) + + // Make sure the calculated timeout won't exceed the allowed maximum. + if nodeDrainTimeout > maxNodeDrainTimeout { + nodeDrainTimeout = maxNodeDrainTimeout + } + + log.Infof("Dynamic timeout for draining node %s: %s (pods deployed: %d)", node, nodeDrainTimeout, numPodsDeployed) + + tester := dd.NewDeploymentsDrain(nodeDrainTimeout, node) test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) - common.RunAndValidateTest(test) + + startTime := time.Now() + result, err := test.Run() + if err != nil { + return err + } + + elapsedTime := time.Since(startTime) + log.Infof("Draining node %s took %s.", node, elapsedTime) + + if result != tnf.SUCCESS { + return fmt.Errorf("tester returned result code %d", result) + } + + return nil } -func uncordonNode(node string) { - context := common.GetContext() +func uncordonNode(node string, context *interactive.Context) { values := make(map[string]interface{}) values["NODE"] = node - test, handlers, result, err := generic.NewGenericFromMap(relativeNodesTestPath, common.RelativeSchemaPath, values) + tester, handlers := utils.NewGenericTesterAndValidate(relativeNodesTestPath, common.RelativeSchemaPath, values) + test, err := tnf.NewTest(context.GetExpecter(), *tester, handlers, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(result).ToNot(gomega.BeNil()) - gomega.Expect(result.Valid()).To(gomega.BeTrue()) - gomega.Expect(handlers).ToNot(gomega.BeNil()) - gomega.Expect(len(handlers)).To(gomega.Equal(1)) gomega.Expect(test).ToNot(gomega.BeNil()) - tester, err := tnf.NewTest(context.GetExpecter(), *test, handlers, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(tester).ToNot(gomega.BeNil()) - - testResult, err := tester.Run() - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("FAILURE: unable to uncordon node %s", node) + ginkgo.AbortSuite(fmt.Sprintf("Failed to uncordon node %s, stopping tests to ensure cluster integrity", node)) + }, func(err error) { + tnf.ClaimFilePrintf("ERROR: unable to uncordon node %s: %s", node, err) + ginkgo.AbortSuite(fmt.Sprintf("Failed to uncordon node %s, stopping tests to ensure cluster integrity", node)) + }) } // Pod antiaffinity test for all deployments -func testPodAntiAffinity(configData *common.ConfigurationData) { - var deployments dp.DeploymentMap +func testPodAntiAffinity(env *config.TestEnvironment) { ginkgo.When("CNF is designed in high availability mode ", func() { - ginkgo.It("Should set pod replica number greater than 1 and corresponding pod anti-affinity rules in deployment", func() { - for _, cut := range configData.ContainersUnderTest { - podNamespace := cut.Oc.GetPodNamespace() - defer results.RecordResult(identifiers.TestPodHighAvailabilityBestPractices) - deployments, _ = getDeployments(podNamespace) - if len(deployments) == 0 { - return - } - for name, d := range deployments { - if name != partnerPod { - podAntiAffinity(name, podNamespace, d.Replicas) - } + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestPodHighAvailabilityBestPractices) + ginkgo.It(testID, ginkgo.Label(testID), func() { + ginkgo.By("Should set pod replica number greater than 1 and corresponding pod anti-affinity rules in deployment") + if len(env.DeploymentsUnderTest) == 0 { + ginkgo.Skip("No test deployments found.") + } + + badDeployments := []configsections.PodSet{} + for _, deployment := range env.DeploymentsUnderTest { + ginkgo.By(fmt.Sprintf("Testing Pod AntiAffinity on Deployment=%s, Replicas=%d (ns=%s)", + deployment.Name, deployment.Replicas, deployment.Namespace)) + if !podAntiAffinity(deployment.Name, deployment.Namespace, deployment.Replicas, env.GetLocalShellContext()) { + badDeployments = append(badDeployments, deployment) } } + + if n := len(badDeployments); n > 0 { + log.Debugf("Deployments without a valid podAntiAffinity rule: %+v", badDeployments) + ginkgo.Fail(fmt.Sprintf("%d deployments failed the test for replicaCount > 1 and podAntiAffinity rule.", n)) + } }) }) } // check pod antiaffinity definition for a deployment -func podAntiAffinity(deployment, podNamespace string, replica int) { - context := common.GetContext() +func podAntiAffinity(deployment, podNamespace string, replica int, context *interactive.Context) bool { values := make(map[string]interface{}) values["DEPLOYMENT_NAME"] = deployment values["DEPLOYMENT_NAMESPACE"] = podNamespace - infoWriter := tnf.CreateTestExtraInfoWriter() - test, handlers, result, err := generic.NewGenericFromMap(relativePodTestPath, common.RelativeSchemaPath, values) + tester, handlers := utils.NewGenericTesterAndValidate(relativePodTestPath, common.RelativeSchemaPath, values) + test, err := tnf.NewTest(context.GetExpecter(), *tester, handlers, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(result).ToNot(gomega.BeNil()) - gomega.Expect(result.Valid()).To(gomega.BeTrue()) - gomega.Expect(handlers).ToNot(gomega.BeNil()) - gomega.Expect(len(handlers)).To(gomega.Equal(1)) gomega.Expect(test).ToNot(gomega.BeNil()) - tester, err := tnf.NewTest(context.GetExpecter(), *test, handlers, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(tester).ToNot(gomega.BeNil()) - testResult, err := tester.Run() - if testResult != tnf.SUCCESS { + result := true + test.RunWithCallbacks(nil, func() { + result = false if replica > 1 { - msg := fmt.Sprintf("The deployment replica count is %d, but a podAntiAffinity rule is not defined, "+ + tnf.ClaimFilePrintf("FAILURE: The deployment replica count is %d, but a podAntiAffinity rule is not defined, "+ "you might want to change it in deployment %s in namespace %s", replica, deployment, podNamespace) - log.Warn(msg) - infoWriter(msg) } else { - msg := fmt.Sprintf("The deployment replica count is %d. Pod replica should be > 1 with an "+ + tnf.ClaimFilePrintf("FAILURE: The deployment replica count is %d. Pod replica should be > 1 with an "+ "podAntiAffinity rule defined . You might want to change it in deployment %s in namespace %s", replica, deployment, podNamespace) - log.Warn(msg) - infoWriter(msg) } - } - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) + }, func(err error) { + result = false + tnf.ClaimFilePrintf("ERROR: Failed to get replica count and podAntiAffinity for deployment %s (ns %s). Error: %v", err) + }) + + return result } -func testOwner(configData *common.ConfigurationData) { - ginkgo.When("Testing owners of CNF pod", func() { - ginkgo.It("Should be only ReplicaSet", func() { - for _, cut := range configData.ContainersUnderTest { - podNamespace := cut.Oc.GetPodNamespace() - podName := cut.Oc.GetPodName() - ginkgo.By(fmt.Sprintf("Should be ReplicaSet %s %s", podNamespace, podName)) - defer results.RecordResult(identifiers.TestPodDeploymentBestPracticesIdentifier) - context := common.GetContext() - tester := owners.NewOwners(common.DefaultTimeout, podNamespace, podName) - test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - testResult, err := test.Run() - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) +func testOwner(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestPodDeploymentBestPracticesIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + ginkgo.By("Testing owners of CNF pod, should be replicas Set") + context := env.GetLocalShellContext() + failedPods := []*configsections.Pod{} + for _, podUnderTest := range env.PodsUnderTest { + ginkgo.By(fmt.Sprintf("Should be ReplicaSet %s %s", podUnderTest.Namespace, podUnderTest.Name)) + tester := owners.NewOwners(common.DefaultTimeout, podUnderTest.Namespace, podUnderTest.Name) + test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("FAILURE: Pod %s/%s is not owned by a replica set", podUnderTest.Namespace, podUnderTest.Name) + failedPods = append(failedPods, podUnderTest) + }, func(err error) { + tnf.ClaimFilePrintf("ERROR: Pod %s/%s, error: %v", podUnderTest.Namespace, podUnderTest.Name, err) + failedPods = append(failedPods, podUnderTest) + }) + } + if n := len(failedPods); n > 0 { + log.Debugf("Pods not owned by a replica set: %+v", failedPods) + ginkgo.Fail(fmt.Sprintf("%d pods are not owned by a replica set.", n)) + } + }) +} + +func testImagePolicy(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestImagePullPolicyIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + context := env.GetLocalShellContext() + failedPods := []*configsections.Pod{} + for _, podUnderTest := range env.PodsUnderTest { + values := make(map[string]interface{}) + values["POD_NAMESPACE"] = podUnderTest.Namespace + values["POD_NAME"] = podUnderTest.Name + for i := 0; i < podUnderTest.ContainerCount; i++ { + values["CONTAINER_NUM"] = i + tester, handlers := utils.NewGenericTesterAndValidate(relativeimagepullpolicyTestPath, common.RelativeSchemaPath, values) + test, err := tnf.NewTest(context.GetExpecter(), *tester, handlers, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(test).ToNot(gomega.BeNil()) + + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("FAILURE: Pod %s/%s does not set imagePullPolicy to IfNotPresent", podUnderTest.Namespace, podUnderTest.Name) + failedPods = append(failedPods, podUnderTest) + }, func(err error) { + tnf.ClaimFilePrintf("ERROR: Pod %s/%s, error: %v", podUnderTest.Namespace, podUnderTest.Name, err) + failedPods = append(failedPods, podUnderTest) + }) } - }) + } + if n := len(failedPods); n > 0 { + log.Debugf("Pods with incorrect image pull policy: %+v", failedPods) + ginkgo.Fail(fmt.Sprintf("%d pods have incorrect image pull policy.", n)) + } }) } diff --git a/test-network-function/networking/doc.go b/test-network-function/networking/doc.go index 6a9f173d0..daab51ff8 100644 --- a/test-network-function/networking/doc.go +++ b/test-network-function/networking/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/test-network-function/networking/suite.go b/test-network-function/networking/suite.go index 4f81e04b3..a80d36eed 100644 --- a/test-network-function/networking/suite.go +++ b/test-network-function/networking/suite.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,109 +17,510 @@ package networking import ( + "encoding/json" "fmt" + "net" + "strconv" + "strings" + "time" + "github.com/test-network-function/test-network-function/pkg/config" "github.com/test-network-function/test-network-function/pkg/tnf/testcases" "github.com/test-network-function/test-network-function/test-network-function/common" "github.com/test-network-function/test-network-function/test-network-function/identifiers" - "github.com/test-network-function/test-network-function/test-network-function/results" - "github.com/onsi/ginkgo" - ginkgoconfig "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" log "github.com/sirupsen/logrus" + "github.com/test-network-function/test-network-function/pkg/config/configsections" "github.com/test-network-function/test-network-function/pkg/tnf" "github.com/test-network-function/test-network-function/pkg/tnf/handlers/nodeport" "github.com/test-network-function/test-network-function/pkg/tnf/handlers/ping" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/podnodename" "github.com/test-network-function/test-network-function/pkg/tnf/interactive" "github.com/test-network-function/test-network-function/pkg/tnf/reel" + "github.com/test-network-function/test-network-function/pkg/utils" + "github.com/test-network-function/test-network-function/test-network-function/results" +) + +const ( + commandportdeclared = "oc get pod %s -n %s -o json | jq -r '.spec.containers[%d].ports'" + commandportlisten = "ss -tulwnH" + ocCommandTimeOut = time.Second * 10 + indexprotocolname = 0 + indexport = 4 + defaultNumPings = 5 ) +type ipVersion string + const ( - defaultNumPings = 5 + IPv4 ipVersion = "IPv4" + IPv6 ipVersion = "IPv6" ) +type key struct { + port int + protocol string +} + +type Port []struct { + ContainerPort int `json:"containerPort"` + Name string `json:"name"` + Protocol string `json:"protocol"` +} + +// netTestContext this is a data structure describing a network test context for a given subnet (e.g. network attachment) +// The test context defines a tester or test initiator, that is initiating the pings. It is selected randomly (first container in the list) +// It also defines a list of destination ping targets corresponding to the other containers IPs on this subnet +type netTestContext struct { + // testerContainerNodeOc session context to access the node running the container selected to initiate tests + testerContainerNodeOc *interactive.Oc + // testerSource is the container select to initiate the ping tests on this given network + testerSource containerIP + // ipDestTargets List of containers to be pinged by the testerSource on this given network + destTargets []containerIP +} + +// containerIP holds a container identification and its IP for networking tests. +type containerIP struct { + // ip address of the target container + ip string + // targetContainerIdentifier container identifier including namespace, pod name, container name, node name, and container UID + containerIdentifier *configsections.ContainerIdentifier +} + +func (testContext netTestContext) String() string { + output := fmt.Sprintf("From initiating container: %s\n", testContext.testerSource.String()) + if len(testContext.destTargets) == 0 { + output = "--> No target containers to test for this network" //nolint:goconst // this is only one time + } + for _, target := range testContext.destTargets { + output += fmt.Sprintf("--> To target container: %s\n", target.String()) + } + return output +} + +func (cip *containerIP) String() string { + return fmt.Sprintf("%s ( %s )", + cip.ip, + cip.containerIdentifier.String(), + ) +} + +func printNetTestContextMap(netsUnderTest map[string]netTestContext) string { + var output string + if len(netsUnderTest) == 0 { + output = "No networks to test.\n" //nolint:goconst // this is only one time + } + for netName, netUnderTest := range netsUnderTest { + output += fmt.Sprintf("***Test for Network attachment: %s\n", netName) + output += fmt.Sprintf("%s\n", netUnderTest.String()) + } + return output +} + // // All actual test code belongs below here. Utilities belong above. // // Runs the "generic" CNF test cases. var _ = ginkgo.Describe(common.NetworkingTestKey, func() { - if testcases.IsInFocus(ginkgoconfig.GinkgoConfig.FocusStrings, common.NetworkingTestKey) { - config := common.GetTestConfiguration() - log.Infof("Test Configuration: %s", config) + conf, _ := ginkgo.GinkgoConfiguration() + if testcases.IsInFocus(conf.FocusStrings, common.NetworkingTestKey) { + env := config.GetTestEnvironment() + ginkgo.BeforeEach(func() { + env.LoadAndRefresh() + gomega.Expect(len(env.PodsUnderTest)).ToNot(gomega.Equal(0)) + gomega.Expect(len(env.ContainersUnderTest)).ToNot(gomega.Equal(0)) + }) - for _, cid := range config.ExcludeContainersFromConnectivityTests { - common.ContainersToExcludeFromConnectivityTests[cid] = "" - } - containersUnderTest := common.CreateContainersUnderTest(config) - partnerContainers := common.CreatePartnerContainers(config) - testOrchestrator := partnerContainers[config.TestOrchestrator] - log.Info(testOrchestrator) - log.Info(containersUnderTest) + ginkgo.ReportAfterEach(results.RecordResult) + ginkgo.AfterEach(env.CloseLocalShellContext) ginkgo.Context("Both Pods are on the Default network", func() { - // for each container under test, ensure bidirectional ICMP traffic between the container and the orchestrator. - for _, containerUnderTest := range containersUnderTest { - if _, ok := common.ContainersToExcludeFromConnectivityTests[containerUnderTest.ContainerIdentifier]; !ok { - testNetworkConnectivity(containerUnderTest.Oc, testOrchestrator.Oc, testOrchestrator.DefaultNetworkIPAddress, defaultNumPings) - testNetworkConnectivity(testOrchestrator.Oc, containerUnderTest.Oc, containerUnderTest.DefaultNetworkIPAddress, defaultNumPings) - } - } + testDefaultNetworkConnectivity(env, defaultNumPings, IPv4) + testDefaultNetworkConnectivity(env, defaultNumPings, IPv6) }) ginkgo.Context("Both Pods are connected via a Multus Overlay Network", func() { - // Unidirectional test; for each container under test, attempt to ping the target Multus IP addresses. - for _, containerUnderTest := range containersUnderTest { - for _, multusIPAddress := range containerUnderTest.ContainerConfiguration.MultusIPAddresses { - testNetworkConnectivity(testOrchestrator.Oc, containerUnderTest.Oc, multusIPAddress, defaultNumPings) - } - } + testMultusNetworkConnectivity(env, defaultNumPings, IPv4) + testMultusNetworkConnectivity(env, defaultNumPings, IPv6) + }) + ginkgo.Context("Should not have type of nodePort", func() { + testNodePort(env) }) + ginkgo.Context("Should not have type of listen port and declared port", func() { + testListenAndDeclared(env) + }) + } +}) - for _, containerUnderTest := range containersUnderTest { - testNodePort(containerUnderTest.Oc.GetPodNamespace()) +// processContainerIpsPerNet takes a container ip addresses for a given network attachment's and uses it as a test target. +// The first container in the loop is selected as the test initiator. the Oc context of the container is used to initiate the pings +func processContainerIpsPerNet(containerID *configsections.ContainerIdentifier, + netKey string, + ipAddresses []string, + netsUnderTest map[string]netTestContext, + containerNodeOc *interactive.Oc, + aIPVersion ipVersion) { + ipAddressesFiltered := FilterIPListPerVersion(ipAddresses, aIPVersion) + if len(ipAddressesFiltered) == 0 { + // if no multus addresses found, skip this container + tnf.ClaimFilePrintf("Skipping container %s, Network %s because no multus IPs are present", containerID.PodName, netKey) + return + } + // Create an entry at "key" if it is not present + if _, ok := netsUnderTest[netKey]; !ok { + netsUnderTest[netKey] = netTestContext{} + } + // get a copy of the content + entry := netsUnderTest[netKey] + // Then modify the copy + firstIPIndex := 0 + if entry.testerContainerNodeOc == nil { + tnf.ClaimFilePrintf("Pod %s, container %s selected to initiate ping tests", containerID.PodName, containerID.ContainerName) + entry.testerSource.containerIdentifier = containerID + entry.testerContainerNodeOc = containerNodeOc + // if multiple interfaces are present for this network on this container/pod, pick the first one as the tester source ip + entry.testerSource.ip = ipAddressesFiltered[firstIPIndex] + // do no include tester's IP in the list of destination IPs to ping + firstIPIndex++ + } + + for _, aIP := range ipAddressesFiltered[firstIPIndex:] { + ipDestEntry := containerIP{} + ipDestEntry.containerIdentifier = containerID + ipDestEntry.ip = aIP + entry.destTargets = append(entry.destTargets, ipDestEntry) + } + + // Then reassign map entry + netsUnderTest[netKey] = entry +} + +func FilterIPListPerVersion(ipList []string, aIPVersion ipVersion) []string { + var filteredIPList []string + for _, aIP := range ipList { + if ver, _ := getIPVersion(aIP); aIPVersion == ver { + filteredIPList = append(filteredIPList, aIP) } + } + return filteredIPList +} +func getIPVersion(aIP string) (ipVersion, error) { + ip := net.ParseIP(aIP) + if ip == nil { + return "", fmt.Errorf("%s is Not an IPv4 or an IPv6", aIP) } -}) + if ip.To4() != nil { + return IPv4, nil + } + return IPv6, nil +} + +// runNetworkingTests takes a map netTestContext, e.g. one context per network attachment +// and runs pings test with it. Returns a network name to a slice of bad target IPs map. +func runNetworkingTests(netsUnderTest map[string]netTestContext, count int, aIPVersion ipVersion) map[string][]string { + tnf.ClaimFilePrintf("%s", printNetTestContextMap(netsUnderTest)) + log.Debugf("%s", printNetTestContextMap(netsUnderTest)) + if len(netsUnderTest) == 0 { + ginkgo.Skip(fmt.Sprintf("There are no %s networks to test, skipping test", aIPVersion)) + } + // maps a net name to a list of failed destination IPs + badNets := map[string][]string{} + + // if no network can be tested, then we need to skip the test entirely. + // If at least one network can be tested (e.g. > 2 IPs/ interfaces present), then we do not skip the test + atLeastOneNetworkTested := false + for netName, netUnderTest := range netsUnderTest { + if len(netUnderTest.destTargets) == 0 { + log.Warnf("There are no containers to ping for %s network %s. A minimum of 2 containers is needed to run a ping test (a source and a destination) Skipping test", aIPVersion, netName) + tnf.ClaimFilePrintf("There are no containers to ping for %s network %s. Skip testing this network", aIPVersion, netName) + continue + } + atLeastOneNetworkTested = true + ginkgo.By(fmt.Sprintf("%s Ping tests on network %s. Number of target IPs: %d", aIPVersion, netName, len(netUnderTest.destTargets))) + for _, aDestIP := range netUnderTest.destTargets { + ginkgo.By(fmt.Sprintf("a %s Ping is issued from %s(%s) %s to %s(%s) %s", + aIPVersion, + netUnderTest.testerSource.containerIdentifier.PodName, + netUnderTest.testerSource.containerIdentifier.ContainerName, + netUnderTest.testerSource.ip, aDestIP.containerIdentifier.PodName, + aDestIP.containerIdentifier.ContainerName, + aDestIP.ip)) + testPass := testPing(netUnderTest.testerContainerNodeOc, netUnderTest.testerSource.containerIdentifier, aDestIP, count) + if !testPass { + if failedDestIps, netFound := badNets[netName]; netFound { + badNets[netName] = append(failedDestIps, aDestIP.ip) + } else { + badNets[netName] = []string{aDestIP.ip} + } + } + } + } + if !atLeastOneNetworkTested { + ginkgo.Skip(fmt.Sprintf("There are no network to test for any %s networks, skipping test", aIPVersion)) + } + return badNets +} +func testDefaultNetworkConnectivity(env *config.TestEnvironment, count int, aIPVersion ipVersion) { + ginkgo.When("Testing Default network connectivity", func() { + identifier := identifiers.TestICMPv4ConnectivityIdentifier + if aIPVersion == IPv6 { + identifier = identifiers.TestICMPv6ConnectivityIdentifier + } + testID := identifiers.XformToGinkgoItIdentifier(identifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + netsUnderTest := make(map[string]netTestContext) + for _, pod := range env.PodsUnderTest { + // The first container is used to get the network namespace + aContainerInPod := pod.ContainerList[0] + if _, ok := env.ContainersToExcludeFromConnectivityTests[aContainerInPod.ContainerIdentifier]; ok { + tnf.ClaimFilePrintf("Skipping pod %s because it is excluded from all connectivity tests", pod.Name) + continue + } + netKey := "default" //nolint:goconst // only used once + defaultIPAddress := pod.DefaultNetworkIPAddresses + gomega.Expect(env).To(gomega.Not(gomega.BeNil())) + gomega.Expect(env.NodesUnderTest[aContainerInPod.NodeName]).To(gomega.Not(gomega.BeNil())) + gomega.Expect(env.NodesUnderTest[aContainerInPod.NodeName].DebugContainer.GetOc()).To(gomega.Not(gomega.BeNil())) + nodeOc := env.NodesUnderTest[aContainerInPod.NodeName].DebugContainer.GetOc() + processContainerIpsPerNet(&aContainerInPod.ContainerIdentifier, netKey, defaultIPAddress, netsUnderTest, nodeOc, aIPVersion) + } + badNets := runNetworkingTests(netsUnderTest, count, aIPVersion) -// Helper to test that a container can ping a target IP address, and report through Ginkgo. -func testNetworkConnectivity(initiatingPodOc, targetPodOc *interactive.Oc, targetPodIPAddress string, count int) { - ginkgo.When(fmt.Sprintf("a Ping is issued from %s(%s) to %s(%s) %s", initiatingPodOc.GetPodName(), - initiatingPodOc.GetPodContainerName(), targetPodOc.GetPodName(), targetPodOc.GetPodContainerName(), - targetPodIPAddress), func() { - ginkgo.It(fmt.Sprintf("%s(%s) should reply", targetPodOc.GetPodName(), targetPodOc.GetPodContainerName()), func() { - defer results.RecordResult(identifiers.TestICMPv4ConnectivityIdentifier) - testPing(initiatingPodOc, targetPodIPAddress, count) + if n := len(badNets); n > 0 { + log.Warnf("Failed nets: %+v", badNets) + ginkgo.Fail(fmt.Sprintf("%d nets failed the default network %s ping test.", n, aIPVersion)) + } + }) + }) +} +func testMultusNetworkConnectivity(env *config.TestEnvironment, count int, aIPVersion ipVersion) { + identifier := identifiers.TestICMPv4ConnectivityMultusIdentifier + if aIPVersion == IPv6 { + identifier = identifiers.TestICMPv6ConnectivityMultusIdentifier + } + ginkgo.When("Testing Multus network connectivity", func() { + testID := identifiers.XformToGinkgoItIdentifier(identifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + netsUnderTest := make(map[string]netTestContext) + for _, pod := range env.PodsUnderTest { + // The first container is used to get the network namespace + aContainerInPod := pod.ContainerList[0] + if _, ok := env.ContainersToExcludeFromConnectivityTests[aContainerInPod.ContainerIdentifier]; ok { + tnf.ClaimFilePrintf("Skipping pod %s because it is excluded from all connectivity tests", pod.Name) + continue + } + if _, ok := env.ContainersToExcludeFromMultusConnectivityTests[aContainerInPod.ContainerIdentifier]; ok { + tnf.ClaimFilePrintf("Skipping pod %s because it is excluded from multus connectivity tests only", pod.Name) + continue + } + for netKey, multusIPAddress := range pod.MultusIPAddressesPerNet { + gomega.Expect(env).To(gomega.Not(gomega.BeNil())) + gomega.Expect(env.NodesUnderTest[aContainerInPod.NodeName]).To(gomega.Not(gomega.BeNil())) + gomega.Expect(env.NodesUnderTest[aContainerInPod.NodeName].DebugContainer.GetOc()).To(gomega.Not(gomega.BeNil())) + nodeOc := env.NodesUnderTest[aContainerInPod.NodeName].DebugContainer.GetOc() + processContainerIpsPerNet(&aContainerInPod.ContainerIdentifier, netKey, multusIPAddress, netsUnderTest, nodeOc, aIPVersion) + } + } + badNets := runNetworkingTests(netsUnderTest, count, aIPVersion) + + if n := len(badNets); n > 0 { + log.Warnf("Failed nets: %+v", badNets) + ginkgo.Fail(fmt.Sprintf("%d nets failed the multus %s ping test.", n, aIPVersion)) + } }) }) } // Test that a container can ping a target IP address. -func testPing(initiatingPodOc *interactive.Oc, targetPodIPAddress string, count int) { - log.Infof("Sending ICMP traffic(%s to %s)", initiatingPodOc.GetPodName(), targetPodIPAddress) - pingTester := ping.NewPing(common.DefaultTimeout, targetPodIPAddress, count) - test, err := tnf.NewTest(initiatingPodOc.GetExpecter(), pingTester, []reel.Handler{pingTester}, initiatingPodOc.GetErrorChannel()) +func testPing(initiatingPodNodeOc *interactive.Oc, sourceContainerID *configsections.ContainerIdentifier, targetContainerIP containerIP, count int) bool { + log.Infof("Sending ICMP traffic(%s to %s)", initiatingPodNodeOc.GetPodName(), targetContainerIP.ip) + env := config.GetTestEnvironment() + gomega.Expect(env).To(gomega.Not(gomega.BeNil())) + gomega.Expect(env.NodesUnderTest[sourceContainerID.NodeName]).To(gomega.Not(gomega.BeNil())) + gomega.Expect(env.NodesUnderTest[sourceContainerID.NodeName].DebugContainer.GetOc()).To(gomega.Not(gomega.BeNil())) + nodeOc := env.NodesUnderTest[sourceContainerID.NodeName].DebugContainer.GetOc() + containerPID := utils.GetContainerPID(sourceContainerID.NodeName, nodeOc, sourceContainerID.ContainerUID, sourceContainerID.ContainerRuntime) + pingTester := ping.NewPingNsenter(common.DefaultTimeout, containerPID, targetContainerIP.ip, count) + test, err := tnf.NewTest(initiatingPodNodeOc.GetExpecter(), pingTester, []reel.Handler{pingTester}, initiatingPodNodeOc.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) - common.RunAndValidateTest(test) - transmitted, received, errors := pingTester.GetStats() - gomega.Expect(received).To(gomega.Equal(transmitted)) - gomega.Expect(errors).To(gomega.BeZero()) -} - -func testNodePort(podNamespace string) { - ginkgo.When(fmt.Sprintf("Testing services in namespace %s", podNamespace), func() { - ginkgo.It("Should not have services of type NodePort", func() { - defer results.RecordResult(identifiers.TestServicesDoNotUseNodeportsIdentifier) - context := common.GetContext() - tester := nodeport.NewNodePort(common.DefaultTimeout, podNamespace) + + sourcePodName := initiatingPodNodeOc.GetPodName() + targetPodName := targetContainerIP.containerIdentifier.PodName + + testResult := false + test.RunWithCallbacks(func() { + transmitted, received, errors := pingTester.GetStats() + if received == transmitted && errors == 0 { + log.Infof("Ping test from pod %s to pod %s (ip %s) succeeded. Tx/Rx/Err: %d/%d/%d", + sourcePodName, targetPodName, targetContainerIP.ip, transmitted, received, errors) + testResult = true + } else { + tnf.ClaimFilePrintf("Ping test from pod %s to pod %s (ip: %s) failed. Tx/Rx/Err: %d/%d/%d", + sourcePodName, targetPodName, targetContainerIP.ip, transmitted, received, errors) + } + }, func() { + tnf.ClaimFilePrintf("FAILURE: Ping test from pod %s to pod %s (ip: %s) failed.", + sourcePodName, targetPodName, targetContainerIP.ip) + }, func(err error) { + tnf.ClaimFilePrintf("ERROR: Ping test from pod %s to pod %s (ip: %s) failed. Error: %v", + sourcePodName, targetPodName, targetContainerIP.ip, err) + if reel.IsTimeout(err) { + env.NodesUnderTest[sourceContainerID.NodeName].DebugContainer.CloseOc() + } + }) + + return testResult +} + +func testNodePort(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestServicesDoNotUseNodeportsIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + badNamespaces := []string{} + context := env.GetLocalShellContext() + for _, ns := range env.NameSpacesUnderTest { + ginkgo.By(fmt.Sprintf("Testing services in namespace %s", ns)) + tester := nodeport.NewNodePort(common.DefaultTimeout, ns) test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) - testResult, err := test.Run() - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("Namespace %s has one or more nodePort/s", ns) + badNamespaces = append(badNamespaces, ns) + }, func(err error) { + tnf.ClaimFilePrintf("nodePort test on namespace %s failed. Error: %v", ns, err) + badNamespaces = append(badNamespaces, ns) + }) + } + + if n := len(badNamespaces); n > 0 { + log.Warnf("Failed namespaces: %+v", badNamespaces) + ginkgo.Fail(fmt.Sprintf("%d namespaces have nodePort/s.", n)) + } + }) +} + +func parseVariables(res string, declaredPorts map[key]bool) error { + var p Port + err := json.Unmarshal([]byte(res), &p) + if err != nil { + return err + } + + for element := range p { + var k key + k.port = p[element].ContainerPort + k.protocol = p[element].Protocol + declaredPorts[k] = true + } + return nil +} +func declaredPortList(container int, podName, podNamespace string, declaredPorts map[key]bool) error { + ocCommandToExecute := fmt.Sprintf(commandportdeclared, podName, podNamespace, container) + res, err := utils.ExecuteCommand(ocCommandToExecute, ocCommandTimeOut, interactive.GetContext(false)) + if err != nil { + return err + } + err = parseVariables(res, declaredPorts) + return err +} + +func listeningPortList(commandlisten []string, nodeOc *interactive.Context, listeningPorts map[key]bool) error { + var k key + listeningPortCommand := strings.Join(commandlisten, " ") + res, err := utils.ExecuteCommand(listeningPortCommand, ocCommandTimeOut, nodeOc) + if err != nil { + return err + } + lines := strings.Split(res, "\n") + for _, line := range lines { + fields := strings.Fields(line) + if !strings.Contains(line, "LISTEN") { + continue + } + if indexprotocolname > len(fields) || indexport > len(fields) { + return err + } + s := strings.Split(fields[indexport], ":") + if len(s) == 0 { + log.Errorf("error decoding port number for line: %s", line) + continue + } + p, _ := strconv.Atoi(strings.ReplaceAll(s[len(s)-1], "\"", "")) + k.port = p + k.protocol = strings.ToUpper(fields[indexprotocolname]) + k.protocol = strings.ReplaceAll(k.protocol, "\"", "") + k.protocol = strings.ReplaceAll(k.protocol, ":", "") + listeningPorts[k] = true + } + return nil +} + +func checkIfListenIsDeclared(listeningPorts, declaredPorts map[key]bool) map[key]bool { + res := make(map[key]bool) + if len(listeningPorts) == 0 { + return res + } + for k := range listeningPorts { + _, ok := declaredPorts[k] + if !ok { + res[k] = listeningPorts[k] + } + } + return res +} + +func testListenAndDeclared(env *config.TestEnvironment) { + var skippedPods []configsections.Pod + var failedPods []configsections.Pod + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestUndeclaredContainerPortsUsage) + ginkgo.It(testID, ginkgo.Label(testID), func() { + OUTER: + for _, podUnderTest := range env.PodsUnderTest { + declaredPorts := make(map[key]bool) + listeningPorts := make(map[key]bool) + for i := 0; i < podUnderTest.ContainerCount; i++ { + err := declaredPortList(i, podUnderTest.Name, podUnderTest.Namespace, declaredPorts) + if err != nil { + tnf.ClaimFilePrintf("Failed to get declared port for container %d due to %v, skipping pod %s", i, err, podUnderTest.Name) + skippedPods = append(skippedPods, *podUnderTest) + continue OUTER + } + } + + nodeName := podnodename.NewPodNodeName(common.DefaultTimeout, podUnderTest.Name, podUnderTest.Namespace) + context := env.GetLocalShellContext() + test, err := tnf.NewTest(context.GetExpecter(), nodeName, []reel.Handler{nodeName}, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) - }) + test.RunAndValidate() + nodeOc := env.NodesUnderTest[nodeName.GetNodeName()].DebugContainer.GetOc() + x := podUnderTest.ContainerList[0] + containerPID := utils.GetContainerPID(nodeName.GetNodeName(), nodeOc, x.ContainerUID, x.ContainerRuntime) + + commandlisten := []string{utils.AddNsenterPrefix(containerPID), commandportlisten} + + err = listeningPortList(commandlisten, nodeOc.Context, listeningPorts) + if err != nil { + tnf.ClaimFilePrintf("Failed to get listening port for pod name %s in pod namespace %s due to %v, skipping this pod", podUnderTest.Name, podUnderTest.Namespace, err) + skippedPods = append(skippedPods, *podUnderTest) + continue + } + // compare between declaredPort,listeningPort + undeclaredPorts := checkIfListenIsDeclared(listeningPorts, declaredPorts) + for k := range undeclaredPorts { + tnf.ClaimFilePrintf("pod %s ns %s is listening on port %d protocol %s, but that port was not declared in any container spec.", podUnderTest.Name, podUnderTest.Namespace, k.port, k.protocol) + } + if len(undeclaredPorts) != 0 { + failedPods = append(failedPods, *podUnderTest) + } + } + + if nf, ns := len(failedPods), len(skippedPods); nf > 0 || ns > 0 { + ginkgo.Fail(fmt.Sprintf("Found %d pods with listening ports not declared and Skipped %d pods due to unexpected error", nf, ns)) + } }) } diff --git a/test-network-function/networking/suite_test.go b/test-network-function/networking/suite_test.go new file mode 100644 index 000000000..c5fb859c0 --- /dev/null +++ b/test-network-function/networking/suite_test.go @@ -0,0 +1,268 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package networking + +import ( + "os" + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" + "github.com/test-network-function/test-network-function/pkg/utils" +) + +func TestParseVariables(t *testing.T) { + // expected inputs + testCases := []struct { + // inputs + // inputRes is string that include the result after we run the command ""oc get pod %s -n %s -o json | jq -r '.spec.containers[%d].ports'"" + inputRes string + // expected outputs here + expectedDeclaredPorts map[key]bool + expectedRes string + }{ + { + inputRes: "[\n {\n \"containerPort\": 8080,\n \"name\": \"http-probe\",\n \"protocol\": \"TCP\"\n },{\n \"containerPort\": 7878,\n \"name\": \"http\",\n \"protocol\": \"TCP\"\n } \n]", + expectedDeclaredPorts: map[key]bool{{port: 8080, protocol: "TCP"}: true, {port: 7878, protocol: "TCP"}: true}, + expectedRes: "[\n {\n \"containerPort\": 8080,\n \"name\": \"http-probe\",\n \"protocol\": \"TCP\"\n },{\n \"containerPort\": 7878,\n \"name\": \"http\",\n \"protocol\": \"TCP\"\n } \n]", + }, + { + inputRes: "[\n {\n \"containerPort\": 8080,\n \"name\": \"http-probe\",\n \"protocol\": \"TCP\"\n }\n]", + expectedDeclaredPorts: map[key]bool{{port: 8080, protocol: "TCP"}: true}, + expectedRes: "[\n {\n \"containerPort\": 8080,\n \"name\": \"http-probe\",\n \"protocol\": \"TCP\"\n }\n]", + }, + { + inputRes: "[\n {\n \"containerPort\": 8080,\n \"name\": \"http-probe\",\n \"protocol\": \"UDP\"\n }\n]", + expectedDeclaredPorts: map[key]bool{{port: 8080, protocol: "UDP"}: true}, + expectedRes: "[\n {\n \"containerPort\": 8080,\n \"name\": \"http-probe\",\n \"protocol\": \"UDP\"\n }\n]", + }, + { + inputRes: "[\n \n]", + expectedDeclaredPorts: map[key]bool{}, + expectedRes: "[\n \n]", + }, + { + inputRes: "[\n {\n \"containerPort\": 9000,\n \"name\": \"http-probe\",\n \"protocol\": \"UDP\"\n }\n]", + expectedDeclaredPorts: map[key]bool{{port: 9000, protocol: "UDP"}: true}, + expectedRes: "[\n {\n \"containerPort\": 9000,\n \"name\": \"http-probe\",\n \"protocol\": \"UDP\"\n }\n]", + }, + } + + for _, tc := range testCases { + declaredPorts := map[key]bool{} + err := parseVariables(tc.inputRes, declaredPorts) + assert.Nil(t, err) + assert.Equal(t, tc.expectedDeclaredPorts, declaredPorts) + } +} + +func TestDeclaredPortList(t *testing.T) { + // expected inputs + testCases := []struct { + // inputs + jsonFileName string + container int + podName string + podNamespace string + declaredPorts map[key]bool + + // expected outputs here + expectedDeclaredPorts map[key]bool + }{ + { + jsonFileName: "testdata/test_ports.json", + container: 0, + podName: "test-54bc4c6d7-8rzch", + podNamespace: "tnf", + declaredPorts: map[key]bool{}, + expectedDeclaredPorts: map[key]bool{{port: 8080, protocol: "TCP"}: true, {port: 8443, protocol: "TCP"}: true, {port: 50051, protocol: "TCP"}: true}, + }, + } + + origFunc := utils.ExecuteCommand + defer func() { + utils.ExecuteCommand = origFunc + }() + for _, tc := range testCases { + utils.ExecuteCommand = func(command string, timeout time.Duration, context *interactive.Context) (string, error) { + output, err := os.ReadFile(tc.jsonFileName) + return string(output), err + } + err := declaredPortList(tc.container, tc.podName, tc.podNamespace, tc.declaredPorts) + assert.Nil(t, err) + assert.Equal(t, tc.expectedDeclaredPorts, tc.declaredPorts) + } +} + +func TestListeningPortList(t *testing.T) { + // expected inputs + testCases := []struct { + // inputs + jsonFileName string + commandlisten []string + nodeOc *interactive.Context + listeningPorts map[key]bool + + // expected outputs here + expectedlisteningPorts map[key]bool + }{ + { + jsonFileName: "testdata/test_listening_port.json", + commandlisten: []string{"nsenter -t 4380 -n", "ss -tulwnH"}, + nodeOc: nil, + listeningPorts: map[key]bool{}, + expectedlisteningPorts: map[key]bool{{port: 8080, protocol: "TCP"}: true, {port: 8443, protocol: "TCP"}: true, {port: 22, protocol: "TCP"}: true}, + }, + } + origFunc := utils.ExecuteCommand + defer func() { + utils.ExecuteCommand = origFunc + }() + for _, tc := range testCases { + utils.ExecuteCommand = func(command string, timeout time.Duration, context *interactive.Context) (string, error) { + output, err := os.ReadFile(tc.jsonFileName) + return string(output), err + } + err := listeningPortList(tc.commandlisten, tc.nodeOc, tc.listeningPorts) + assert.Nil(t, err) + assert.Equal(t, tc.listeningPorts, tc.expectedlisteningPorts) + } +} + +func TestCheckIfListenIsDeclared(t *testing.T) { + // expected inputs + testCases := []struct { + // inputs + listeningPorts map[key]bool + declaredPorts map[key]bool + + // expected outputs here + expectedres map[key]bool + }{ + { + listeningPorts: map[key]bool{}, + declaredPorts: map[key]bool{}, + expectedres: map[key]bool{}, + }, + { + listeningPorts: map[key]bool{{port: 8080, protocol: "TCP"}: true}, + declaredPorts: map[key]bool{{port: 8080, protocol: "TCP"}: true}, + expectedres: map[key]bool{}, + }, + + { + listeningPorts: map[key]bool{{port: 8080, protocol: "TCP"}: true}, + declaredPorts: map[key]bool{}, + expectedres: map[key]bool{{port: 8080, protocol: "TCP"}: true}, + }, + { + listeningPorts: map[key]bool{{port: 8080, protocol: "TCP"}: true, {port: 8443, protocol: "TCP"}: true}, + declaredPorts: map[key]bool{{port: 8080, protocol: "TCP"}: true}, + expectedres: map[key]bool{{port: 8443, protocol: "TCP"}: true}, + }, + { + listeningPorts: map[key]bool{}, + declaredPorts: map[key]bool{{port: 8080, protocol: "TCP"}: true}, + expectedres: map[key]bool{}, + }, + { + listeningPorts: map[key]bool{{port: 8080, protocol: "TCP"}: true, {port: 8443, protocol: "TCP"}: true}, + declaredPorts: map[key]bool{{port: 8080, protocol: "TCP"}: true, {port: 8443, protocol: "TCP"}: true}, + expectedres: map[key]bool{}, + }, + } + for _, tc := range testCases { + res := checkIfListenIsDeclared(tc.listeningPorts, tc.declaredPorts) + assert.Equal(t, res, tc.expectedres) + } +} + +func TestFilterIPListPerVersion(t *testing.T) { + type args struct { + ipList []string + ipVersion ipVersion + } + tests := []struct { + name string + args args + want []string + }{ + {name: "IPv4", + args: args{ipList: []string{"2.2.2.2", "3.3.3.3", "fd00:10:244:1::3", "fd00:10:244:1::4"}, ipVersion: IPv4}, + want: []string{"2.2.2.2", "3.3.3.3"}, + }, + {name: "IPv6", + args: args{ipList: []string{"2.2.2.2", "3.3.3.3", "fd00:10:244:1::3", "fd00:10:244:1::4"}, ipVersion: IPv6}, + want: []string{"fd00:10:244:1::3", "fd00:10:244:1::4"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := FilterIPListPerVersion(tt.args.ipList, tt.args.ipVersion); !reflect.DeepEqual(got, tt.want) { + t.Errorf("FilterIPListPerVersion() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getIPVersion(t *testing.T) { + type args struct { + aIP string + } + tests := []struct { + name string + args args + want ipVersion + wantErr bool + }{ + {name: "GoodIPv4", + args: args{aIP: "2.2.2.2"}, + want: IPv4, + wantErr: false, + }, + {name: "GoodIPv6", + args: args{aIP: "fd00:10:244:1::3"}, + want: IPv6, + wantErr: false, + }, + {name: "BadIPv4", + args: args{aIP: "2.hfh.2.2"}, + want: "", + wantErr: true, + }, + {name: "BadIPv6", + args: args{aIP: "fd00:10:ono;ogmo:1::3"}, + want: "", + wantErr: true, + }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getIPVersion(tt.args.aIP) + if (err != nil) != tt.wantErr { + t.Errorf("getIPVersion() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("getIPVersion() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/test-network-function/networking/testdata/test_listening_port.json b/test-network-function/networking/testdata/test_listening_port.json new file mode 100644 index 000000000..bcce3c275 --- /dev/null +++ b/test-network-function/networking/testdata/test_listening_port.json @@ -0,0 +1,5 @@ +[ + "tcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*", + "tcp LISTEN 0 128 0.0.0.0:8443 0.0.0.0:*", + "tcp LISTEN 0 128 [::]:22" +] \ No newline at end of file diff --git a/test-network-function/networking/testdata/test_ports.json b/test-network-function/networking/testdata/test_ports.json new file mode 100644 index 000000000..733316944 --- /dev/null +++ b/test-network-function/networking/testdata/test_ports.json @@ -0,0 +1,22 @@ +[ +{ + "containerPort": 8443, + "name": "https", + "protocol": "TCP" +}, +{ + "containerPort": 50051, + "name": "grpc", + "protocol": "TCP" +}, +{ + "containerPort": 8080, + "name": "http-probe", + "protocol": "TCP" +}, +{ + "containerPort": 8080, + "name": "http-probe", + "protocol": "TCP" +} +] \ No newline at end of file diff --git a/test-network-function/observability/doc.go b/test-network-function/observability/doc.go index 587e62f2e..eeea9e484 100644 --- a/test-network-function/observability/doc.go +++ b/test-network-function/observability/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/test-network-function/observability/suite.go b/test-network-function/observability/suite.go index b31378c43..28f722061 100644 --- a/test-network-function/observability/suite.go +++ b/test-network-function/observability/suite.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,15 +17,18 @@ package observability import ( + "fmt" "path" + "time" - "github.com/onsi/ginkgo" - ginkgoconfig "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" log "github.com/sirupsen/logrus" + "github.com/test-network-function/test-network-function/pkg/config" + "github.com/test-network-function/test-network-function/pkg/config/configsections" "github.com/test-network-function/test-network-function/pkg/tnf" - "github.com/test-network-function/test-network-function/pkg/tnf/handlers/generic" "github.com/test-network-function/test-network-function/pkg/tnf/testcases" + "github.com/test-network-function/test-network-function/pkg/utils" "github.com/test-network-function/test-network-function/test-network-function/common" "github.com/test-network-function/test-network-function/test-network-function/identifiers" "github.com/test-network-function/test-network-function/test-network-function/results" @@ -37,55 +40,101 @@ import ( var ( // loggingTestPath is the file location of the logging.json test case relative to the project root. loggingTestPath = path.Join("pkg", "tnf", "handlers", "logging", "logging.json") - // relativeLoggingTestPath is the relative path to the logging.json test case. relativeLoggingTestPath = path.Join(common.PathRelativeToRoot, loggingTestPath) -) + // crdTestPath is the file location of the CRD status existence test case relative to the project root. + crdTestPath = path.Join("pkg", "tnf", "handlers", "crdstatusexistence", "crdstatusexistence.json") + // relativeCrdTestPath is the relative path to the crdstatusexistence.json test case. + relativeCrdTestPath = path.Join(common.PathRelativeToRoot, crdTestPath) + // testCrdsTimeout is the timeout in seconds for the CRDs TC. + testCrdsTimeout = 10 * time.Second + // retrieve the singleton instance of test environment + env *config.TestEnvironment = config.GetTestEnvironment() +) var _ = ginkgo.Describe(common.ObservabilityTestKey, func() { - if testcases.IsInFocus(ginkgoconfig.GinkgoConfig.FocusStrings, common.ObservabilityTestKey) { - config := common.GetTestConfiguration() - log.Infof("Test Configuration: %s", config) + conf, _ := ginkgo.GinkgoConfiguration() - for _, cid := range config.ExcludeContainersFromConnectivityTests { - common.ContainersToExcludeFromConnectivityTests[cid] = "" + if testcases.IsInFocus(conf.FocusStrings, common.ObservabilityTestKey) { + ginkgo.BeforeEach(func() { + env.LoadAndRefresh() + gomega.Expect(len(env.PodsUnderTest)).ToNot(gomega.Equal(0)) + gomega.Expect(len(env.ContainersUnderTest)).ToNot(gomega.Equal(0)) + }) + ginkgo.ReportAfterEach(results.RecordResult) + ginkgo.AfterEach(env.CloseLocalShellContext) + testLogging() + testCrds() + } +}) + +func testLogging() { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestLoggingIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + failedCutIds := []*configsections.ContainerIdentifier{} + for _, cut := range env.ContainersUnderTest { + cutIdentifier := &cut.ContainerIdentifier + ginkgo.By(fmt.Sprintf("Test container: %+v. should emit at least one line of log to stderr/stdout", cutIdentifier)) + + context := env.GetLocalShellContext() + + values := make(map[string]interface{}) + values["POD_NAMESPACE"] = cutIdentifier.Namespace + values["POD_NAME"] = cutIdentifier.PodName + values["CONTAINER_NAME"] = cutIdentifier.ContainerName + tester, handlers := utils.NewGenericTesterAndValidate(relativeLoggingTestPath, common.RelativeSchemaPath, values) + test, err := tnf.NewTest(context.GetExpecter(), *tester, handlers, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(test).ToNot(gomega.BeNil()) + + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("FAILURE: Container: %s (Pod %s ns %s) does not have any line of log to stderr/stdout", + cutIdentifier.ContainerName, cutIdentifier.PodName, cutIdentifier.Namespace) + failedCutIds = append(failedCutIds, cutIdentifier) + }, func(err error) { + tnf.ClaimFilePrintf("ERROR: Container: %s (Pod %s) does not have any line of log to stderr/stdout. Error: %v", + cutIdentifier.ContainerName, cutIdentifier.PodName, cutIdentifier.Namespace, err) + failedCutIds = append(failedCutIds, cutIdentifier) + }) } - containersUnderTest := common.CreateContainersUnderTest(config) - log.Info(containersUnderTest) - for _, containerUnderTest := range containersUnderTest { - testLogging(containerUnderTest.Oc.GetPodNamespace(), containerUnderTest.Oc.GetPodName(), containerUnderTest.Oc.GetPodContainerName()) + if n := len(failedCutIds); n > 0 { + log.Debugf("Containers without logging: %+v", failedCutIds) + ginkgo.Fail(fmt.Sprintf("%d containers don't have any log to stdout/stderr.", n)) } + }) +} - } -}) +func testCrds() { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestCrdsStatusSubresourceIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + ginkgo.By("CRDs should have a status subresource") + context := env.GetLocalShellContext() + failedCrds := []string{} + for _, crdName := range env.CrdNames { + ginkgo.By("Testing CRD " + crdName) -func testLogging(podNameSpace, podName, containerName string) { - ginkgo.When("Testing PUT is emitting logs to stdout/stderr", func() { - ginkgo.It("should return at least one line of log", func() { - defer results.RecordResult(identifiers.TestLoggingIdentifier) - loggingTest(podNameSpace, podName, containerName) - }) + values := make(map[string]interface{}) + values["CRD_NAME"] = crdName + values["TIMEOUT"] = testCrdsTimeout.Nanoseconds() + + tester, handlers := utils.NewGenericTesterAndValidate(relativeCrdTestPath, common.RelativeSchemaPath, values) + test, err := tnf.NewTest(context.GetExpecter(), *tester, handlers, context.GetErrorChannel()) + gomega.Expect(test).ToNot(gomega.BeNil()) + gomega.Expect(err).To(gomega.BeNil()) + + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("FAILURE: CRD %s does not have a status subresource.", crdName) + failedCrds = append(failedCrds, crdName) + }, func(err error) { + tnf.ClaimFilePrintf("FAILURE: CRD %s does not have a status subresource.", crdName) + failedCrds = append(failedCrds, crdName) + }) + } + + if n := len(failedCrds); n > 0 { + log.Debugf("CRDs without status subresource: %+v", failedCrds) + ginkgo.Fail(fmt.Sprintf("%d CRDs don't have status subresource", n)) + } }) } -func loggingTest(podNamespace, podName, containerName string) { - context := common.GetContext() - values := make(map[string]interface{}) - values["POD_NAMESPACE"] = podNamespace - values["POD_NAME"] = podName - values["CONTAINER_NAME"] = containerName - test, handlers, result, err := generic.NewGenericFromMap(relativeLoggingTestPath, common.RelativeSchemaPath, values) - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(result).ToNot(gomega.BeNil()) - gomega.Expect(result.Valid()).To(gomega.BeTrue()) - gomega.Expect(handlers).ToNot(gomega.BeNil()) - gomega.Expect(handlers).ToNot(gomega.BeNil()) - gomega.Expect(test).ToNot(gomega.BeNil()) - tester, err := tnf.NewTest(context.GetExpecter(), *test, handlers, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(tester).ToNot(gomega.BeNil()) - - testResult, err := tester.Run() - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) -} diff --git a/test-network-function/operator/doc.go b/test-network-function/operator/doc.go index 1e02342e9..8fcc7c6c8 100644 --- a/test-network-function/operator/doc.go +++ b/test-network-function/operator/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/test-network-function/operator/suite.go b/test-network-function/operator/suite.go index 41879c06e..b333da643 100644 --- a/test-network-function/operator/suite.go +++ b/test-network-function/operator/suite.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -20,42 +20,31 @@ import ( "fmt" "path" "strings" - "time" - "github.com/test-network-function/test-network-function/pkg/tnf/handlers/generic" + log "github.com/sirupsen/logrus" + "github.com/test-network-function/test-network-function/pkg/config/configsections" + "github.com/test-network-function/test-network-function/pkg/utils" + "github.com/test-network-function/test-network-function/test-network-function/common" "github.com/test-network-function/test-network-function/test-network-function/identifiers" - "github.com/test-network-function/test-network-function/test-network-function/results" - "github.com/onsi/ginkgo" - ginkgoconfig "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" - "github.com/test-network-function/test-network-function/internal/api" "github.com/test-network-function/test-network-function/pkg/config" - "github.com/test-network-function/test-network-function/pkg/config/configsections" "github.com/test-network-function/test-network-function/pkg/tnf" "github.com/test-network-function/test-network-function/pkg/tnf/handlers/operator" - "github.com/test-network-function/test-network-function/pkg/tnf/interactive" "github.com/test-network-function/test-network-function/pkg/tnf/reel" "github.com/test-network-function/test-network-function/pkg/tnf/testcases" + "github.com/test-network-function/test-network-function/test-network-function/results" ) const ( configuredTestFile = "testconfigure.yml" // The default test timeout. - defaultTimeoutSeconds = 10 - // timeout for eventually call - eventuallyTimeoutSeconds = 30 - // interval of time - interval = 1 - testSpecName = "operator" - subscriptionTest = "SUBSCRIPTION_INSTALLED" + testSpecName = "operator" ) var ( - defaultTimeout = time.Duration(defaultTimeoutSeconds) * time.Second - context *interactive.Context - err error // checkSubscriptionTestPath is the file location of the uncordon.json test case relative to the project root. checkSubscriptionTestPath = path.Join("pkg", "tnf", "handlers", "checksubscription", "check-subscription.json") @@ -63,8 +52,8 @@ var ( // pathRelativeToRoot is used to calculate relative filepaths for the `test-network-function` executable entrypoint. pathRelativeToRoot = path.Join("..") - // relativeNodesTestPath is the relative path to the nodes.json test case. - relativeNodesTestPath = path.Join(pathRelativeToRoot, checkSubscriptionTestPath) + // relativecheckSubscriptionTestPath is the relative path to the nodes.json test case. + relativecheckSubscriptionTestPath = path.Join(pathRelativeToRoot, checkSubscriptionTestPath) // relativeSchemaPath is the relative path to the generic-test.schema.json JSON schema. relativeSchemaPath = path.Join(pathRelativeToRoot, schemaPath) @@ -74,135 +63,108 @@ var ( ) var _ = ginkgo.Describe(testSpecName, func() { - if testcases.IsInFocus(ginkgoconfig.GinkgoConfig.FocusStrings, testSpecName) { - defer ginkgo.GinkgoRecover() - ginkgo.When("a local shell is spawned", func() { - goExpectSpawner := interactive.NewGoExpectSpawner() - var spawner interactive.Spawner = goExpectSpawner - context, err = interactive.SpawnShell(&spawner, defaultTimeout, interactive.Verbose(true)) - ginkgo.It("should be created without error", func() { - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(context).ToNot(gomega.BeNil()) - gomega.Expect(context.GetExpecter()).ToNot(gomega.BeNil()) - }) + conf, _ := ginkgo.GinkgoConfiguration() + if testcases.IsInFocus(conf.FocusStrings, testSpecName) { + env := config.GetTestEnvironment() + ginkgo.BeforeEach(func() { + env.LoadAndRefresh() + if len(env.OperatorsUnderTest) == 0 { + ginkgo.Skip("No Operator found.") + } }) + ginkgo.ReportAfterEach(results.RecordResult) + ginkgo.AfterEach(env.CloseLocalShellContext) + defer ginkgo.GinkgoRecover() ginkgo.Context("Runs test on operators", func() { - itRunsTestsOnOperator() + itRunsTestsOnOperator(env) }) - testOperatorsAreInstalledViaOLM() + testOperatorsAreInstalledViaOLM(env) } }) // testOperatorsAreInstalledViaOLM ensures all configured operators have a proper OLM subscription. -func testOperatorsAreInstalledViaOLM() { - _, operatorsInTest := getConfig() - for _, operatorInTest := range operatorsInTest { - ginkgo.Context("an operator is installed", func() { - ginkgo.When("subscriptions are polled", func() { - ginkgo.It(fmt.Sprintf("%s in namespace %s Should have a valid subscription", operatorInTest.SubscriptionName, operatorInTest.Namespace), func() { - defer results.RecordResult(identifiers.TestOperatorIsInstalledViaOLMIdentifier) - testOperatorIsInstalledViaOLM(operatorInTest.SubscriptionName, operatorInTest.Namespace) - }) - }) - }) - } -} - -// testOperatorIsInstalledViaOLM tests that an operator is installed via OLM. -func testOperatorIsInstalledViaOLM(subscriptionName, subscriptionNamespace string) { - values := make(map[string]interface{}) - values["SUBSCRIPTION_NAME"] = subscriptionName - values["SUBSCRIPTION_NAMESPACE"] = subscriptionNamespace - test, handlers, result, err := generic.NewGenericFromMap(relativeNodesTestPath, relativeSchemaPath, values) - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(result).ToNot(gomega.BeNil()) - gomega.Expect(result.Valid()).To(gomega.BeTrue()) - gomega.Expect(handlers).ToNot(gomega.BeNil()) - gomega.Expect(len(handlers)).To(gomega.Equal(1)) - gomega.Expect(test).ToNot(gomega.BeNil()) - - tester, err := tnf.NewTest(context.GetExpecter(), *test, handlers, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(tester).ToNot(gomega.BeNil()) - - testResult, err := tester.Run() - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) -} - -func getConfig() ([]configsections.CertifiedOperatorRequestInfo, []configsections.Operator) { - conf := config.GetConfigInstance() - operatorsToQuery := conf.CertifiedOperatorInfo - operatorsInTest := conf.Operators - return operatorsToQuery, operatorsInTest -} +func testOperatorsAreInstalledViaOLM(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestOperatorIsInstalledViaOLMIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + badOperators := []configsections.Operator{} + for _, operatorInTest := range env.OperatorsUnderTest { + ginkgo.By(fmt.Sprintf("%s in namespace %s Should have a valid subscription", operatorInTest.SubscriptionName, operatorInTest.Namespace)) + values := make(map[string]interface{}) + values["SUBSCRIPTION_NAME"] = operatorInTest.SubscriptionName + values["SUBSCRIPTION_NAMESPACE"] = operatorInTest.Namespace + tester, handlers := utils.NewGenericTesterAndValidate(relativecheckSubscriptionTestPath, relativeSchemaPath, values) + context := env.GetLocalShellContext() + test, err := tnf.NewTest(context.GetExpecter(), *tester, handlers, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(test).ToNot(gomega.BeNil()) -func itRunsTestsOnOperator() { - operatorsToQuery, operatorsInTest := getConfig() - if len(operatorsToQuery) > 0 { - certAPIClient := api.NewHTTPClient() - for _, certified := range operatorsToQuery { - // Care: this test takes some time to run, failures at later points while before this has finished may be reported as a failure here. Read the failure reason carefully. - ginkgo.It(fmt.Sprintf("should eventually be verified as certified (operator %s/%s)", certified.Organization, certified.Name), func() { - defer results.RecordResult(identifiers.TestOperatorIsCertifiedIdentifier) - certified := certified // pin - gomega.Eventually(func() bool { - isCertified := certAPIClient.IsOperatorCertified(certified.Organization, certified.Name) - return isCertified - }, eventuallyTimeoutSeconds, interval).Should(gomega.BeTrue()) + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("Operator %s doesn't have a proper OLM subscription.", operatorInTest.Name) + badOperators = append(badOperators, *operatorInTest) + }, func(err error) { + tnf.ClaimFilePrintf("Operator %s doesn't have a proper OLM subscription. Error: %v", operatorInTest.Name, err) + badOperators = append(badOperators, *operatorInTest) }) } - } - gomega.Expect(operatorsInTest).ToNot(gomega.BeNil()) - for _, op := range operatorsInTest { - // TODO: Gather facts for operator - for _, testType := range op.Tests { - testFile, err := testcases.LoadConfiguredTestFile(configuredTestFile) - gomega.Expect(testFile).ToNot(gomega.BeNil()) - gomega.Expect(err).To(gomega.BeNil()) - testConfigure := testcases.ContainsConfiguredTest(testFile.OperatorTest, testType) - renderedTestCase, err := testConfigure.RenderTestCaseSpec(testcases.Operator, testType) - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(renderedTestCase).ToNot(gomega.BeNil()) - for _, testCase := range renderedTestCase.TestCase { - if testCase.SkipTest { - continue - } - if testCase.ExpectedType == testcases.Function { - for _, val := range testCase.ExpectedStatus { - testCase.ExpectedStatusFn(op.Name, testcases.StatusFunctionType(val)) - } - } - name := agrName(op.Name, op.SubscriptionName, testCase.Name) - args := []interface{}{name, op.Namespace} - runTestsOnOperator(args, name, op.Namespace, testCase) - } + + if n := len(badOperators); n > 0 { + log.Warnf("Operators without proper OLM subscription: %+v", badOperators) + ginkgo.Fail(fmt.Sprintf("%d operators found without proper OLM subscription.", n)) } - } + }) } -func agrName(operatorName, subName, testName string) string { - name := operatorName - if testName == subscriptionTest { - name = subName +func itRunsTestsOnOperator(env *config.TestEnvironment) { + for _, testType := range testcases.GetConfiguredOperatorTests() { + testFile, err := testcases.LoadConfiguredTestFile(configuredTestFile) + gomega.Expect(testFile).ToNot(gomega.BeNil()) + gomega.Expect(err).To(gomega.BeNil()) + testConfigure := testcases.ContainsConfiguredTest(testFile.OperatorTest, testType) + renderedTestCase, err := testConfigure.RenderTestCaseSpec(testcases.Operator, testType) + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(renderedTestCase).ToNot(gomega.BeNil()) + for _, testCase := range renderedTestCase.TestCase { + if testCase.SkipTest { + continue + } + runTestsOnOperator(env, testCase) + } } - return name } //nolint:gocritic // ignore hugeParam error. Pointers to loop iterator vars are bad and `testCmd` is likely to be such. -func runTestsOnOperator(args []interface{}, name, namespace string, testCmd testcases.BaseTestCase) { - ginkgo.When(fmt.Sprintf("under test is: %s/%s ", namespace, name), func() { - ginkgo.It(fmt.Sprintf("tests for: %s", testCmd.Name), func() { - defer results.RecordResult(identifiers.TestOperatorInstallStatusIdentifier) - cmdArgs := strings.Split(fmt.Sprintf(testCmd.Command, args...), " ") - opInTest := operator.NewOperator(cmdArgs, name, namespace, testCmd.ExpectedStatus, testCmd.ResultType, testCmd.Action, defaultTimeout) +func runTestsOnOperator(env *config.TestEnvironment, testCase testcases.BaseTestCase) { + testID := identifiers.XformToGinkgoItIdentifierExtended(identifiers.TestOperatorInstallStatusIdentifier, testCase.Name) + ginkgo.It(testID, ginkgo.Label(testID), func() { + badOperators := []configsections.Operator{} + for _, op := range env.OperatorsUnderTest { + if testCase.ExpectedType == testcases.Function { + for _, val := range testCase.ExpectedStatus { + testCase.ExpectedStatusFn(op.Name, testcases.StatusFunctionType(val)) + } + } + name := op.Name + args := []interface{}{name, op.Namespace} + cmdArgs := strings.Split(fmt.Sprintf(testCase.Command, args...), " ") + opInTest := operator.NewOperator(cmdArgs, name, op.Namespace, testCase.ExpectedStatus, testCase.ResultType, testCase.Action, common.DefaultTimeout) gomega.Expect(opInTest).ToNot(gomega.BeNil()) + context := env.GetLocalShellContext() test, err := tnf.NewTest(context.GetExpecter(), opInTest, []reel.Handler{opInTest}, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) gomega.Expect(test).ToNot(gomega.BeNil()) - testResult, err := test.Run() - gomega.Expect(err).To(gomega.BeNil()) - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) - }) + + test.RunWithCallbacks(nil, func() { + tnf.ClaimFilePrintf("Operator %s failed TC: %s", name, testCase.Name) + badOperators = append(badOperators, *op) + }, func(err error) { + tnf.ClaimFilePrintf("Operator %s failed TC: %s. Error: %v", name, testCase.Name, err) + badOperators = append(badOperators, *op) + }) + } + + if n := len(badOperators); n > 0 { + log.Warnf("Operators that failed TC %s: %+v", testCase.Name, badOperators) + ginkgo.Fail(fmt.Sprintf("%d operators failed TC %s", n, testCase.Name)) + } }) } diff --git a/test-network-function/platform/doc.go b/test-network-function/platform/doc.go index 9b9074871..9b2d9e2fa 100644 --- a/test-network-function/platform/doc.go +++ b/test-network-function/platform/doc.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/test-network-function/platform/suite.go b/test-network-function/platform/suite.go index b1f9e65e5..971c96b80 100644 --- a/test-network-function/platform/suite.go +++ b/test-network-function/platform/suite.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -20,29 +20,28 @@ import ( "encoding/json" "fmt" "regexp" + "sort" "strconv" "strings" + "time" + log "github.com/sirupsen/logrus" + + "github.com/test-network-function/test-network-function/pkg/config" + "github.com/test-network-function/test-network-function/pkg/config/configsections" "github.com/test-network-function/test-network-function/pkg/tnf/testcases" "github.com/test-network-function/test-network-function/test-network-function/common" "github.com/test-network-function/test-network-function/test-network-function/identifiers" - "github.com/test-network-function/test-network-function/test-network-function/results" - "github.com/onsi/ginkgo" - ginkgoconfig "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" - log "github.com/sirupsen/logrus" "github.com/test-network-function/test-network-function/pkg/tnf" - "github.com/test-network-function/test-network-function/pkg/tnf/handlers/bootconfigentries" + "github.com/test-network-function/test-network-function/pkg/tnf/handlers/base/redhat" "github.com/test-network-function/test-network-function/pkg/tnf/handlers/cnffsdiff" - "github.com/test-network-function/test-network-function/pkg/tnf/handlers/containerid" "github.com/test-network-function/test-network-function/pkg/tnf/handlers/currentkernelcmdlineargs" - "github.com/test-network-function/test-network-function/pkg/tnf/handlers/hugepages" "github.com/test-network-function/test-network-function/pkg/tnf/handlers/mckernelarguments" - "github.com/test-network-function/test-network-function/pkg/tnf/handlers/nodehugepages" "github.com/test-network-function/test-network-function/pkg/tnf/handlers/nodemcname" - "github.com/test-network-function/test-network-function/pkg/tnf/handlers/nodenames" "github.com/test-network-function/test-network-function/pkg/tnf/handlers/nodetainted" "github.com/test-network-function/test-network-function/pkg/tnf/handlers/podnodename" "github.com/test-network-function/test-network-function/pkg/tnf/handlers/readbootconfig" @@ -50,72 +49,173 @@ import ( "github.com/test-network-function/test-network-function/pkg/tnf/interactive" "github.com/test-network-function/test-network-function/pkg/tnf/reel" utils "github.com/test-network-function/test-network-function/pkg/utils" + "github.com/test-network-function/test-network-function/test-network-function/results" +) + +const ( + RhelDefaultHugepagesz = 2048 // kB + RhelDefaultHugepages = 0 + HugepagesParam = "hugepages" + HugepageszParam = "hugepagesz" + DefaultHugepagesz = "default_hugepagesz" + KernArgsKeyValueSplitLen = 2 + commandTimeout = 30 * time.Second ) +type hugePagesConfig struct { + hugepagesSize int // size in kb + hugepagesCount int +} + +// numaHugePagesPerSize maps a numa id to an array of hugePagesConfig structs. +type numaHugePagesPerSize map[int][]hugePagesConfig + +// String is the stringer implementation for the numaHugePagesPerSize type so debug/info +// lines look better. +func (numaHugepages numaHugePagesPerSize) String() string { + // Order numa ids/indexes + numaIndexes := make([]int, 0) + for numaIdx := range numaHugepages { + numaIndexes = append(numaIndexes, numaIdx) + } + sort.Ints(numaIndexes) + + str := "" + for _, numaIdx := range numaIndexes { + hugepagesPerSize := numaHugepages[numaIdx] + str += fmt.Sprintf("Numa=%d ", numaIdx) + for _, hugepages := range hugepagesPerSize { + str += fmt.Sprintf("[Size=%dkB Count=%d] ", hugepages.hugepagesSize, hugepages.hugepagesCount) + } + } + return str +} + +// machineConfig maps a json machineconfig object to get the KernelArguments and systemd units info. +type machineConfig struct { + Spec struct { + KernelArguments []string `json:"kernelArguments"` + Config struct { + Systemd struct { + Units []systemdHugePagesUnit `json:"units"` + } + } `json:"config"` + } `json:"spec"` +} + +// systemdHugePagesUnit maps a systemd unit in a machineconfig json object. +type systemdHugePagesUnit struct { + Contents string `json:"contents"` + Name string `json:"name"` +} + // // All actual test code belongs below here. Utilities belong above. // -var _ = ginkgo.Describe(common.PlatformAlterationTestKey, func() { - if testcases.IsInFocus(ginkgoconfig.GinkgoConfig.FocusStrings, common.PlatformAlterationTestKey) { - config := common.GetTestConfiguration() - log.Infof("Test Configuration: %s", config) - - containersUnderTest := common.CreateContainersUnderTest(config) - partnerContainers := common.CreatePartnerContainers(config) - fsDiffContainer := partnerContainers[config.FsDiffMasterContainer] - log.Info(containersUnderTest) - - ginkgo.Context("Container does not have additional packages installed", func() { - // use this boolean to turn off tests that require OS packages - if !common.IsMinikube() { - if fsDiffContainer != nil { - for _, containerUnderTest := range containersUnderTest { - testFsDiff(fsDiffContainer.Oc, containerUnderTest.Oc) - } - } else { - log.Warn("no fs diff container is configured, cannot run fs diff test") - } - } - }) - testTainted() - testHugepages() +func getTaintedBitValues() []string { + return []string{"proprietary module was loaded", + "module was force loaded", + "kernel running on an out of specification system", + "module was force unloaded", + "processor reported a Machine Check Exception (MCE)", + "bad page referenced or some unexpected page flags", + "taint requested by userspace application", + "kernel died recently, i.e. there was an OOPS or BUG", + "ACPI table overridden by user", + "kernel issued warning", + "staging driver was loaded", + "workaround for bug in platform firmware applied", + "externally-built (“out-of-tree”) module was loaded", + "unsigned module was loaded", + "soft lockup occurred", + "kernel has been live patched", + "auxiliary taint, defined for and used by distros", + "kernel was built with the struct randomization plugin", + } +} - if !common.IsMinikube() { - for _, containersUnderTest := range containersUnderTest { - testBootParams(common.GetContext(), containersUnderTest.Oc.GetPodName(), containersUnderTest.Oc.GetPodNamespace(), containersUnderTest.Oc) - } +var _ = ginkgo.Describe(common.PlatformAlterationTestKey, func() { + conf, _ := ginkgo.GinkgoConfiguration() + if testcases.IsInFocus(conf.FocusStrings, common.PlatformAlterationTestKey) { + env := config.GetTestEnvironment() + ginkgo.BeforeEach(func() { + env.LoadAndRefresh() + gomega.Expect(len(env.PodsUnderTest)).ToNot(gomega.Equal(0)) + gomega.Expect(len(env.ContainersUnderTest)).ToNot(gomega.Equal(0)) + }) + ginkgo.ReportAfterEach(results.RecordResult) + ginkgo.AfterEach(env.CloseLocalShellContext) + // use this boolean to turn off tests that require OS packages + if !common.IsNonOcpCluster() { + testContainersFsDiff(env) + testHugepages(env) + testBootParams(env) + testSysctlConfigs(env) } + testTainted(env) // minikube tainted kernels are allowed via config + testIsRedHatRelease(env) + } +}) - if !common.IsMinikube() { - for _, containersUnderTest := range containersUnderTest { - // no test identifier defined, not an official test - testSysctlConfigs(common.GetContext(), containersUnderTest.Oc.GetPodName(), containersUnderTest.Oc.GetPodNamespace()) - } +// testIsRedHatRelease fetch the configuration and test containers attached to oc is Red Hat based. +func testIsRedHatRelease(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestIsRedHatReleaseIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + ginkgo.By("should report a proper Red Hat version") + for _, cut := range env.ContainersUnderTest { + testContainerIsRedHatRelease(cut) } + }) +} - } -}) +// testContainerIsRedHatRelease tests whether the container attached to oc is Red Hat based. +func testContainerIsRedHatRelease(cut *configsections.Container) { + podName := cut.GetOc().GetPodName() + containerName := cut.GetOc().GetPodContainerName() + context := cut.GetOc() + ginkgo.By(fmt.Sprintf("%s(%s) is checked for Red Hat version", podName, containerName)) + versionTester := redhat.NewRelease(common.DefaultTimeout) + test, err := tnf.NewTest(context.GetExpecter(), versionTester, []reel.Handler{versionTester}, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + test.RunAndValidate() +} -// Helper to test that the PUT didn't install new packages after starting, and report through Ginkgo. -func testFsDiff(masterPodOc, targetPodOc *interactive.Oc) { - ginkgo.It(fmt.Sprintf("%s(%s) should not install new packages after starting", targetPodOc.GetPodName(), targetPodOc.GetPodContainerName()), func() { - defer results.RecordResult(identifiers.TestUnalteredBaseImageIdentifier) - targetPodOc.GetExpecter() - containerIDTester := containerid.NewContainerID(common.DefaultTimeout) - test, err := tnf.NewTest(targetPodOc.GetExpecter(), containerIDTester, []reel.Handler{containerIDTester}, targetPodOc.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - testResult, err := test.Run() - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) - gomega.Expect(err).To(gomega.BeNil()) - containerID := containerIDTester.GetID() - - fsDiffTester := cnffsdiff.NewFsDiff(common.DefaultTimeout, containerID) - test, err = tnf.NewTest(masterPodOc.GetExpecter(), fsDiffTester, []reel.Handler{fsDiffTester}, masterPodOc.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - testResult, err = test.Run() - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) - gomega.Expect(err).To(gomega.BeNil()) +// testContainersFsDiff test that all CUT didn't install new packages are starting +func testContainersFsDiff(env *config.TestEnvironment) { + ginkgo.Context("Container does not have additional packages installed", func() { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestUnalteredBaseImageIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + var badContainers []string + var errContainers []string + for _, cut := range env.ContainersUnderTest { + podName := cut.GetOc().GetPodName() + containerName := cut.GetOc().GetPodContainerName() + nodeName := cut.NodeName + ginkgo.By(fmt.Sprintf("%s(%s) should not install new packages after starting", podName, containerName)) + nodeOc := env.NodesUnderTest[nodeName].DebugContainer.GetOc() + fsDiffTester := cnffsdiff.NewFsDiff(common.DefaultTimeout, cut.ContainerUID, nodeName) + test, err := tnf.NewTest(nodeOc.GetExpecter(), fsDiffTester, []reel.Handler{fsDiffTester}, nodeOc.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + var message string + test.RunWithCallbacks(nil, func() { + badContainers = append(badContainers, containerName) + message = fmt.Sprintf("pod %s container %s did update/install/modify additional packages", podName, containerName) + }, func(err error) { + errContainers = append(errContainers, containerName) + if reel.IsTimeout(err) { + env.NodesUnderTest[nodeName].DebugContainer.CloseOc() + } + message = fmt.Sprintf("Failed to check pod %s container %s for additional packages due to: %v", podName, containerName, err) + }) + _, err = ginkgo.GinkgoWriter.Write([]byte(message)) + if err != nil { + log.Errorf("Ginkgo writer could not write because: %s", err) + } + } + gomega.Expect(badContainers).To(gomega.BeNil()) + gomega.Expect(errContainers).To(gomega.BeNil()) + }) }) } @@ -123,7 +223,7 @@ func getMcKernelArguments(context *interactive.Context, mcName string) map[strin mcKernelArgumentsTester := mckernelarguments.NewMcKernelArguments(common.DefaultTimeout, mcName) test, err := tnf.NewTest(context.GetExpecter(), mcKernelArgumentsTester, []reel.Handler{mcKernelArgumentsTester}, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) - common.RunAndValidateTest(test) + test.RunAndValidate() mcKernelArguments := mcKernelArgumentsTester.GetKernelArguments() var mcKernelArgumentsJSON []string err = json.Unmarshal([]byte(mcKernelArguments), &mcKernelArgumentsJSON) @@ -136,7 +236,7 @@ func getMcName(context *interactive.Context, nodeName string) string { mcNameTester := nodemcname.NewNodeMcName(common.DefaultTimeout, nodeName) test, err := tnf.NewTest(context.GetExpecter(), mcNameTester, []reel.Handler{mcNameTester}, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) - common.RunAndValidateTest(test) + test.RunAndValidate() return mcNameTester.GetMcName() } @@ -144,52 +244,25 @@ func getPodNodeName(context *interactive.Context, podName, podNamespace string) podNameTester := podnodename.NewPodNodeName(common.DefaultTimeout, podName, podNamespace) test, err := tnf.NewTest(context.GetExpecter(), podNameTester, []reel.Handler{podNameTester}, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) - common.RunAndValidateTest(test) + test.RunAndValidate() return podNameTester.GetNodeName() } -func getCurrentKernelCmdlineArgs(targetPodOc *interactive.Oc) map[string]string { +func getCurrentKernelCmdlineArgs(targetContainerOc *interactive.Oc) map[string]string { currentKernelCmdlineArgsTester := currentkernelcmdlineargs.NewCurrentKernelCmdlineArgs(common.DefaultTimeout) - test, err := tnf.NewTest(targetPodOc.GetExpecter(), currentKernelCmdlineArgsTester, []reel.Handler{currentKernelCmdlineArgsTester}, targetPodOc.GetErrorChannel()) + test, err := tnf.NewTest(targetContainerOc.GetExpecter(), currentKernelCmdlineArgsTester, []reel.Handler{currentKernelCmdlineArgsTester}, targetContainerOc.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) - common.RunAndValidateTest(test) + test.RunAndValidate() currnetKernelCmdlineArgs := currentKernelCmdlineArgsTester.GetKernelArguments() currentSplitKernelCmdlineArgs := strings.Split(currnetKernelCmdlineArgs, " ") return utils.ArgListToMap(currentSplitKernelCmdlineArgs) } -func getBootEntryIndex(bootEntry string) (int, error) { - return strconv.Atoi(strings.Split(bootEntry, "-")[1]) -} - -func getMaxIndexEntry(bootConfigEntries []string) string { - maxIndex, err := getBootEntryIndex(bootConfigEntries[0]) +func getGrubKernelArgs(context *interactive.Oc) map[string]string { + readBootConfigTester := readbootconfig.NewReadBootConfig(common.DefaultTimeout) + test, err := tnf.NewTest(context.GetExpecter(), readBootConfigTester, []reel.Handler{readBootConfigTester}, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) - maxIndexEntryName := bootConfigEntries[0] - for _, bootEntry := range bootConfigEntries { - if entryIndex, err2 := getBootEntryIndex(bootEntry); entryIndex > maxIndex { - maxIndex = entryIndex - gomega.Expect(err2).To(gomega.BeNil()) - maxIndexEntryName = bootEntry - } - } - - return maxIndexEntryName -} - -func getGrubKernelArgs(context *interactive.Context, nodeName string) map[string]string { - bootConfigEntriesTester := bootconfigentries.NewBootConfigEntries(common.DefaultTimeout, nodeName) - test, err := tnf.NewTest(context.GetExpecter(), bootConfigEntriesTester, []reel.Handler{bootConfigEntriesTester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - common.RunAndValidateTest(test) - bootConfigEntries := bootConfigEntriesTester.GetBootConfigEntries() - - maxIndexEntryName := getMaxIndexEntry(bootConfigEntries) - - readBootConfigTester := readbootconfig.NewReadBootConfig(common.DefaultTimeout, nodeName, maxIndexEntryName) - test, err = tnf.NewTest(context.GetExpecter(), readBootConfigTester, []reel.Handler{readBootConfigTester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - common.RunAndValidateTest(test) + test.RunAndValidate() bootConfig := readBootConfigTester.GetBootConfig() splitBootConfig := strings.Split(bootConfig, "\n") @@ -216,7 +289,7 @@ func parseSysctlSystemOutput(sysctlSystemOutput string) map[string]string { continue } - keyValRegexp := regexp.MustCompile(`( \S+)(\s*)=(\s*)(\S+)`) // A line is of the form "kernel.yama.ptrace_scope = 0" + keyValRegexp := regexp.MustCompile(`(\S+)(\s*)=(\s*)(\S+)`) // A line is of the form "kernel.yama.ptrace_scope = 0" if !keyValRegexp.MatchString(line) { continue } @@ -228,136 +301,505 @@ func parseSysctlSystemOutput(sysctlSystemOutput string) map[string]string { return retval } -func getSysctlConfigArgs(context *interactive.Context, nodeName string) map[string]string { - sysctlAllConfigsArgsTester := sysctlallconfigsargs.NewSysctlAllConfigsArgs(common.DefaultTimeout, nodeName) +func getSysctlConfigArgs(context *interactive.Oc) map[string]string { + sysctlAllConfigsArgsTester := sysctlallconfigsargs.NewSysctlAllConfigsArgs(common.DefaultTimeout) test, err := tnf.NewTest(context.GetExpecter(), sysctlAllConfigsArgsTester, []reel.Handler{sysctlAllConfigsArgsTester}, context.GetErrorChannel()) gomega.Expect(err).To(gomega.BeNil()) - common.RunAndValidateTest(test) + test.RunAndValidate() sysctlAllConfigsArgs := sysctlAllConfigsArgsTester.GetSysctlAllConfigsArgs() return parseSysctlSystemOutput(sysctlAllConfigsArgs) } -func testBootParams(context *interactive.Context, podName, podNamespace string, targetPodOc *interactive.Oc) { - ginkgo.It(fmt.Sprintf("Testing boot params for the pod's node %s/%s", podNamespace, podName), func() { - defer results.RecordResult(identifiers.TestUnalteredStartupBootParamsIdentifier) - nodeName := getPodNodeName(context, podName, podNamespace) - mcName := getMcName(context, nodeName) - mcKernelArgumentsMap := getMcKernelArguments(context, mcName) - currentKernelArgsMap := getCurrentKernelCmdlineArgs(targetPodOc) - grubKernelConfigMap := getGrubKernelArgs(context, nodeName) +func testBootParams(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestUnalteredStartupBootParamsIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + context := env.GetLocalShellContext() + for _, cut := range env.ContainersUnderTest { + podName := cut.GetOc().GetPodName() + podNameSpace := cut.GetOc().GetPodNamespace() + targetContainerOc := cut.GetOc() + testBootParamsHelper(context, podName, podNameSpace, targetContainerOc) + } + }) +} +func testBootParamsHelper(context *interactive.Context, podName, podNamespace string, targetContainerOc *interactive.Oc) { + ginkgo.By(fmt.Sprintf("Testing boot params for the pod's node %s/%s", podNamespace, podName)) + nodeName := getPodNodeName(context, podName, podNamespace) + mcName := getMcName(context, nodeName) + mcKernelArgumentsMap := getMcKernelArguments(context, mcName) + currentKernelArgsMap := getCurrentKernelCmdlineArgs(targetContainerOc) + env := config.GetTestEnvironment() + nodeOC := env.NodesUnderTest[nodeName].DebugContainer.GetOc() + grubKernelConfigMap := getGrubKernelArgs(nodeOC) + + for key, mcVal := range mcKernelArgumentsMap { + if currentVal, ok := currentKernelArgsMap[key]; ok { + gomega.Expect(currentVal).To(gomega.Equal(mcVal)) + } + if grubVal, ok := grubKernelConfigMap[key]; ok { + gomega.Expect(grubVal).To(gomega.Equal(mcVal)) + } + } +} - for key, mcVal := range mcKernelArgumentsMap { - if currentVal, ok := currentKernelArgsMap[key]; ok { - gomega.Expect(currentVal).To(gomega.Equal(mcVal)) +func testSysctlConfigs(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestSysctlConfigsIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + for _, podUnderTest := range env.PodsUnderTest { + testSysctlConfigsHelper(podUnderTest.Name, podUnderTest.Namespace, env.GetLocalShellContext()) + } + }) +} + +func testSysctlConfigsHelper(podName, podNamespace string, context *interactive.Context) { + ginkgo.By(fmt.Sprintf("Testing sysctl config files for the pod's node %s/%s", podNamespace, podName)) + nodeName := getPodNodeName(context, podName, podNamespace) + env := config.GetTestEnvironment() + nodeOc := env.NodesUnderTest[nodeName].DebugContainer.GetOc() + combinedSysctlSettings := getSysctlConfigArgs(nodeOc) + mcName := getMcName(context, nodeName) + mcKernelArgumentsMap := getMcKernelArguments(context, mcName) + for key, sysctlConfigVal := range combinedSysctlSettings { + if mcVal, ok := mcKernelArgumentsMap[key]; ok { + gomega.Expect(mcVal).To(gomega.Equal(sysctlConfigVal)) + } + } +} + +//nolint:gocritic +func decodeKernelTaints(bitmap uint64) (string, []string) { + values := getTaintedBitValues() + var out string + individualTaints := []string{} + for i := 0; i < 32; i++ { + bit := (bitmap >> i) & 1 + if bit == 1 { + out += fmt.Sprintf("%s, ", values[i]) + // Storing the individual taint messages for extra parsing. + individualTaints = append(individualTaints, values[i]) + } + } + return out, individualTaints +} + +//nolint:funlen +func testTainted(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestNonTaintedNodeKernelsIdentifier) + ginkgo.It(testID, ginkgo.Label(testID), func() { + ginkgo.By("Testing tainted nodes in cluster") + + var taintedNodes []string + var errNodes []string + for _, node := range env.NodesUnderTest { + if !node.HasDebugPod() { + continue } - if grubVal, ok := grubKernelConfigMap[key]; ok { - gomega.Expect(grubVal).To(gomega.Equal(mcVal)) + ginkgo.By(fmt.Sprintf("Checking kernel taints of node %s", node.Name)) + log.Debug("Node has a debug pod") + context := node.DebugContainer.GetOc() + tester := nodetainted.NewNodeTainted(common.DefaultTimeout) + test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) + gomega.Expect(err).To(gomega.BeNil()) + + var message string + test.RunWithCallbacks(func() { + message = fmt.Sprintf("Decoded tainted kernel causes (code=0) for node %s : None\n", node.Name) + }, func() { + var taintedBitmap uint64 + nodeTaintsAccepted := true + taintedBitmap, err = strconv.ParseUint(tester.Match, 10, 32) //nolint:gomnd // base 10 and uint32 + if err != nil { + message = fmt.Sprintf("Could not decode tainted kernel causes (code=%d) for node %s\n", taintedBitmap, node.Name) + return + } + taintMsg, individualTaints := decodeKernelTaints(taintedBitmap) + + // Count how many taints come from `module was loaded` taints versus `other` + log.Debug("Checking for 'module was loaded' taints") + log.Debug("individualTaints", individualTaints) + moduleTaintsFound := false + otherTaintsFound := false + for _, it := range individualTaints { + if strings.Contains(it, `module was loaded`) { + moduleTaintsFound = true + } else { + otherTaintsFound = true + } + } + + if otherTaintsFound { + nodeTaintsAccepted = false + } else if moduleTaintsFound { + // Retrieve the modules from the node. + modules := utils.GetModulesFromNode(node.Name, context) + log.Debug("Got the modules from node") + + // Loop through the modules looking for `InTree: Y`. + // If the module info does not contain this string, the module is "tainted". + taintedModules := getOutOfTreeModules(modules, node.Name, context) + log.Debug("Collected all of the tainted modules: ", taintedModules) + tnf.ClaimFilePrintf("Kernel Modules loaded that cause taints: %v", taintedModules) + tnf.ClaimFilePrintf("Modules allowed via configuration: %v", env.Config.AcceptedKernelTaints) + + // Looks through the accepted taints listed in the tnf-config file. + // If all of the tainted modules show up in the configuration file, don't fail the test. + nodeTaintsAccepted = taintsAccepted(env.Config.AcceptedKernelTaints, taintedModules) + } + + message = fmt.Sprintf("Decoded tainted kernel causes (code=%d) for node %s : %s\n", taintedBitmap, node.Name, taintMsg) + // Only add the tainted node to the slice if the taint is acceptable. + if !nodeTaintsAccepted { + taintedNodes = append(taintedNodes, node.Name) + } + }, func(e error) { + message = fmt.Sprintf("Failed to retrieve tainted kernel code for node %s\n", node.Name) + errNodes = append(errNodes, node.Name) + }) + + _, err = ginkgo.GinkgoWriter.Write([]byte(message)) + if err != nil { + log.Errorf("Ginkgo writer could not write because: %s", err) } } + + // We are expecting tainted nodes to be Nil, but only if: + // 1) The reason for the tainted node is contains(`module was loaded`) + // 2) The modules loaded are all whitelisted. + gomega.Expect(taintedNodes).To(gomega.BeNil()) + gomega.Expect(errNodes).To(gomega.BeNil()) }) } -func testSysctlConfigs(context *interactive.Context, podName, podNamespace string) { - ginkgo.It(fmt.Sprintf("Testing sysctl config files for the pod's node %s/%s", podNamespace, podName), func() { - nodeName := getPodNodeName(context, podName, podNamespace) - combinedSysctlSettings := getSysctlConfigArgs(context, nodeName) - mcName := getMcName(context, nodeName) - mcKernelArgumentsMap := getMcKernelArguments(context, mcName) +func taintsAccepted(confTaints []configsections.AcceptedKernelTaintsInfo, taintedModules []string) bool { + for _, taintedModule := range taintedModules { + found := false + log.Debug("Accepted Taints from Config: ", confTaints) + for _, confTaint := range confTaints { + log.Debug(fmt.Sprintf("Comparing confTaint: %s to taintedModule: %s", confTaint.Module, taintedModule)) + if confTaint.Module == taintedModule { + found = true + break + } + } + + if !found { + // Tainted modules were not found to be in the allow-list. + return false + } + } + return true +} + +func getOutOfTreeModules(modules []string, nodeName string, ctx *interactive.Oc) []string { + taintedModules := []string{} + for _, module := range modules { + if !utils.ModuleInTree(nodeName, module, ctx) { + taintedModules = append(taintedModules, module) + } + } + return taintedModules +} + +func hugepageSizeToInt(s string) int { + num, _ := strconv.Atoi(s[:len(s)-1]) + unit := s[len(s)-1] + switch unit { + case 'M': + num *= 1024 + case 'G': + num *= 1024 * 1024 + } + + return num +} + +func logMcKernelArgumentsHugepages(hugepagesPerSize map[int]int, defhugepagesz int) { + logStr := fmt.Sprintf("MC KernelArguments hugepages config: default_hugepagesz=%d-kB", defhugepagesz) + for size, count := range hugepagesPerSize { + logStr += fmt.Sprintf(", size=%dkB - count=%d", size, count) + } + log.Info(logStr) +} + +// getMcHugepagesFromMcKernelArguments gets the hugepages params from machineconfig's kernelArguments +func getMcHugepagesFromMcKernelArguments(mc *machineConfig) (hugepagesPerSize map[int]int, defhugepagesz int) { + defhugepagesz = RhelDefaultHugepagesz + hugepagesPerSize = map[int]int{} + + hugepagesz := 0 + for _, arg := range mc.Spec.KernelArguments { + keyValueSlice := strings.Split(arg, "=") + if len(keyValueSlice) != KernArgsKeyValueSplitLen { + // Some kernel arguments don't come in name=value + continue + } - for key, sysctlConfigVal := range combinedSysctlSettings { - if mcVal, ok := mcKernelArgumentsMap[key]; ok { - gomega.Expect(mcVal).To(gomega.Equal(sysctlConfigVal)) + key, value := keyValueSlice[0], keyValueSlice[1] + if key == HugepagesParam && value != "" { + hugepages, _ := strconv.Atoi(value) + if _, sizeFound := hugepagesPerSize[hugepagesz]; sizeFound { + // hugepagesz was parsed before. + hugepagesPerSize[hugepagesz] = hugepages + } else { + // use RHEL's default size for this count. + hugepagesPerSize[RhelDefaultHugepagesz] = hugepages } } + + if key == HugepageszParam && value != "" { + hugepagesz = hugepageSizeToInt(value) + // Create new map entry for this size + hugepagesPerSize[hugepagesz] = 0 + } + + if key == DefaultHugepagesz && value != "" { + defhugepagesz = hugepageSizeToInt(value) + // In case only default_hugepagesz and hugepages values are provided. The actual value should be + // parsed next and this default value overwritten. + hugepagesPerSize[defhugepagesz] = RhelDefaultHugepages + hugepagesz = defhugepagesz + } + } + + if len(hugepagesPerSize) == 0 { + hugepagesPerSize[RhelDefaultHugepagesz] = RhelDefaultHugepages + log.Warnf("No hugepages size found in node's machineconfig. Defaulting to size=%dkB (count=%d)", RhelDefaultHugepagesz, RhelDefaultHugepages) + } + + logMcKernelArgumentsHugepages(hugepagesPerSize, defhugepagesz) + return hugepagesPerSize, defhugepagesz +} + +// getNodeNumaHugePages gets the actual node's hugepages config based on /sys/devices/system/node/nodeX files. +func getNodeNumaHugePages(node *config.NodeConfig) (hugepages numaHugePagesPerSize, err error) { + const cmd = "for file in `find /sys/devices/system/node/ -name nr_hugepages`; do echo $file count:`cat $file` ; done" + const outputRegex = `node(\d+).*hugepages-(\d+)kB.* count:(\d+)` + const numRegexFields = 4 + + // This command must run inside the node, so we'll need the node's context to run commands inside the debug daemonset pod. + var commandErr error + hugepagesCmdOut := utils.ExecuteCommandAndValidate(cmd, commandTimeout, node.DebugContainer.GetOc().Context, func() { + commandErr = fmt.Errorf("failed to get node %s hugepages per numa", node.Name) + }) + if commandErr != nil { + return numaHugePagesPerSize{}, commandErr + } + + hugepages = numaHugePagesPerSize{} + r := regexp.MustCompile(outputRegex) + for _, line := range strings.Split(hugepagesCmdOut, "\n") { + values := r.FindStringSubmatch(line) + if len(values) != numRegexFields { + return numaHugePagesPerSize{}, fmt.Errorf("failed to parse node's numa hugepages output line:%s", line) + } + + numaNode, _ := strconv.Atoi(values[1]) + hpSize, _ := strconv.Atoi(values[2]) + hpCount, _ := strconv.Atoi(values[3]) + + hugepagesCfg := hugePagesConfig{ + hugepagesCount: hpCount, + hugepagesSize: hpSize, + } + + if numaHugepagesCfg, exists := hugepages[numaNode]; exists { + numaHugepagesCfg = append(numaHugepagesCfg, hugepagesCfg) + hugepages[numaNode] = numaHugepagesCfg + } else { + hugepages[numaNode] = []hugePagesConfig{hugepagesCfg} + } + } + + log.Infof("Node %s hugepages: %s", node.Name, hugepages) + return hugepages, nil +} + +// getMachineConfig gets the machineconfig in json format does the unmarshalling. +func getMachineConfig(mcName string, context *interactive.Context) (machineConfig, error) { + var commandErr error + + mcJSON := utils.ExecuteCommandAndValidate(fmt.Sprintf("oc get mc %s -o json", mcName), commandTimeout, context, func() { + commandErr = fmt.Errorf("failed to get json machineconfig %s", mcName) }) + if commandErr != nil { + return machineConfig{}, commandErr + } + + var mc machineConfig + err := json.Unmarshal([]byte(mcJSON), &mc) + if err != nil { + return machineConfig{}, fmt.Errorf("failed to unmarshal (err: %v)", err) + } + + return mc, nil } -func testTainted() { - if common.IsMinikube() { - return +// getMcSystemdUnitsHugepagesConfig gets the hugepages information from machineconfig's systemd units. +func getMcSystemdUnitsHugepagesConfig(mc *machineConfig) (hugepages numaHugePagesPerSize, err error) { + const UnitContentsRegexMatchLen = 4 + hugepages = numaHugePagesPerSize{} + + r := regexp.MustCompile(`(?ms)HUGEPAGES_COUNT=(\d+).*HUGEPAGES_SIZE=(\d+).*NUMA_NODE=(\d+)`) + for _, unit := range mc.Spec.Config.Systemd.Units { + unit.Name = strings.Trim(unit.Name, "\"") + if !strings.Contains(unit.Name, "hugepages-allocation") { + continue + } + unit.Contents = strings.Trim(unit.Contents, "\"") + values := r.FindStringSubmatch(unit.Contents) + if len(values) < UnitContentsRegexMatchLen { + return numaHugePagesPerSize{}, fmt.Errorf("unable to get hugepages values from mc (contents=%s)", unit.Contents) + } + + numaNode, _ := strconv.Atoi(values[3]) + hpSize, _ := strconv.Atoi(values[2]) + hpCount, _ := strconv.Atoi(values[1]) + + hugepagesCfg := hugePagesConfig{ + hugepagesCount: hpCount, + hugepagesSize: hpSize, + } + + if numaHugepagesCfg, exists := hugepages[numaNode]; exists { + numaHugepagesCfg = append(numaHugepagesCfg, hugepagesCfg) + hugepages[numaNode] = numaHugepagesCfg + } else { + hugepages[numaNode] = []hugePagesConfig{hugepagesCfg} + } } - var nodeNames []string - ginkgo.When("Testing tainted nodes in cluster", func() { - ginkgo.It("Should return list of node names", func() { - context := common.GetContext() - tester := nodenames.NewNodeNames(common.DefaultTimeout, nil) - test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - testResult, err := test.Run() - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) - gomega.Expect(err).To(gomega.BeNil()) - nodeNames = tester.GetNodeNames() - gomega.Expect(nodeNames).NotTo(gomega.BeNil()) - }) - ginkgo.It("Should not have tainted nodes", func() { - defer results.RecordResult(identifiers.TestNonTaintedNodeKernelsIdentifier) - if len(nodeNames) == 0 { - ginkgo.Skip("Can't test tainted nodes when list of nodes is empty. Please check previous tests.") + if len(hugepages) > 0 { + log.Infof("Machineconfig's systemd.units hugepages: %v", hugepages) + } else { + log.Infof("No hugepages found in machineconfig system.units") + } + + return hugepages, nil +} + +// testNodeHugepagesWithMcSystemd compares the node's hugepages values against the mc's systemd units ones. +func testNodeHugepagesWithMcSystemd(nodeName string, nodeNumaHugePages, mcSystemdHugepages numaHugePagesPerSize) (bool, error) { + // Iterate through mc's numas and make sure they exist and have the same sizes and values in the node. + for mcNumaIdx, mcNumaHugepageCfgs := range mcSystemdHugepages { + nodeNumaHugepageCfgs, exists := nodeNumaHugePages[mcNumaIdx] + if !exists { + return false, fmt.Errorf("node %s has no hugepages config for machine config's numa %d", nodeName, mcNumaIdx) + } + + // For this numa, iterate through each of the mc's hugepages sizes and compare with node ones. + for _, mcHugepagesCfg := range mcNumaHugepageCfgs { + configMatching := false + for _, nodeHugepagesCfg := range nodeNumaHugepageCfgs { + if nodeHugepagesCfg.hugepagesSize == mcHugepagesCfg.hugepagesSize && nodeHugepagesCfg.hugepagesCount == mcHugepagesCfg.hugepagesCount { + log.Infof("MC numa=%d, hugepages count:%d, size:%d match node ones: %s", + mcNumaIdx, mcHugepagesCfg.hugepagesCount, mcHugepagesCfg.hugepagesSize, nodeNumaHugePages) + configMatching = true + break + } } - var taintedNodes []string - for _, node := range nodeNames { - context := common.GetContext() - tester := nodetainted.NewNodeTainted(common.DefaultTimeout, node) - test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - testResult, err := test.Run() - gomega.Expect(testResult).NotTo(gomega.Equal(tnf.ERROR)) - gomega.Expect(err).To(gomega.BeNil()) - if testResult == tnf.FAILURE { - taintedNodes = append(taintedNodes, node) + if !configMatching { + return false, fmt.Errorf("MC numa=%d, hugepages (count:%d, size:%d) not matching node ones: %s", + mcNumaIdx, mcHugepagesCfg.hugepagesCount, mcHugepagesCfg.hugepagesSize, nodeNumaHugePages) + } + } + } + + return true, nil +} + +// testNodeHugepagesWithKernelArgs compares node hugepages against kernelArguments config. +// The total count of hugepages of the size defined in the kernelArguments must match the kernArgs' hugepages value. +// For other sizes, the sum should be 0. +func testNodeHugepagesWithKernelArgs(nodeName string, nodeNumaHugePages numaHugePagesPerSize, kernelArgsHugepagesPerSize map[int]int) (bool, error) { + for size, count := range kernelArgsHugepagesPerSize { + total := 0 + for numaIdx, numaHugepages := range nodeNumaHugePages { + found := false + for _, hugepages := range numaHugepages { + if hugepages.hugepagesSize == size { + total += hugepages.hugepagesCount + found = true + break } } - gomega.Expect(taintedNodes).To(gomega.BeNil()) - }) - }) + if !found { + return false, fmt.Errorf("node %s: numa %d has no hugepages of size %d", nodeName, numaIdx, size) + } + } + + if total == count { + log.Infof("kernelArguments' hugepages count:%d, size:%d match total node ones for that size.", count, size) + } else { + return false, fmt.Errorf("node %s: total hugepages of size %d won't match (node count=%d, expected=%d)", + nodeName, size, total, count) + } + } + + return true, nil } -func testHugepages() { - if common.IsMinikube() { - return +func getNodeMachineConfig(nodeName string, machineconfigs map[string]machineConfig, context *interactive.Context) machineConfig { + mcName := strings.Trim(getMcName(context, nodeName), "\"") + log.Infof("Node %s is using machineconfig %s", nodeName, mcName) + + if mc, exists := machineconfigs[mcName]; exists { + log.Infof("MC %s: json already parsed.", mcName) + return mc } - var nodeNames []string - var clusterHugepages, clusterHugepagesz int - ginkgo.When("Testing worker nodes' hugepages configuration", func() { - ginkgo.It("Should return list of worker node names", func() { - context := common.GetContext() - tester := nodenames.NewNodeNames(common.DefaultTimeout, map[string]*string{"node-role.kubernetes.io/worker": nil}) - test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - testResult, err := test.Run() - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) - gomega.Expect(err).To(gomega.BeNil()) - nodeNames = tester.GetNodeNames() - gomega.Expect(nodeNames).NotTo(gomega.BeNil()) - }) - ginkgo.It("Should return cluster's hugepages configuration", func() { - context := common.GetContext() - tester := hugepages.NewHugepages(common.DefaultTimeout) - test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - testResult, err := test.Run() - gomega.Expect(testResult).To(gomega.Equal(tnf.SUCCESS)) - gomega.Expect(err).To(gomega.BeNil()) - clusterHugepages = tester.GetHugepages() - clusterHugepagesz = tester.GetHugepagesz() - }) - ginkgo.It("Should have same configuration as cluster", func() { - defer results.RecordResult(identifiers.TestHugepagesNotManuallyManipulated) - var badNodes []string - for _, node := range nodeNames { - context := common.GetContext() - tester := nodehugepages.NewNodeHugepages(common.DefaultTimeout, node, clusterHugepagesz, clusterHugepages) - test, err := tnf.NewTest(context.GetExpecter(), tester, []reel.Handler{tester}, context.GetErrorChannel()) - gomega.Expect(err).To(gomega.BeNil()) - testResult, err := test.Run() - gomega.Expect(err).To(gomega.BeNil()) - if testResult != tnf.SUCCESS { - badNodes = append(badNodes, node) + + mc, err := getMachineConfig(mcName, context) + if err != nil { + ginkgo.Fail(fmt.Sprintf("Unable to unmarshal mc %s from node %s", mcName, nodeName)) + } + machineconfigs[mcName] = mc + + return mc +} + +func testHugepages(env *config.TestEnvironment) { + testID := identifiers.XformToGinkgoItIdentifier(identifiers.TestHugepagesNotManuallyManipulated) + ginkgo.It(testID, ginkgo.Label(testID), func() { + // Map to save already retrieved and parsed machineconfigs. + machineconfigs := map[string]machineConfig{} + var badNodes []string + + for _, node := range env.NodesUnderTest { + if !node.IsWorker() || !node.HasDebugPod() { + continue + } + + ginkgo.By(fmt.Sprintf("Should get node %s numa's hugepages values.", node.Name)) + nodeNumaHugePages, err := getNodeNumaHugePages(node) + if err != nil { + ginkgo.Fail(fmt.Sprintf("Unable to get node hugepages values from node %s", node.Name)) + } + + // Get and parse node's machineconfig, in case it's not already parsed. + mc := getNodeMachineConfig(node.Name, machineconfigs, env.GetLocalShellContext()) + + ginkgo.By("Should parse machineconfig's kernelArguments and systemd's hugepages units.") + mcSystemdHugepages, err := getMcSystemdUnitsHugepagesConfig(&mc) + if err != nil { + ginkgo.Fail(fmt.Sprintf("Failed to get MC systemd hugepages config. Error: %v", err)) + } + + // KernelArguments params will only be used in case no systemd units were found. + if len(mcSystemdHugepages) == 0 { + ginkgo.By("Comparing MC KernelArguments hugepages info against node values.") + hugepagesPerSize, _ := getMcHugepagesFromMcKernelArguments(&mc) + if pass, err := testNodeHugepagesWithKernelArgs(node.Name, nodeNumaHugePages, hugepagesPerSize); !pass { + log.Error(err) + badNodes = append(badNodes, node.Name) + } + } else { + ginkgo.By("Comparing MC Systemd hugepages info against node values.") + if pass, err := testNodeHugepagesWithMcSystemd(node.Name, nodeNumaHugePages, mcSystemdHugepages); !pass { + log.Error(err) + badNodes = append(badNodes, node.Name) } } - gomega.Expect(badNodes).To(gomega.BeNil()) - }) + } + gomega.Expect(badNodes).To(gomega.BeNil()) }) } diff --git a/test-network-function/platform/suite_test.go b/test-network-function/platform/suite_test.go new file mode 100644 index 000000000..ed9af2b73 --- /dev/null +++ b/test-network-function/platform/suite_test.go @@ -0,0 +1,244 @@ +// Copyright (C) 2020-2022 Red Hat, Inc. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package platform + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/test-network-function/test-network-function/pkg/config/configsections" + "github.com/test-network-function/test-network-function/pkg/tnf/interactive" + "github.com/test-network-function/test-network-function/pkg/utils" +) + +const ( + // Sizes, in KBs. + oneGB = 1024 * 1024 // 1G + twoMB = 2 * 1024 // 2M: also RHEL's default hugepages size +) + +var ( + // No hugepages params + testKernelArgsHpNoParams = []string{"systemd.cpu_affinity=0,1,40,41,20,21,60,61", "nmi_watchdog=0"} + + // Single param + testKernelArgsHpSingleParam1 = []string{"systemd.cpu_affinity=0,1,40,41,20,21,60,61", "hugepages=16", "nmi_watchdog=0"} + testKernelArgsHpSingleParam2 = []string{"systemd.cpu_affinity=0,1,40,41,20,21,60,61", "default_hugepagesz=1G", "nmi_watchdog=0"} + testKernelArgsHpSingleParam3 = []string{"systemd.cpu_affinity=0,1,40,41,20,21,60,61", "default_hugepagesz=2M", "nmi_watchdog=0"} + testKernelArgsHpSingleParam4 = []string{"systemd.cpu_affinity=0,1,40,41,20,21,60,61", "hugepagesz=1G", "nmi_watchdog=0"} + + // Default size + size only + testKernelArgsHpDefParamsOnly = []string{"systemd.cpu_affinity=0,1,40,41,20,21,60,61", "default_hugepagesz=1G", "hugepagesz=1G", "nmi_watchdog=0"} + + // size + count pairs. + testKernelArgsHpPair1 = []string{"systemd.cpu_affinity=0,1,40,41,20,21,60,61", "hugepagesz=1G", "hugepages=16", "nmi_watchdog=0"} + testKernelArgsHpPair2 = []string{"systemd.cpu_affinity=0,1,40,41,20,21,60,61", "hugepagesz=2M", "hugepages=256", "nmi_watchdog=0"} + testKernelArgsHpPair3 = []string{"systemd.cpu_affinity=0,1,40,41,20,21,60,61", "hugepagesz=1G", "hugepages=16", "hugepagesz=2M", "hugepages=256", "nmi_watchdog=0"} + + // default size + (size+count) pairs + testKernelArgsHpDefSizePlusPairs1 = []string{"systemd.cpu_affinity=0,1,40,41,20,21,60,61", "default_hugepagesz=2M", "hugepagesz=1G", "hugepages=16", "nmi_watchdog=0"} + testKernelArgsHpDefSizePlusPairs2 = []string{"systemd.cpu_affinity=0,1,40,41,20,21,60,61", "default_hugepagesz=1G", "hugepagesz=2M", "hugepages=256", "nmi_watchdog=0"} + testKernelArgsHpDefSizePlusPairs3 = []string{"systemd.cpu_affinity=0,1,40,41,20,21,60,61", "default_hugepagesz=1G", "hugepagesz=1G", "hugepages=16", "hugepagesz=2M", "hugepages=256", "nmi_watchdog=0"} +) + +func TestDecodeKernelTaints(t *testing.T) { + taint1, taint1Slice := decodeKernelTaints(2048) + assert.Equal(t, taint1, "workaround for bug in platform firmware applied, ") + assert.Len(t, taint1Slice, 1) + + taint2, taint2Slice := decodeKernelTaints(32769) + assert.Equal(t, taint2, "proprietary module was loaded, kernel has been live patched, ") + assert.Len(t, taint2Slice, 2) +} + +//nolint:funlen +func Test_hugepagesFromKernelArgsFunc(t *testing.T) { + testCases := []struct { + expectedHugepagesDefSize int + expectedHugepagesPerSize map[int]int + kernelArgs []string + }{ + // No params + { + expectedHugepagesDefSize: twoMB, + expectedHugepagesPerSize: map[int]int{twoMB: 0}, + kernelArgs: testKernelArgsHpNoParams, + }, + + // Single params TCs. + { + expectedHugepagesDefSize: twoMB, + expectedHugepagesPerSize: map[int]int{twoMB: 16}, + kernelArgs: testKernelArgsHpSingleParam1, + }, + { + expectedHugepagesDefSize: oneGB, + expectedHugepagesPerSize: map[int]int{oneGB: 0}, + kernelArgs: testKernelArgsHpSingleParam2, + }, + { + expectedHugepagesDefSize: twoMB, + expectedHugepagesPerSize: map[int]int{twoMB: 0}, + kernelArgs: testKernelArgsHpSingleParam3, + }, + { + expectedHugepagesDefSize: twoMB, + expectedHugepagesPerSize: map[int]int{oneGB: 0}, + kernelArgs: testKernelArgsHpSingleParam4, + }, + { + expectedHugepagesDefSize: twoMB, + expectedHugepagesPerSize: map[int]int{oneGB: 16}, + kernelArgs: testKernelArgsHpPair1, + }, + + // Default sizes Tc: + { + expectedHugepagesDefSize: oneGB, + expectedHugepagesPerSize: map[int]int{oneGB: 0}, + kernelArgs: testKernelArgsHpDefParamsOnly, + }, + + // size+count pairs + { + expectedHugepagesDefSize: twoMB, + expectedHugepagesPerSize: map[int]int{oneGB: 16}, + kernelArgs: testKernelArgsHpPair1, + }, + { + expectedHugepagesDefSize: twoMB, + expectedHugepagesPerSize: map[int]int{twoMB: 256}, + kernelArgs: testKernelArgsHpPair2, + }, + { + expectedHugepagesDefSize: twoMB, + expectedHugepagesPerSize: map[int]int{oneGB: 16, twoMB: 256}, + kernelArgs: testKernelArgsHpPair3, + }, + + // default size + (size+count) pairs + { + expectedHugepagesDefSize: twoMB, + expectedHugepagesPerSize: map[int]int{twoMB: 0, oneGB: 16}, + kernelArgs: testKernelArgsHpDefSizePlusPairs1, + }, + { + expectedHugepagesDefSize: oneGB, + expectedHugepagesPerSize: map[int]int{oneGB: 0, twoMB: 256}, + kernelArgs: testKernelArgsHpDefSizePlusPairs2, + }, + { + expectedHugepagesDefSize: oneGB, + expectedHugepagesPerSize: map[int]int{oneGB: 16, twoMB: 256}, + kernelArgs: testKernelArgsHpDefSizePlusPairs3, + }, + } + + mc := machineConfig{} + for _, tc := range testCases { + // Prepare fake MC object: only KernelArguments is needed. + mc.Spec.KernelArguments = tc.kernelArgs + + // Call the function under test. + hugepagesPerSize, defSize := getMcHugepagesFromMcKernelArguments(&mc) + + assert.Equal(t, defSize, tc.expectedHugepagesDefSize) + assert.Equal(t, hugepagesPerSize, tc.expectedHugepagesPerSize) + } +} + +func TestGetOutOfTreeModules(t *testing.T) { + testCases := []struct { + modules []string // Note: We are only using one item in this list for the test. + modinfo map[string]string + expectedTaintedModules []string + }{ + { + modules: []string{ + "test1", + }, + modinfo: map[string]string{ + "test1": ``, + }, + expectedTaintedModules: []string{}, // test1 is 'intree' + }, + { + modules: []string{ + "test2", + }, + modinfo: map[string]string{ + "test2": `O`, + }, + expectedTaintedModules: []string{"test2"}, // test2 is not 'intree' + }, + } + + // Spoof the output from the RunCommandInNode. + // This will allow us to return "InTree" status to the test. + origFunc := utils.RunCommandInNode + defer func() { + utils.RunCommandInNode = origFunc + }() + + for _, tc := range testCases { + utils.RunCommandInNode = func(nodeName string, nodeOc *interactive.Oc, command string, timeout time.Duration) string { + return tc.modinfo[tc.modules[0]] + } + assert.Equal(t, tc.expectedTaintedModules, getOutOfTreeModules(tc.modules, "testnode", nil)) + } +} + +func TestTaintsAccepted(t *testing.T) { + testCases := []struct { + confTaints []configsections.AcceptedKernelTaintsInfo + taintedModules []string + expected bool + }{ + { + confTaints: []configsections.AcceptedKernelTaintsInfo{ + { + Module: "taint1", + }, + }, + taintedModules: []string{ + "taint1", + }, + expected: true, + }, + { + confTaints: []configsections.AcceptedKernelTaintsInfo{}, // no accepted modules + taintedModules: []string{ + "taint1", + }, + expected: false, + }, + { // We have no tainted modules, so the configuration does not matter. + confTaints: []configsections.AcceptedKernelTaintsInfo{ + { + Module: "taint1", + }, + }, + taintedModules: []string{}, + expected: true, + }, + } + + for _, tc := range testCases { + assert.Equal(t, tc.expected, taintsAccepted(tc.confTaints, tc.taintedModules)) + } +} diff --git a/test-network-function/results/results.go b/test-network-function/results/results.go index 399c29b3a..924e0428e 100644 --- a/test-network-function/results/results.go +++ b/test-network-function/results/results.go @@ -18,47 +18,57 @@ package results import ( "fmt" + "strings" - "github.com/onsi/ginkgo" + ginkgoTypes "github.com/onsi/ginkgo/v2/types" "github.com/test-network-function/test-network-function-claim/pkg/claim" - "github.com/test-network-function/test-network-function/pkg/junit" + "github.com/test-network-function/test-network-function/test-network-function/identifiers" ) -var results = map[claim.Identifier][]claim.Result{} +// results is the results map +var results = map[string][]claim.Result{} // RecordResult is a hook provided to save aspects of the ginkgo.GinkgoTestDescription for a given claim.Identifier. // Multiple results for a given identifier are aggregated as an array under the same key. -func RecordResult(identifier claim.Identifier) { - testContext := ginkgo.CurrentGinkgoTestDescription() - results[identifier] = append(results[identifier], claim.Result{ - Duration: int(testContext.Duration.Nanoseconds()), - Filename: testContext.FileName, - IsMeasurement: testContext.IsMeasurement, - LineNumber: testContext.LineNumber, - TestText: testContext.FullTestText, - }) +func RecordResult(report ginkgoTypes.SpecReport) { //nolint:gocritic // From Ginkgo + if claimID, ok := identifiers.TestIDToClaimID[report.LeafNodeText]; ok { + var key string + for _, level := range report.ContainerHierarchyTexts { + levelNoSpace := strings.ReplaceAll(level, " ", "_") + key = key + "-" + levelNoSpace + } + key = strings.TrimLeft(key, "-") + "-" + report.LeafNodeText + testText := identifiers.Catalog[claimID].Description + results[key] = append(results[key], claim.Result{ + Duration: int(report.RunTime.Nanoseconds()), + FailureLocation: report.FailureLocation().String(), + FailureLineContent: report.FailureLocation().ContentsOfLine(), + TestText: testText, + FailureReason: report.FailureMessage(), + State: report.State.String(), + StartTime: report.StartTime.String(), + EndTime: report.EndTime.String(), + CapturedTestOutput: report.CapturedGinkgoWriterOutput, + TestID: &claimID, + }) + } else { + panic(fmt.Sprintf("TestID %s has no corresponding Claim ID", report.LeafNodeText)) + } } // GetReconciledResults is a function added to aggregate a Claim's results. Due to the limitations of // test-network-function-claim's Go Client, results are generalized to map[string]interface{}. This method is needed // to take the results gleaned from JUnit output, and to combine them with the contexts built up by subsequent calls to // RecordResult. The combination of the two forms a Claim's results. -func GetReconciledResults(testResults map[string]junit.TestResult) map[string]interface{} { +func GetReconciledResults() map[string]interface{} { resultMap := make(map[string]interface{}) for key, vals := range results { - // JSON cannot handle complex key types, so this flattens the complex key into a string format. - strKey := fmt.Sprintf("{\"url\":\"%s\",\"version\":\"%s\"}", key.Url, key.Version) // initializes the result map, if necessary - if _, ok := resultMap[strKey]; !ok { - resultMap[strKey] = make([]claim.Result, 0) + if _, ok := resultMap[key]; !ok { + resultMap[key] = make([]claim.Result, 0) } - // a codec which correlates claim.Result, JUnit results (testResults), and builds up the map - // of claim's results. - for _, val := range vals { - val.Passed = testResults[val.TestText].Passed - testFailReason := testResults[val.TestText].FailureReason - val.FailureReason = testFailReason - resultMap[strKey] = append(resultMap[strKey].([]claim.Result), val) + for _, val := range vals { //nolint:gocritic // Only done once at the end + resultMap[key] = append(resultMap[key].([]claim.Result), val) } } return resultMap diff --git a/test-network-function/suite_test.go b/test-network-function/suite_test.go index 661faa9b3..0aadeec51 100644 --- a/test-network-function/suite_test.go +++ b/test-network-function/suite_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -19,15 +19,15 @@ package suite import ( j "encoding/json" "flag" - "io/ioutil" "os" "path/filepath" "testing" "time" + "github.com/test-network-function/test-network-function/test-network-function/diagnostic" "github.com/test-network-function/test-network-function/test-network-function/results" - "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" log "github.com/sirupsen/logrus" "github.com/test-network-function/test-network-function-claim/pkg/claim" @@ -35,16 +35,16 @@ import ( "github.com/test-network-function/test-network-function/pkg/junit" "github.com/test-network-function/test-network-function/pkg/tnf" + utils "github.com/test-network-function/test-network-function/pkg/utils" _ "github.com/test-network-function/test-network-function/test-network-function/accesscontrol" _ "github.com/test-network-function/test-network-function/test-network-function/certification" - "github.com/test-network-function/test-network-function/test-network-function/diagnostic" + "github.com/test-network-function/test-network-function/test-network-function/common" _ "github.com/test-network-function/test-network-function/test-network-function/generic" _ "github.com/test-network-function/test-network-function/test-network-function/lifecycle" _ "github.com/test-network-function/test-network-function/test-network-function/networking" _ "github.com/test-network-function/test-network-function/test-network-function/observability" _ "github.com/test-network-function/test-network-function/test-network-function/operator" _ "github.com/test-network-function/test-network-function/test-network-function/platform" - "github.com/test-network-function/test-network-function/test-network-function/version" ) const ( @@ -67,6 +67,17 @@ const ( var ( claimPath *string junitPath *string + // GitCommit is the latest commit in the current git branch + GitCommit string + // GitRelease is the list of tags (if any) applied to the latest commit + // in the current branch + GitRelease string + // GitPreviousRelease is the last release at the date of the latest commit + // in the current branch + GitPreviousRelease string + // gitDisplayRelease is a string used to hold the text to display + // the version on screen and in the claim file + gitDisplayRelease string ) func init() { @@ -105,71 +116,112 @@ func loadJUnitXMLIntoMap(result map[string]interface{}, junitFilename, key strin } } -// TestTest invokes the CNF Certification Test Suite. +//nolint:funlen // TestTest invokes the CNF Certification Test Suite. func TestTest(t *testing.T) { - // set up input flags and register failure handlers. - flag.Parse() + // When running unit tests, skip the suite + if os.Getenv("UNIT_TEST") != "" { + t.Skip("Skipping test suite when running unit tests") + } + // Checking if output directories exist + utils.CheckFileExists(*claimPath, "claim") + utils.CheckFileExists(*junitPath, "junit") + + ginkgoConfig, _ := ginkgo.GinkgoConfiguration() + log.Infof("Focused test suites : %v", ginkgoConfig.FocusStrings) + log.Infof("TC skip patterns : %v", ginkgoConfig.SkipStrings) + log.Infof("Labels filter : %v", ginkgoConfig.LabelFilter) + + // Diagnostic functions will run also when no focus test suites were provided. + diagnosticMode := len(ginkgoConfig.FocusStrings) == 0 + gomega.RegisterFailHandler(ginkgo.Fail) + common.SetLogFormat() + common.SetLogLevel() + if common.LogLevelTraceEnabled { + config.EnableExpectersVerboseMode() + } + // Display GinkGo Version + log.Info("Ginkgo Version: ", ginkgo.GINKGO_VERSION) + // Display the latest previously released build in case this build is not released + // Otherwise display the build version + if GitRelease == "" { + gitDisplayRelease = "Unreleased build post " + GitPreviousRelease + } else { + gitDisplayRelease = GitRelease + } + log.Info("Version: ", gitDisplayRelease, " ( ", GitCommit, " )") // Initialize the claim with the start time, tnf version, etc. claimRoot := createClaimRoot() claimData := claimRoot.Claim claimData.Configurations = make(map[string]interface{}) claimData.Nodes = make(map[string]interface{}) - incorporateTNFVersion(claimData) - // run the test suite - ginkgo.RunSpecs(t, CnfCertificationTestSuiteName) + if diagnosticMode { + log.Warn("No test suites selected to run. Diagnostic mode enabled.") + // In diagnostic mode, we need to remove labels explicitly before exiting tnf. + defer common.RemoveDebugPods() + } + + // Make sure cluster nodes don't have the debug pod label from previous runs, + // which might happen in case of some aborted/failed tnf runs. + common.RemoveLabelsFromAllNodes() + + // Run first autodiscovery. + config.GetTestEnvironment().LoadAndRefresh() + + // Collect diagnostic data + errs := diagnostic.GetDiagnosticData() + if len(errs) > 0 { + // Should we abort here? + log.Errorf("Errors found while getting diagnostic information from cluster: %v", errs) + } + + incorporateVersions(claimData) + + configurations := marshalConfigurations() + claimData.Nodes = generateNodes() + unmarshalConfigurations(configurations, claimData.Configurations) + + // Run tests specs only if not in diagnostic mode, otherwise all TSs would run. + if !diagnosticMode { + ginkgo.RunSpecs(t, CnfCertificationTestSuiteName) + } + endTime := time.Now() + claimData.Metadata.EndTime = endTime.UTC().Format(dateTimeFormatDirective) - // process the test results from this test suite, the cnf-features-deploy test suite, and any extra informational - // messages. + // Process the test results from the suites, the cnf-features-deploy test suite, + // and any extra informational messages. junitMap := make(map[string]interface{}) cnfCertificationJUnitFilename := filepath.Join(*junitPath, TNFJunitXMLFileName) - loadJUnitXMLIntoMap(junitMap, cnfCertificationJUnitFilename, TNFReportKey) - appendCNFFeatureValidationReportResults(junitPath, junitMap) + + if !diagnosticMode { + loadJUnitXMLIntoMap(junitMap, cnfCertificationJUnitFilename, TNFReportKey) + appendCNFFeatureValidationReportResults(junitPath, junitMap) + } + junitMap[extraInfoKey] = tnf.TestsExtraInfo - // fill out the remaining claim information. + // Append results to claim file data. claimData.RawResults = junitMap - resultMap := generateResultMap(junitMap) - claimData.Results = results.GetReconciledResults(resultMap) - configurations := marshalConfigurations() - claimData.Nodes = generateNodes() - unmarshalConfigurations(configurations, claimData.Configurations) - claimData.Metadata.EndTime = endTime.UTC().Format(dateTimeFormatDirective) + claimData.Results = results.GetReconciledResults() - // marshal the claim and output to file + // Marshal the claim and output to file payload := marshalClaimOutput(claimRoot) claimOutputFile := filepath.Join(*claimPath, claimFileName) writeClaimOutput(claimOutputFile, payload) } -// getTNFVersion gets the TNF version, or fatally fails. -func getTNFVersion() *version.Version { - // Extract the version, which should be placed by the build system. - tnfVersion, err := version.GetVersion() - if err != nil { - log.Fatalf("Couldn't determine the version: %v", err) - } - return tnfVersion -} - // incorporateTNFVersion adds the TNF version to the claim. -func incorporateTNFVersion(claimData *claim.Claim) { +func incorporateVersions(claimData *claim.Claim) { claimData.Versions = &claim.Versions{ - Tnf: getTNFVersion().Tag, - } -} - -// generateResultMap is a conversion utility to generate results. If an error is encountered, than this method fails -// fatally. -func generateResultMap(junitMap map[string]interface{}) map[string]junit.TestResult { - resultMap, err := junit.ExtractTestSuiteResults(junitMap, TNFReportKey) - if err != nil { - log.Fatalf("Could not extract the test suite results: %s", err) + Tnf: gitDisplayRelease, + TnfGitCommit: GitCommit, + OcClient: diagnostic.GetVersionsOcp().Oc, + Ocp: diagnostic.GetVersionsOcp().Ocp, + K8s: diagnostic.GetVersionsOcp().K8s, } - return resultMap } // appendCNFFeatureValidationReportResults is a helper method to add the results of running the cnf-features-deploy @@ -184,7 +236,7 @@ func appendCNFFeatureValidationReportResults(junitPath *string, junitMap map[str // marshalConfigurations creates a byte stream representation of the test configurations. In the event of an error, // this method fatally fails. func marshalConfigurations() []byte { - configurations, err := j.Marshal(config.GetConfigInstance()) + configurations, err := j.Marshal(config.GetTestEnvironment().Config) if err != nil { log.Fatalf("error converting configurations to JSON: %v", err) } @@ -212,7 +264,7 @@ func marshalClaimOutput(claimRoot *claim.Root) []byte { // writeClaimOutput writes the output payload to the claim file. In the event of an error, this method fatally fails. func writeClaimOutput(claimOutputFile string, payload []byte) { - err := ioutil.WriteFile(claimOutputFile, payload, claimFilePermissions) + err := os.WriteFile(claimOutputFile, payload, claimFilePermissions) if err != nil { log.Fatalf("Error writing claim data:\n%s", string(payload)) } @@ -220,13 +272,16 @@ func writeClaimOutput(claimOutputFile string, payload []byte) { func generateNodes() map[string]interface{} { const ( - nodeSummaryField = "nodeSummary" - cniPluginsField = "cniPlugins" - nodesHwInfo = "nodesHwInfo" + nodeSummaryField = "nodeSummary" + cniPluginsField = "cniPlugins" + nodesHwInfo = "nodesHwInfo" + csiDriverInfo = "csiDriver" + initialRuntimeEnv = "InitialRuntimeEnv" ) nodes := map[string]interface{}{} nodes[nodeSummaryField] = diagnostic.GetNodeSummary() nodes[cniPluginsField] = diagnostic.GetCniPlugins() nodes[nodesHwInfo] = diagnostic.GetNodesHwInfo() + nodes[csiDriverInfo] = diagnostic.GetCsiDriverInfo() return nodes } diff --git a/test-network-function/testconfigure.yml b/test-network-function/testconfigure.yml index c40f068b5..c9615e31e 100644 --- a/test-network-function/testconfigure.yml +++ b/test-network-function/testconfigure.yml @@ -16,5 +16,4 @@ operatortest: - name: "OPERATOR_STATUS" tests: - "CSV_INSTALLED" - - "SUBSCRIPTION_INSTALLED" - - "CSV_SCC" \ No newline at end of file + - "CSV_SCC" diff --git a/test-network-function/tnf_config.yml b/test-network-function/tnf_config.yml index 83bed489c..40662c27a 100644 --- a/test-network-function/tnf_config.yml +++ b/test-network-function/tnf_config.yml @@ -1,43 +1,23 @@ +targetNameSpaces: + - name: tnf targetPodLabels: - # populate this list with the spec.selector.matchLabels from all deployment/replica under test - #- namespace: - # name: - # value: -generic: - containersUnderTest: - - namespace: tnf - podName: test - containerName: test - defaultNetworkDevice: eth0 - multusIpAddresses: - - 10.217.0.8 - partnerContainers: - - namespace: tnf - podName: partner - containerName: partner - defaultNetworkDevice: eth0 - multusIpAddresses: - - 10.217.0.29 - testOrchestrator: - namespace: tnf - podName: partner - containerName: partner -operators: - - name: etcdoperator.v0.9.4 - namespace: default - subscriptionName: etcd - autogenerate: false - tests: - - OPERATOR_STATUS -cnfs: - - name: ubuntu - namespace: default - tests: - - PRIVILEGED_POD - - PRIVILEGED_ROLE + - prefix: test-network-function.com + name: generic + value: target +targetCrdFilters: + - nameSuffix: "group1.test.com" + - nameSuffix: "test-network-function.com" certifiedcontainerinfo: - name: nginx-116 # working example repository: rhel8 + tag: 1-112 # optional, "latest" assumed if empty + digest: # if set, takes precedence over tag. e.g. "sha256:aa34453a6417f8f76423ffd2cf874e9c4a1a5451ac872b78dc636ab54a0ebbc3" +checkDiscoveredContainerCertificationStatus: false certifiedoperatorinfo: - - name: etcd-operator - organization: redhat-marketplace \ No newline at end of file + - name: etcd + organization: community-operators # working example +acceptedKernelTaints: + - module: vboxsf + - module: vboxguest +skipHelmChartList: + - name: coredns diff --git a/test-network-function/version/version.go b/test-network-function/version/version.go index c0112d620..cbc6e8629 100644 --- a/test-network-function/version/version.go +++ b/test-network-function/version/version.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Red Hat, Inc. +// Copyright (C) 2020-2022 Red Hat, Inc. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,7 +18,7 @@ package version import ( "encoding/json" - "io/ioutil" + "os" "path" ) @@ -34,7 +34,7 @@ type Version struct { // GetVersion extracts the test-network-function version. func GetVersion() (*Version, error) { - contents, err := ioutil.ReadFile(defaultVersionFile) + contents, err := os.ReadFile(defaultVersionFile) if err != nil { return nil, err } diff --git a/version.json b/version.json index 170080018..aad724399 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "tag": "v2.0.0" + "partner_tag": "v3.3.0" }