diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ede48262..81202ebd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,14 +5,21 @@ name: Test on: push: branches: - - '**' + - "**" paths-ignore: - - 'docs/**' - - '**.md' + - "docs/**" + - "**.md" tags-ignore: - - '**' + - "**" pull_request_target: +concurrency: + group: >- + ${{ github.workflow }}-${{ + github.event.pull_request.number || github.sha + }} + cancel-in-progress: true + jobs: authorize: name: Authorize @@ -39,8 +46,8 @@ jobs: - name: Install Python Requirements uses: actions/setup-python@v4 with: - python-version: '3.10' - cache: 'pip' + python-version: "3.10" + cache: "pip" - name: Perform PIP installs run: pip install ansible-lint==6.16.0 @@ -53,8 +60,100 @@ jobs: ansible-galaxy collection install --force -r ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk/requirements.yml ansible-lint --config-file ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk/.ansible-lint --project-dir ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk/playbooks/ --exclude ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk/playbooks/molecule/ - test: - name: Test + sanity: + name: Run Sanity Tests (Ⓐ${{ matrix.ansible }}) + needs: [lint, authorize] + runs-on: >- + ${{ contains(fromJson( + '["stable-2.9", "stable-2.10", "stable-2.11"]' + ), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }} + strategy: + matrix: + ansible: + - stable-2.16 + # - milestone + steps: + - name: Perform sanity testing with ansible-test + uses: ansible-community/ansible-test-gh-action@release/v1 + with: + collection-root: /ibm/operator_collection_sdk/ + ansible-core-version: stable-2.16 + testing-type: sanity + + unit: + name: Run Unit Tests (Ⓐ${{ matrix.ansible }}) + needs: [lint, authorize] + runs-on: >- + ${{ contains(fromJson( + '["stable-2.9", "stable-2.10", "stable-2.11"]' + ), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }} + strategy: + matrix: + ansible: + - stable-2.16 + steps: + - name: Perform unit testing with ansible-test + uses: ansible-community/ansible-test-gh-action@release/v1 + with: + collection-root: /ibm/operator_collection_sdk/ + ansible-core-version: stable-2.16 + target-python-version: 3.11 + testing-type: units + test-deps: >- + kubernetes.core + + integration: + name: Run Integration Tests + needs: [lint, authorize] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Log into OCP + uses: redhat-actions/oc-login@v1 + with: + openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} + openshift_token: ${{ secrets.OPENSHIFT_TOKEN }} + + - name: Setup Cluster Environment before integration Tests + continue-on-error: true + run: | + chmod +x ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk/tests/integration/integration_setup.sh + bash ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk/tests/integration/integration_setup.sh \ + --collection-root=${GITHUB_WORKSPACE}/ibm/operator_collection_sdk + env: + OCP_NAMESPACE: integration-${GITHUB_REF_NAME}-${RUNNER_OS} + ZOSCB_RELEASE: ibm-zoscb.${{ vars.ZOSCB_RELEASE }} + + - name: Install Python Requirements + uses: actions/setup-python@v4 + with: + python-version: "3.11" + cache: "pip" + + - name: Perform PIP and Collection Installs + run: | + pip install -r ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk/tests/integration/requirements.txt + ansible-galaxy collection install --force -r ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk/tests/integration/requirements.yml + ansible-galaxy collection install ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk --force + + - name: Perform Integration Testing with ansible-test + run: | + cd ~/.ansible/collections/ansible_collections/ibm/operator_collection_sdk + ansible-test integration -v + + - name: Cleanup Cluster Environment after integration Tests + if: always() + run: | + chmod +x ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk/tests/integration/integration_clean.sh + bash ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk/tests/integration/integration_clean.sh \ + --collection-root=${GITHUB_WORKSPACE}/ibm/operator_collection_sdk + env: + OCP_NAMESPACE: integration-${GITHUB_REF_NAME}-${RUNNER_OS} + ZOSCB_RELEASE: ibm-zoscb.${{ vars.ZOSCB_RELEASE }} + + molecule-tests: + name: Run Molecule Tests needs: [lint, authorize] runs-on: ${{matrix.os}} strategy: @@ -75,15 +174,17 @@ jobs: - name: Install Python Requirements uses: actions/setup-python@v4 with: - python-version: '3.10' - cache: 'pip' + python-version: "3.10" + cache: "pip" - name: Perform PIP installs run: pip install -r ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk/requirements.txt - name: Perform collection installs - run: ansible-galaxy collection install ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk --force - + run: | + ansible-galaxy collection install --force -r ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk/requirements.yml + ansible-galaxy collection install ${GITHUB_WORKSPACE}/ibm/operator_collection_sdk --force + - name: Install oc cli uses: redhat-actions/openshift-tools-installer@v1 with: @@ -101,6 +202,6 @@ jobs: export ANSIBLE_PYTHON_INTERPRETER="$(which python)" OCP_NAMESPACE=molecule-${GITHUB_REF_NAME}-${RUNNER_OS} molecule test --all env: - OPENSHIFT_SERVER: ${{ secrets.OPENSHIFT_SERVER }} - OPENSHIFT_TOKEN: ${{ secrets.OPENSHIFT_TOKEN }} - ZOSCB_RELEASE: ibm-zoscb.${{ vars.ZOSCB_RELEASE }} \ No newline at end of file + OPENSHIFT_SERVER: ${{ secrets.OPENSHIFT_SERVER }} + OPENSHIFT_TOKEN: ${{ secrets.OPENSHIFT_TOKEN }} + ZOSCB_RELEASE: ibm-zoscb.${{ vars.ZOSCB_RELEASE }} diff --git a/.gitignore b/.gitignore index 014f58dc..398166a5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ ibm/operator_collection_sdk/local/* examples/racf-operator/local/* .DS_Store .vscode/* -*.idea/ \ No newline at end of file +*.idea/ +__pycache__ +integration_config.yml \ No newline at end of file diff --git a/examples/racf-operator/inventories/inventory.yml b/examples/racf-operator/inventories/inventory.yml index 8e18baf3..b85571f8 100644 --- a/examples/racf-operator/inventories/inventory.yml +++ b/examples/racf-operator/inventories/inventory.yml @@ -14,5 +14,5 @@ source_system: hosts: zos_host: - ansible_host: ***HOST_REMOVED*** + ansible_host: "" ansible_user: omvsadm diff --git a/examples/racf-operator/playbooks/roles/add-zos-user/defaults/main.yml b/examples/racf-operator/playbooks/roles/add-zos-user/defaults/main.yml index d0a3d8d4..d2421928 100644 --- a/examples/racf-operator/playbooks/roles/add-zos-user/defaults/main.yml +++ b/examples/racf-operator/playbooks/roles/add-zos-user/defaults/main.yml @@ -94,7 +94,7 @@ print_pass: true ################################################################################################################## # Specifies the SMTP server to use for sending email, if sending email is desired. ################################################################################################################## -smtp_server: ***HOST_REMOVED*** +smtp_server: "" ################################################################################################################## # Specifies the SMTP port to use for sending email, if sending email is desired. diff --git a/examples/racf-operator/playbooks/roles/remove-zos-user/vars/main.yml b/examples/racf-operator/playbooks/roles/remove-zos-user/vars/main.yml index 15332de2..2c10fddb 100644 --- a/examples/racf-operator/playbooks/roles/remove-zos-user/vars/main.yml +++ b/examples/racf-operator/playbooks/roles/remove-zos-user/vars/main.yml @@ -94,7 +94,7 @@ print_pass: true ################################################################################################################## # Specifies the SMTP server to use for sending email, if sending email is desired. ################################################################################################################## -smtp_server: ***HOST_REMOVED*** +smtp_server: "" ################################################################################################################## # Specifies the SMTP port to use for sending email, if sending email is desired. diff --git a/ibm/operator_collection_sdk/galaxy.yml b/ibm/operator_collection_sdk/galaxy.yml index c66f52a5..add91db9 100644 --- a/ibm/operator_collection_sdk/galaxy.yml +++ b/ibm/operator_collection_sdk/galaxy.yml @@ -70,4 +70,3 @@ build_ignore: # 'omit_default_directives' is a boolean that controls whether the default directives are used. Mutually exclusive # with 'build_ignore' # manifest: null - diff --git a/ibm/operator_collection_sdk/playbooks/molecule/cluster_clean.yml b/ibm/operator_collection_sdk/playbooks/molecule/cluster_clean.yml index d0c3b097..ff413a54 100644 --- a/ibm/operator_collection_sdk/playbooks/molecule/cluster_clean.yml +++ b/ibm/operator_collection_sdk/playbooks/molecule/cluster_clean.yml @@ -5,10 +5,15 @@ --- - name: Clean Test Environment - hosts: all + hosts: localhost connection: local - gather_facts: false + gather_facts: false tasks: + - name: Install dependency # necessary for gh-actions ansible tests + pip: + name: + - kubernetes + - name: Get SubOperatorConfigs kubernetes.core.k8s_info: api_version: zoscb.ibm.com/v2beta2 diff --git a/ibm/operator_collection_sdk/playbooks/molecule/cluster_setup.yml b/ibm/operator_collection_sdk/playbooks/molecule/cluster_setup.yml index ffa71f58..47de5410 100644 --- a/ibm/operator_collection_sdk/playbooks/molecule/cluster_setup.yml +++ b/ibm/operator_collection_sdk/playbooks/molecule/cluster_setup.yml @@ -5,10 +5,15 @@ --- - name: Test Environment Setup - hosts: all + hosts: localhost connection: local - gather_facts: false + gather_facts: false tasks: + - name: Install dependency # necessary for gh-actions ansible tests + pip: + name: + - kubernetes + - name: Create Namespace kubernetes.core.k8s: state: present diff --git a/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements/destroy.yml b/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements/destroy.yml index 3dbff54e..566b310e 100644 --- a/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements/destroy.yml +++ b/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements/destroy.yml @@ -15,4 +15,3 @@ ansible.builtin.file: path: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../examples/racf-operator-test" state: absent - diff --git a/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements/verify.yml b/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements/verify.yml index 246805a7..9b2933e7 100644 --- a/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements/verify.yml +++ b/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements/verify.yml @@ -55,7 +55,7 @@ - name: Assert that requirements.yml contains correct ansible requirements ansible.builtin.assert: that: - - "zos_core_filename in requirementsyml.collections[0].name" + - "zos_core_filename in requirementsyml.collections[0].name" - name: Search for wheelhouse directory ansible.builtin.find: diff --git a/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements_manual_downloads/destroy.yml b/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements_manual_downloads/destroy.yml index 3dbff54e..566b310e 100644 --- a/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements_manual_downloads/destroy.yml +++ b/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements_manual_downloads/destroy.yml @@ -15,4 +15,3 @@ ansible.builtin.file: path: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../examples/racf-operator-test" state: absent - diff --git a/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements_manual_downloads/verify.yml b/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements_manual_downloads/verify.yml index cb477137..52bb6010 100644 --- a/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements_manual_downloads/verify.yml +++ b/ibm/operator_collection_sdk/playbooks/molecule/create_offline_requirements_manual_downloads/verify.yml @@ -55,6 +55,6 @@ - name: Assert that requirements.yml contains correct ansible requirements ansible.builtin.assert: that: - - "zos_core_filename in requirementsyml.collections[0].name" + - "zos_core_filename in requirementsyml.collections[0].name" diff --git a/ibm/operator_collection_sdk/playbooks/molecule/create_operator_config/verify.yml b/ibm/operator_collection_sdk/playbooks/molecule/create_operator_config/verify.yml index 10896b0a..9c92ee39 100644 --- a/ibm/operator_collection_sdk/playbooks/molecule/create_operator_config/verify.yml +++ b/ibm/operator_collection_sdk/playbooks/molecule/create_operator_config/verify.yml @@ -18,7 +18,7 @@ - name: Assert that the galaxy.yml file exists ansible.builtin.assert: that: - - galaxyyml.matched == 1 + - galaxyyml.matched == 1 - name: Read galaxy.yml ansible.builtin.set_fact: @@ -33,7 +33,7 @@ - name: Assert that the operator-config.yml file exists ansible.builtin.assert: that: - - operatorconfig.matched == 1 + - operatorconfig.matched == 1 - name: Read operator-config ansible.builtin.set_fact: @@ -42,18 +42,18 @@ - name: Assert that operator-config.yml contains correct values ansible.builtin.assert: that: - - operatorconfigdata.description == galaxydata.description - - operatorconfigdata.version == galaxydata.version - - operatorconfigdata.displayName is defined - - operatorconfigdata.domain == "{{ collection_namespace | lower | replace('_','-') }}" - - operatorconfigdata.name == "{{ collection_name | lower | replace('_','-') }}" - - operatorconfigdata.resources[0].description is defined - - operatorconfigdata.resources[0].displayName is defined - - operatorconfigdata.resources[0].kind is defined - - operatorconfigdata.resources[0].playbook == 'playbooks/playbook.yml' - - operatorconfigdata.resources[0].vars[0].description is defined - - operatorconfigdata.resources[0].vars[0].displayName is defined - - operatorconfigdata.resources[0].vars[0].name is defined - - operatorconfigdata.resources[0].vars[0].type is defined + - operatorconfigdata.description == galaxydata.description + - operatorconfigdata.version == galaxydata.version + - operatorconfigdata.displayName is defined + - operatorconfigdata.domain == "{{ collection_namespace | lower | replace('_','-') }}" + - operatorconfigdata.name == "{{ collection_name | lower | replace('_','-') }}" + - operatorconfigdata.resources[0].description is defined + - operatorconfigdata.resources[0].displayName is defined + - operatorconfigdata.resources[0].kind is defined + - operatorconfigdata.resources[0].playbook == 'playbooks/playbook.yml' + - operatorconfigdata.resources[0].vars[0].description is defined + - operatorconfigdata.resources[0].vars[0].displayName is defined + - operatorconfigdata.resources[0].vars[0].name is defined + - operatorconfigdata.resources[0].vars[0].type is defined diff --git a/ibm/operator_collection_sdk/playbooks/molecule/create_operator_local/converge.yml b/ibm/operator_collection_sdk/playbooks/molecule/create_operator_local/converge.yml index 8331c616..4380a8e4 100644 --- a/ibm/operator_collection_sdk/playbooks/molecule/create_operator_local/converge.yml +++ b/ibm/operator_collection_sdk/playbooks/molecule/create_operator_local/converge.yml @@ -9,5 +9,3 @@ - name: Converge import_playbook: ../../create_operator.yml - vars: - filepath: "{{ operator_path }}" diff --git a/ibm/operator_collection_sdk/playbooks/molecule/create_operator_local/verify.yml b/ibm/operator_collection_sdk/playbooks/molecule/create_operator_local/verify.yml index 5e814f59..7a6e7f82 100644 --- a/ibm/operator_collection_sdk/playbooks/molecule/create_operator_local/verify.yml +++ b/ibm/operator_collection_sdk/playbooks/molecule/create_operator_local/verify.yml @@ -20,9 +20,9 @@ - name: Assert ZosEndpoint configuration ansible.builtin.assert: that: - - zosendpoint_results.resources[0].status.phase == 'Successful' - - zosendpoint_results.resources[0].status.suboperatorconfigs | length == 1 - - zosendpoint_results.resources[0].spec.endpointType == "local" + - zosendpoint_results.resources[0].status.phase == 'Successful' + - zosendpoint_results.resources[0].status.suboperatorconfigs | length == 1 + - zosendpoint_results.resources[0].spec.endpointType == "local" - name: Validate SubOperatorConfig kubernetes.core.k8s_info: @@ -35,15 +35,15 @@ - name: Assert SubOperatorConfig configuration ansible.builtin.assert: that: - - suboperatorconfig_results.resources[0].spec.credentialType == 'shared' - - suboperatorconfig_results.resources[0].spec.mapping[0].namespace == '{{ ocpnamespace }}' - - suboperatorconfig_results.resources[0].spec.mapping[0].zosendpoints[0].name == '{{ zosendpoint_name }}' - - suboperatorconfig_results.resources[0].spec.mapping[0].zosendpoints[0].credentialName is undefined - - suboperatorconfig_results.resources[0].spec.operatorCollection is defined - - suboperatorconfig_results.resources[0].status.phase == 'Successful' - - suboperatorconfig_results.resources[0].status.clusterserviceversion is defined - - suboperatorconfig_results.resources[0].status.customresourcedefinition | length > 0 - - suboperatorconfig_results.resources[0].status.packages is defined + - suboperatorconfig_results.resources[0].spec.credentialType == 'shared' + - suboperatorconfig_results.resources[0].spec.mapping[0].namespace == '{{ ocpnamespace }}' + - suboperatorconfig_results.resources[0].spec.mapping[0].zosendpoints[0].name == '{{ zosendpoint_name }}' + - suboperatorconfig_results.resources[0].spec.mapping[0].zosendpoints[0].credentialName is undefined + - suboperatorconfig_results.resources[0].spec.operatorCollection is defined + - suboperatorconfig_results.resources[0].status.phase == 'Successful' + - suboperatorconfig_results.resources[0].status.clusterserviceversion is defined + - suboperatorconfig_results.resources[0].status.customresourcedefinition | length > 0 + - suboperatorconfig_results.resources[0].status.packages is defined - name: Validate OperatorCollection kubernetes.core.k8s_info: @@ -56,10 +56,10 @@ - name: Assert OperatorCollection configuration ansible.builtin.assert: that: - - operatorcollection_results.resources[0].spec.skipSignatureVerification is true - - operatorcollection_results.resources[0].status.config.collectionPath is defined - - operatorcollection_results.resources[0].status.phase == 'Successful' - - operatorcollection_results.resources[0].status.subOperatorConfigMappingStatus | length == 1 + - operatorcollection_results.resources[0].spec.skipSignatureVerification is true + - operatorcollection_results.resources[0].status.config.collectionPath is defined + - operatorcollection_results.resources[0].status.phase == 'Successful' + - operatorcollection_results.resources[0].status.subOperatorConfigMappingStatus | length == 1 - name: Validate CSV kubernetes.core.k8s_info: @@ -76,4 +76,4 @@ - name: Assert CSV configuration ansible.builtin.assert: that: - - csv_results.resources[0].status.phase == 'Succeeded' \ No newline at end of file + - csv_results.resources[0].status.phase == 'Succeeded' \ No newline at end of file diff --git a/ibm/operator_collection_sdk/playbooks/molecule/create_redeploy_delete_operator/verify.yml b/ibm/operator_collection_sdk/playbooks/molecule/create_redeploy_delete_operator/verify.yml index d21e2811..4ef6c9a0 100644 --- a/ibm/operator_collection_sdk/playbooks/molecule/create_redeploy_delete_operator/verify.yml +++ b/ibm/operator_collection_sdk/playbooks/molecule/create_redeploy_delete_operator/verify.yml @@ -20,9 +20,9 @@ - name: Assert ZosEndpoint configuration ansible.builtin.assert: that: - - zosendpoint_results.resources[0].status.phase == 'Successful' - - zosendpoint_results.resources[0].status.suboperatorconfigs | length == 1 - - zosendpoint_results.resources[0].spec.endpointType == "remote" + - zosendpoint_results.resources[0].status.phase == 'Successful' + - zosendpoint_results.resources[0].status.suboperatorconfigs | length == 1 + - zosendpoint_results.resources[0].spec.endpointType == "remote" - name: Validate SubOperatorConfig kubernetes.core.k8s_info: @@ -35,15 +35,15 @@ - name: Assert SubOperatorConfig configuration ansible.builtin.assert: that: - - suboperatorconfig_results.resources[0].spec.credentialType == 'personal' - - suboperatorconfig_results.resources[0].spec.mapping[0].namespace == '{{ ocpnamespace }}' - - suboperatorconfig_results.resources[0].spec.mapping[0].zosendpoints[0].name == '{{ zosendpoint_name }}' - - suboperatorconfig_results.resources[0].spec.mapping[0].zosendpoints[0].credentialName is undefined - - suboperatorconfig_results.resources[0].spec.operatorCollection is defined - - suboperatorconfig_results.resources[0].status.phase == 'Successful' - - suboperatorconfig_results.resources[0].status.clusterserviceversion is defined - - suboperatorconfig_results.resources[0].status.customresourcedefinition | length > 0 - - suboperatorconfig_results.resources[0].status.packages is defined + - suboperatorconfig_results.resources[0].spec.credentialType == 'personal' + - suboperatorconfig_results.resources[0].spec.mapping[0].namespace == '{{ ocpnamespace }}' + - suboperatorconfig_results.resources[0].spec.mapping[0].zosendpoints[0].name == '{{ zosendpoint_name }}' + - suboperatorconfig_results.resources[0].spec.mapping[0].zosendpoints[0].credentialName is undefined + - suboperatorconfig_results.resources[0].spec.operatorCollection is defined + - suboperatorconfig_results.resources[0].status.phase == 'Successful' + - suboperatorconfig_results.resources[0].status.clusterserviceversion is defined + - suboperatorconfig_results.resources[0].status.customresourcedefinition | length > 0 + - suboperatorconfig_results.resources[0].status.packages is defined - name: Validate OperatorCollection kubernetes.core.k8s_info: @@ -56,10 +56,10 @@ - name: Assert OperatorCollection configuration ansible.builtin.assert: that: - - operatorcollection_results.resources[0].spec.skipSignatureVerification is true - - operatorcollection_results.resources[0].status.config.collectionPath is defined - - operatorcollection_results.resources[0].status.phase == 'Successful' - - operatorcollection_results.resources[0].status.subOperatorConfigMappingStatus | length == 1 + - operatorcollection_results.resources[0].spec.skipSignatureVerification is true + - operatorcollection_results.resources[0].status.config.collectionPath is defined + - operatorcollection_results.resources[0].status.phase == 'Successful' + - operatorcollection_results.resources[0].status.subOperatorConfigMappingStatus | length == 1 - name: Validate CSV kubernetes.core.k8s_info: @@ -76,7 +76,7 @@ - name: Assert CSV configuration ansible.builtin.assert: that: - - csv_results.resources[0].status.phase == 'Succeeded' + - csv_results.resources[0].status.phase == 'Succeeded' - name: Validate default secret kubernetes.core.k8s_info: @@ -93,7 +93,7 @@ - name: Assert default secret ansible.builtin.assert: that: - - default_secret_results.resources[0].data is defined - - default_secret_results.resources[0].type == "Opaque" - - default_secret_results.resources[0].data.ssh_key != "" - - default_secret_results.resources[0].data.username != "" \ No newline at end of file + - default_secret_results.resources[0].data is defined + - default_secret_results.resources[0].type == "Opaque" + - default_secret_results.resources[0].data.ssh_key != "" + - default_secret_results.resources[0].data.username != "" \ No newline at end of file diff --git a/ibm/operator_collection_sdk/playbooks/molecule/init_collection/destroy.yml b/ibm/operator_collection_sdk/playbooks/molecule/init_collection/destroy.yml index 80cf5bed..fbd3544b 100644 --- a/ibm/operator_collection_sdk/playbooks/molecule/init_collection/destroy.yml +++ b/ibm/operator_collection_sdk/playbooks/molecule/init_collection/destroy.yml @@ -15,4 +15,3 @@ ansible.builtin.file: path: "{{ lookup('env', 'PWD') }}/{{ collection_namespace | lower | replace('-','_') }}" state: absent - diff --git a/ibm/operator_collection_sdk/playbooks/molecule/init_collection/verify.yml b/ibm/operator_collection_sdk/playbooks/molecule/init_collection/verify.yml index b1cb3b72..ba535734 100644 --- a/ibm/operator_collection_sdk/playbooks/molecule/init_collection/verify.yml +++ b/ibm/operator_collection_sdk/playbooks/molecule/init_collection/verify.yml @@ -40,8 +40,8 @@ - name: Assert that requirements.yml contains correct ansible requirements ansible.builtin.assert: that: - - "'operator_sdk.util' in requirementsyml.collections[0].name" - - "'kubernetes.core' in requirementsyml.collections[1].name" + - "'operator_sdk.util' in requirementsyml.collections[0].name" + - "'kubernetes.core' in requirementsyml.collections[1].name" - name: Find playbooks directory ansible.builtin.find: @@ -64,8 +64,8 @@ - name: Assert that requirements.yml contains correct ansible requirements ansible.builtin.assert: that: - - "'all' in playbookyml[0].hosts" - - "'vars/my_vars.yml' in playbookyml[0].vars_files[0]" + - "'all' in playbookyml[0].hosts" + - "'vars/my_vars.yml' in playbookyml[0].vars_files[0]" - name: Find roles directory ansible.builtin.find: @@ -75,7 +75,7 @@ - name: Assert that the roles directory exists ansible.builtin.assert: that: - - roles_path.warnings is not defined + - roles_path.warnings is not defined - name: Find galaxy.yml file ansible.builtin.find: @@ -86,7 +86,7 @@ - name: Assert that the galaxy.yml file exists ansible.builtin.assert: that: - - galaxyyml.matched == 1 + - galaxyyml.matched == 1 - name: Read galaxy.yml ansible.builtin.set_fact: @@ -101,7 +101,7 @@ - name: Assert that the operator-config.yml file exists ansible.builtin.assert: that: - - operatorconfig.matched == 1 + - operatorconfig.matched == 1 - name: Read operator-config ansible.builtin.set_fact: @@ -110,19 +110,19 @@ - name: Assert that operator-config.yml contains correct values ansible.builtin.assert: that: - - operatorconfigdata.description == galaxydata.description - - operatorconfigdata.version == galaxydata.version - - operatorconfigdata.displayName is defined - - operatorconfigdata.domain == "{{ collection_namespace | lower | replace('_','-') }}" - - operatorconfigdata.name == "{{ collection_name | lower | replace('_','-') }}" - - operatorconfigdata.resources[0].description is defined - - operatorconfigdata.resources[0].displayName is defined - - operatorconfigdata.resources[0].kind is defined - - operatorconfigdata.resources[0].playbook == 'playbooks/playbook.yml' - - operatorconfigdata.resources[0].vars[0].description is defined - - operatorconfigdata.resources[0].vars[0].displayName is defined - - operatorconfigdata.resources[0].vars[0].name is defined - - operatorconfigdata.resources[0].vars[0].type is defined + - operatorconfigdata.description == galaxydata.description + - operatorconfigdata.version == galaxydata.version + - operatorconfigdata.displayName is defined + - operatorconfigdata.domain == "{{ collection_namespace | lower | replace('_','-') }}" + - operatorconfigdata.name == "{{ collection_name | lower | replace('_','-') }}" + - operatorconfigdata.resources[0].description is defined + - operatorconfigdata.resources[0].displayName is defined + - operatorconfigdata.resources[0].kind is defined + - operatorconfigdata.resources[0].playbook == 'playbooks/playbook.yml' + - operatorconfigdata.resources[0].vars[0].description is defined + - operatorconfigdata.resources[0].vars[0].displayName is defined + - operatorconfigdata.resources[0].vars[0].name is defined + - operatorconfigdata.resources[0].vars[0].type is defined - name: Find ./.vscode/extensions.json file" ansible.builtin.stat: @@ -140,6 +140,6 @@ - name: Assert that requirements.yml contains correct ansible requirements ansible.builtin.assert: that: - - "'recommendations' in extensionsjson" - - "'ibm.operator-collection-sdk' in extensionsjson.recommendations" - - "'redhat.ansible' in extensionsjson.recommendations" + - "'recommendations' in extensionsjson" + - "'ibm.operator-collection-sdk' in extensionsjson.recommendations" + - "'redhat.ansible' in extensionsjson.recommendations" diff --git a/ibm/operator_collection_sdk/playbooks/molecule/init_collection_offline/destroy.yml b/ibm/operator_collection_sdk/playbooks/molecule/init_collection_offline/destroy.yml index 80cf5bed..fbd3544b 100644 --- a/ibm/operator_collection_sdk/playbooks/molecule/init_collection_offline/destroy.yml +++ b/ibm/operator_collection_sdk/playbooks/molecule/init_collection_offline/destroy.yml @@ -15,4 +15,3 @@ ansible.builtin.file: path: "{{ lookup('env', 'PWD') }}/{{ collection_namespace | lower | replace('-','_') }}" state: absent - diff --git a/ibm/operator_collection_sdk/playbooks/molecule/init_collection_offline/verify.yml b/ibm/operator_collection_sdk/playbooks/molecule/init_collection_offline/verify.yml index d4fac3fc..71094502 100644 --- a/ibm/operator_collection_sdk/playbooks/molecule/init_collection_offline/verify.yml +++ b/ibm/operator_collection_sdk/playbooks/molecule/init_collection_offline/verify.yml @@ -88,8 +88,8 @@ - name: Assert that requirements.yml contains correct ansible requirements ansible.builtin.assert: that: - - "'all' in playbookyml[0].hosts" - - "'vars/my_vars.yml' in playbookyml[0].vars_files[0]" + - "'all' in playbookyml[0].hosts" + - "'vars/my_vars.yml' in playbookyml[0].vars_files[0]" - name: Find roles directory ansible.builtin.find: @@ -99,7 +99,7 @@ - name: Assert that the roles directory exists ansible.builtin.assert: that: - - roles_path.warnings is not defined + - roles_path.warnings is not defined - name: Find galaxy.yml file ansible.builtin.find: @@ -110,7 +110,7 @@ - name: Assert that the galaxy.yml file exists ansible.builtin.assert: that: - - galaxyyml.matched == 1 + - galaxyyml.matched == 1 - name: Read galaxy.yml ansible.builtin.set_fact: @@ -125,7 +125,7 @@ - name: Assert that the operator-config.yml file exists ansible.builtin.assert: that: - - operatorconfig.matched == 1 + - operatorconfig.matched == 1 - name: Read operator-config ansible.builtin.set_fact: @@ -134,19 +134,19 @@ - name: Assert that operator-config.yml contains correct values ansible.builtin.assert: that: - - operatorconfigdata.description == galaxydata.description - - operatorconfigdata.version == galaxydata.version - - operatorconfigdata.displayName is defined - - operatorconfigdata.domain == "{{ collection_namespace | lower | replace('_','-') }}" - - operatorconfigdata.name == "{{ collection_name | lower | replace('_','-') }}" - - operatorconfigdata.resources[0].description is defined - - operatorconfigdata.resources[0].displayName is defined - - operatorconfigdata.resources[0].kind is defined - - operatorconfigdata.resources[0].playbook == 'playbooks/playbook.yml' - - operatorconfigdata.resources[0].vars[0].description is defined - - operatorconfigdata.resources[0].vars[0].displayName is defined - - operatorconfigdata.resources[0].vars[0].name is defined - - operatorconfigdata.resources[0].vars[0].type is defined + - operatorconfigdata.description == galaxydata.description + - operatorconfigdata.version == galaxydata.version + - operatorconfigdata.displayName is defined + - operatorconfigdata.domain == "{{ collection_namespace | lower | replace('_','-') }}" + - operatorconfigdata.name == "{{ collection_name | lower | replace('_','-') }}" + - operatorconfigdata.resources[0].description is defined + - operatorconfigdata.resources[0].displayName is defined + - operatorconfigdata.resources[0].kind is defined + - operatorconfigdata.resources[0].playbook == 'playbooks/playbook.yml' + - operatorconfigdata.resources[0].vars[0].description is defined + - operatorconfigdata.resources[0].vars[0].displayName is defined + - operatorconfigdata.resources[0].vars[0].name is defined + - operatorconfigdata.resources[0].vars[0].type is defined - name: Find ./.vscode/extensions.json file" ansible.builtin.stat: @@ -164,6 +164,6 @@ - name: Assert that requirements.yml contains correct ansible requirements ansible.builtin.assert: that: - - "'recommendations' in extensionsjson" - - "'ibm.operator-collection-sdk' in extensionsjson.recommendations" - - "'redhat.ansible' in extensionsjson.recommendations" + - "'recommendations' in extensionsjson" + - "'ibm.operator-collection-sdk' in extensionsjson.recommendations" + - "'redhat.ansible' in extensionsjson.recommendations" diff --git a/ibm/operator_collection_sdk/playbooks/redeploy_operator.yml b/ibm/operator_collection_sdk/playbooks/redeploy_operator.yml index c7d4ddc3..6990fe9d 100644 --- a/ibm/operator_collection_sdk/playbooks/redeploy_operator.yml +++ b/ibm/operator_collection_sdk/playbooks/redeploy_operator.yml @@ -82,7 +82,6 @@ wait: true wait_sleep: 5 wait_timeout: 140 - ignore_errors: true - name: Set OperatorCollection name ansible.builtin.set_fact: diff --git a/ibm/operator_collection_sdk/playbooks/templates/zoscloudbroker.yml.j2 b/ibm/operator_collection_sdk/playbooks/templates/zoscloudbroker.yml.j2 new file mode 100644 index 00000000..210586e1 --- /dev/null +++ b/ibm/operator_collection_sdk/playbooks/templates/zoscloudbroker.yml.j2 @@ -0,0 +1,37 @@ +{# templates/zoscloudbroker.yml.j2 #} + +apiVersion: zoscb.ibm.com/v2beta1 +kind: ZosCloudBroker +metadata: + name: {{name}} + namespace: {{namespace}} + labels: {{ labels }} +spec: + catalogResources: {} + galaxyConfig: + enabled: {{ ansible_galaxy_configuration.enabled }} + galaxyURL: {{ ansible_galaxy_configuration.url }} + license: + accept: {{ accept_license }} + logLevel: {{ log_level }} + managerResources: {} + multiNamespace: {{ multi_namespace_suboperators }} + storage: + {% if storage.configure %} + accessModes: ReadWriteMany + volumeMode: Filesystem + configure: true + enabled: false + size: {{ storage.storage_size }} + storageClassName: {{ storage.storage_class }} + {% else %} + {% if storage.enabled %} + configure: false + enabled: true + pvc: {{ storage.persistent_volume_claim }} + {% else %} + configure: false + enabled: false + {% endif %} + {% endif %} + uiResources: {} \ No newline at end of file diff --git a/ibm/operator_collection_sdk/plugins/doc_fragments/__init__.py b/ibm/operator_collection_sdk/plugins/doc_fragments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ibm/operator_collection_sdk/plugins/module_utils/__init__.py b/ibm/operator_collection_sdk/plugins/module_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ibm/operator_collection_sdk/plugins/module_utils/util.py b/ibm/operator_collection_sdk/plugins/module_utils/util.py new file mode 100644 index 00000000..e2778564 --- /dev/null +++ b/ibm/operator_collection_sdk/plugins/module_utils/util.py @@ -0,0 +1,52 @@ +# (c) Copyright IBM Corp. 2024 +# Apache License, Version 2.0 (see https://opensource.org/licenses/Apache-2.0) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import os +import subprocess +from re import findall +from json import loads + + +def get_collection_root_path(): + error = None + collections_root_path = None + + # check defaults + default_paths = [ + "/usr/share/ansible/collections/ansible_collections", + os.path.expanduser("~/.ansible/collections/ansible_collections") + ] + for path in default_paths: + candidate = os.path.join(path, "ibm", "operator_collection_sdk") + if os.path.exists(candidate): + collections_root_path = candidate + break + + # attempt to derive path from shell + if collections_root_path is None: + try: + command = ["ansible-config dump"] + stdout = subprocess.run(command, shell=True, text=True, capture_output=True, check=True).stdout + + # filter output for COLLECTIONS_PATHS = [*] + match = findall(r"COLLECTIONS_PATHS[^=]*= [^\]]*\]", stdout) + del stdout + + if len(match) > 0: + string_paths = match[0].split("=")[1].strip() + + # valid json must be double quoted for json.loads + possible_paths = loads(string_paths.replace('\'', "\"")) + + for path in possible_paths: + candidate = os.path.join(path, "ansible_collections", "ibm", "operator_collection_sdk") + if os.path.exists(candidate): + collections_root_path = candidate + break + except Exception as e: + error = str(e) + + return collections_root_path, error diff --git a/ibm/operator_collection_sdk/plugins/modules/__init__.py b/ibm/operator_collection_sdk/plugins/modules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ibm/operator_collection_sdk/plugins/modules/oc_operatorcollection.py b/ibm/operator_collection_sdk/plugins/modules/oc_operatorcollection.py index 2f12fc4a..ece4c815 100644 --- a/ibm/operator_collection_sdk/plugins/modules/oc_operatorcollection.py +++ b/ibm/operator_collection_sdk/plugins/modules/oc_operatorcollection.py @@ -132,7 +132,7 @@ import os from kubernetes import config, client except ImportError as e: - DEPENDENCY_IMPORT_ERROR = f"Failed to import dependency: {e}" + DEPENDENCY_IMPORT_ERROR = "Failed to import dependency: {}".format(e) def run_operatorcollection_module(): @@ -280,7 +280,7 @@ def extract_and_grep(localpath): return filename, "HII" except Exception as e: - return "", f"Error extracting operator-config yaml: {e}" + return "", "Error extracting operator-config yaml: {}".format(e) def fetch_manager_pod_name_and_copy_collection_to_manager(namespace, localpath, name, version): @@ -309,13 +309,12 @@ def fetch_manager_pod_name_and_copy_collection_to_manager(namespace, localpath, oc_command = "oc cp " + localpath + " " + remote_dir result = subprocess.run(oc_command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') except subprocess.CalledProcessError as e: - return f"Error copying tar file to pod: {e}" + return "Error copying tar file to pod: {}".format(e) - # print(f"Successfully copied {localpath} to {pod_name} at {remote_dir}") return None except Exception as e: - return f"Error fetching manager pod info: {e}" + return "Error fetching manager pod info: {}".format(e) return None diff --git a/ibm/operator_collection_sdk/plugins/modules/oc_suboperatorconfig.py b/ibm/operator_collection_sdk/plugins/modules/oc_suboperatorconfig.py index 97236abe..5a1f9a37 100644 --- a/ibm/operator_collection_sdk/plugins/modules/oc_suboperatorconfig.py +++ b/ibm/operator_collection_sdk/plugins/modules/oc_suboperatorconfig.py @@ -130,7 +130,7 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.runner import run_module from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import CoreException except ImportError as e: - DEPENDENCY_IMPORT_ERROR = f"Failed to import dependency: {e}" + DEPENDENCY_IMPORT_ERROR = "Failed to import dependency: {}".format(e) def run_suboperatorconfig_module(): diff --git a/ibm/operator_collection_sdk/plugins/modules/oc_zoscloudbroker.py b/ibm/operator_collection_sdk/plugins/modules/oc_zoscloudbroker.py new file mode 100644 index 00000000..d718e6bf --- /dev/null +++ b/ibm/operator_collection_sdk/plugins/modules/oc_zoscloudbroker.py @@ -0,0 +1,404 @@ +#!/usr/bin/python + +# (c) Copyright IBM Corp. 2024 +# Apache License, Version 2.0 (see https://opensource.org/licenses/Apache-2.0) +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = r""" +--- +module: oc_zoscloudbroker + +short_description: This module creates or delete an IBM® z/OS® Cloud Broker (zoscloudbroker) instance. + +version_added: "2.0.0" + +author: + - Yemi Kelani (@yemi-kelani) + +description: This module creates or delete an IBM® z/OS® Cloud Broker (zoscloudbroker) instance. +options: + state: + description: > + Dictates whether an instance of the zoscloudbroker is created or deleted. + When set to C(present), an instance will be created, if it does not already exist. + If set to C(absent), an existing instance will be deleted. + required: false + type: str + default: "present" + choices: ["absent", "present"] + name: + description: The name of the zoscloudbroker. + required: false + type: str + default: "zoscloudbroker" + namespace: + description: The namespace in which to create the instance. + required: true + type: str + accept_license: + description: > + Boolean indicating if the IBM® z/OS® Cloud Broker license agreement (https://ibm.biz/ibm-zoscb-license) + is accepted (a value of true indicates acceptance). + required: false + type: bool + default: false + labels: + description: Labels i.e. type=ibm-zos-cloud-broker. Key-value pairs where key and value are separated by "=". + required: false + default: [] + type: list + elements: str + multi_namespace_suboperators: + description: Boolean indicating whether to expose suboperators across multiple namespaces. + required: false + type: bool + default: true + aliases: ["multi_namespace"] + log_level: + description: Deployment log level. + required: false + type: str + default: "debug" + choices: ["info", "debug", "trace"] + ansible_galaxy_configuration: + description: Configure Ansible Galaxy Server settings. + type: dict + default: {'enabled': 'true', 'url': 'https://galaxy.ansible.com'} + aliases: ["galaxyConfig", "galaxy_config", "galaxy_configuration"] + suboptions: + enabled: + description: Enables Ansible Galaxy integration. Please disable when running in an air-gapped environment. + required: false + type: bool + default: true + url: + description: Specify the URL for Ansible Galaxy. + required: false + type: str + default: "https://galaxy.ansible.com" + storage: + description: > + Persistent Storage is recommended to enable the persistence of imported Ansible Collections. This may be + required in clusters with network firewall configurations. For more information, please refer to + https://ibm.biz/ibm-zoscb-storage. + required: false + type: dict + suboptions: + configure: + description: > + If set to true, create a new Persistent Volume Claim otherwise use existing PVC i.e. + (PVC enabled is True). + required: false + type: bool + volume_access_mode: + description: AccessModes contains the desired access modes the volume should have (ignored if using an existing PVC). + required: false + type: str + default: "ReadWriteMany" + choices: ["ReadWriteMany"] + storage_size: + description: Size represents the storage size (ignored if using an existing PVC). + required: false + type: str + default: "5Gi" + storage_class: + description: Name of the StorageClass required by the claim (ignored if using an existing PVC). + required: false + type: str + volume_mode: + description: VolumeMode defines what type of volume is required by the claim (ignored if using an existing PVC). + required: false + type: str + default: "Filesystem" + choices: ["Filesystem", "filesystem", "FILESYSTEM"] + persistent_volume_claim: + description: Utilize An Existing Persistent Volume Claim (ignored if configuring a new PVC). + required: false + type: str + aliases: ["pvc", "volume_claim"] + +# extends_documentation_fragment: +# - ibm.operator_collection_sdk.documentaion +""" + +EXAMPLES = r""" +- name: Create ZosCloudBroker + ibm.operator_collection_sdk.oc_zoscloudbroker: + state: present + namespace: yemi-test + accept_license: true + multi_namespace: true + log_level: trace + storage: + configure: true + storage_class: rook-cephfs + register: zcb_results + +- name: Create ZosCloudBroker + ibm.operator_collection_sdk.oc_zoscloudbroker: + state: present + name: zoscloudbroker + namespace: yemi-test + accept_license: true + multi_namespace: true + log_level: debug + labels: + - test=true + - namespace=yemi-test + storage: + enabled: true + pvc: zoscloudbroker + +- name: Create ZosCloudBroker + ibm.operator_collection_sdk.oc_zoscloudbroker: + state: present + namespace: yemi-test-2 + accept_license: true + storage: + enabled: true + pvc: zoscloudbroker + register: zcb_results + +- name: Delete ZosCloudBroker + ibm.operator_collection_sdk.oc_zoscloudbroker: + state: absent + name: zoscloudbroker + namespace: yemi-test-2 + register: zcb_results +""" + +RETURN = r""" +changed: + description: Boolean indicating whether target was modified. + returned: always + type: bool +error: + description: Boolean indicating whether the module errored. + returned: always + type: bool +result: + description: The Custom Resource Definition returned by the performed operation. + returned: success + type: complex + contains: + api_version: + description: Version schema. + returned: success + type: str + sample: v1 + kind: + description: The resource type this object is. + returned: success + type: str + sample: Status + status: + description: The status of the request. + returned: success + type: str + sample: Success + metadata: + description: Target resource metadata. + returned: success + type: complex + details: + description: Information about the target resource. + returned: success + type: complex + contains: + group: + description: API group. + returned: success + type: str + sample: zoscb.ibm.com + kind: + description: Custom resource kind. + returned: success + type: str + sample: zoscloudbrokers + name: + description: Custom resource name. + returned: success + type: str + sample: zoscloudbroker + uid: + description: Resource UID. + returned: success + type: str + sample: 1f922d43-ce6f-42fd-9290-75f2a274a6d2 +""" + +from ansible.module_utils.basic import AnsibleModule + +DEPENDENCY_IMPORT_ERROR = None + +try: + import os + import re + import copy + from jinja2 import Environment, FileSystemLoader + from ..module_utils.util import get_collection_root_path + from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import AnsibleK8SModule + from ansible_collections.kubernetes.core.plugins.module_utils.k8s.runner import run_module + from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import CoreException +except ImportError as e: + DEPENDENCY_IMPORT_ERROR = "Failed to import dependency: {}".format(e) + + +def validate_module_parameters(module, result): + # make a copy of module params + params_copy = copy.deepcopy(module.params) + + if params_copy["state"] == "absent": + return params_copy + + # set default values if necessary + if "configure" not in params_copy["storage"]: + params_copy["storage"]["configure"] = False + if "enabled" not in params_copy["storage"]: + params_copy["storage"]["enabled"] = False + + # validate storage has either configured or enabled or both are false + if params_copy["storage"]["configure"] and params_copy["storage"]["enabled"]: + module.fail_json( + msg="Invalid parameters; Storage parameters 'configure' and 'enabled' cannot both be true.", + **result) + + # validate required params are present + if params_copy["storage"]["configure"]: + required_params = [ + # "volume_access_mode" + # "volume_mode", + # "storage_size", + "storage_class" + ] + for param in required_params: + if param not in params_copy["storage"] \ + or params_copy["storage"][param] == "" \ + or params_copy["storage"][param] is None: + module.fail_json( + msg="Missing required argument '{}'. The following arguments must be suppled when \ + argument storage.configure is True: {}.".format(param, ', '.join(required_params)), + **result) + elif params_copy["storage"]["enabled"]: + if "persistent_volume_claim" not in params_copy["storage"] \ + or params_copy["storage"]["persistent_volume_claim"] == "" \ + or params_copy["storage"]["persistent_volume_claim"] is None: + module.fail_json( + msg="Missing required argument 'persistent_volume_claim'. persistent_volume_claim must be \ + suppled when argument storage.enabled is True.", + **result) + + # validate label format if they exist + if "labels" in params_copy: + labels = {} + for label in params_copy["labels"]: + if re.match("^[^=]*=[^=]*$", label) is None: + module.fail_json( + msg="Recieved label with misshapen form: '{}'. Each label in the list should take the \ + form 'key=value'.".format(label), + **result) + else: + [key, value] = label.split("=") + labels[key] = value + + params_copy["labels"] = labels + + return params_copy + + +def create_and_validate_module(): + module_args = dict( + state=dict(type="str", required=False, default="present", choices=["absent", "present"]), + name=dict(type="str", required=False, default="zoscloudbroker"), + namespace=dict(type="str", required=True), + accept_license=dict(type="bool", required=False, default=False), + labels=dict(type="list", elements="str", required=False, default=[]), + multi_namespace_suboperators=dict(type="bool", required=False, default=True, aliases=["multi_namespace"]), + log_level=dict(type="str", required=False, default="debug", choices=["info", "debug", "trace"]), + ansible_galaxy_configuration=dict( + type="dict", + required=False, + default=dict(enabled="true", url="https://galaxy.ansible.com"), + aliases=["galaxy_configuration", "galaxyConfig", "galaxy_config"], + options=dict( + enabled=dict(type="bool", required=False, default=True), + url=dict(type="str", required=False, default="https://galaxy.ansible.com"), + ) + ), + storage=dict( + type="dict", + required=False, + options=dict( + configure=dict(type="bool", required=False), + enabled=dict(type="bool", required=False), + volume_access_mode=dict(type="str", default="ReadWriteMany", choices=["ReadWriteMany"]), + storage_size=dict(type="str", required=False, default="5Gi"), + storage_class=dict(type="str", required=False), + volume_mode=dict(type="str", required=False, default="Filesystem", choices=["Filesystem", "filesystem", "FILESYSTEM"]), + persistent_volume_claim=dict(type="str", required=False, aliases=["pvc", "volume_claim"]) + ) + ) + ) + + if DEPENDENCY_IMPORT_ERROR is not None: + module = AnsibleModule(argument_spec=module_args) + module.fail_json(msg=DEPENDENCY_IMPORT_ERROR) + + module = AnsibleK8SModule( + module_class=AnsibleModule, + argument_spec=module_args, + supports_check_mode=True, + required_if=[ + ["state", "present", ["accept_license", "storage"], True], + ["state", "absent", ["name"], True] + ] + ) + + result = dict( + changed=False, # if this module effectively modified the target + error=False + ) + + validated_params = validate_module_parameters(module=module, result=result) + + return module, result, validated_params + + +def run_zoscloudbroker_module(module, result, validated_params): + # if in only check mode, return the state with no modifications + if module.check_mode: + module.exit_json(**result) + + # load template and render CRD + collections_root_path, error = get_collection_root_path() + if error is not None: + module.fail_json(msg="Failed to locate the ibm.operator_collection_sdk collection in paths \ + specified under COLLECTIONS_PATHS in the ansible.cfg: {}".format(error)) + + template_path = os.path.join(collections_root_path, "playbooks", "templates") + environment = Environment(loader=FileSystemLoader(searchpath=template_path)) + template = environment.get_template("zoscloudbroker.yml.j2") + custom_resource_definition = template.render(validated_params) + module.params["resource_definition"] = custom_resource_definition + + # attempt to modify target + try: + run_module(module) + result["changed"] = True + except CoreException as e: + result["error"] = True + module.fail_from_exception(e) + + # successful module execution, exit and pass results + module.exit_json(**result) + + +def main(): + module, result, validated_params = create_and_validate_module() + run_zoscloudbroker_module(module, result, validated_params) + + +if __name__ == '__main__': + main() diff --git a/ibm/operator_collection_sdk/plugins/modules/oc_zosendpoint.py b/ibm/operator_collection_sdk/plugins/modules/oc_zosendpoint.py index 4e25ecb8..1a47e4e1 100644 --- a/ibm/operator_collection_sdk/plugins/modules/oc_zosendpoint.py +++ b/ibm/operator_collection_sdk/plugins/modules/oc_zosendpoint.py @@ -124,7 +124,7 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.runner import run_module from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import CoreException except ImportError as e: - DEPENDENCY_IMPORT_ERROR = f"Failed to import dependency: {e}" + DEPENDENCY_IMPORT_ERROR = "Failed to import dependency: {}".format(e) def run_zosendpoint_module(): diff --git a/ibm/operator_collection_sdk/roles/upload_collection_to_manager/tasks/set_vars.yml b/ibm/operator_collection_sdk/roles/upload_collection_to_manager/tasks/set_vars.yml index 749447f9..8a7511b1 100644 --- a/ibm/operator_collection_sdk/roles/upload_collection_to_manager/tasks/set_vars.yml +++ b/ibm/operator_collection_sdk/roles/upload_collection_to_manager/tasks/set_vars.yml @@ -42,4 +42,3 @@ operator_name: "{{ operator_config.name | lower }}" operator_version: "{{ operator_config.version }}" when: operator_name is undefined or operator_version is undefined - diff --git a/ibm/operator_collection_sdk/tests/README.md b/ibm/operator_collection_sdk/tests/README.md new file mode 100644 index 00000000..8ab024c8 --- /dev/null +++ b/ibm/operator_collection_sdk/tests/README.md @@ -0,0 +1,53 @@ +### Running Tests Locally + +--- + +> This document does not cover molecule testing. + +Test scripts can be run from anywhere within the collection, given the current working directory is `~/**/ibm/operator_collection_sdk/*`. + +By default, the tests will install the codebase/collection locally. This functionality can be configured with the following flags:`-i` / `--install-collection` +which can be set either `true` or `false`, i.e. `--install-collection=false`. + +Usage: + +- `--install-collection=`, default: `true` +- Alias: `-i=` + +**Quick Start:** + +1. Navigate to `~/**/ibm/operator_collection_sdk/*`. +2. Deactivate your virtual environment (if it is active). +3. Run a test script. + +--- + +#### Unit Tests + +To install the collection and run all unit tests: + +```bash +./tests/unit/unit.sh +``` + +--- + +#### Sanity Tests + +To install the collection and run sanity tests: + +```bash +./tests/unit/unit.sh +``` + +--- + +#### Integration Tests + +To install the collection and run all Integration tests: + +```bash +./tests/integration/integration.sh +``` + +Note: This test will set up and teardown a cluster environment too, so you must be logged in and connected to a cluster. \ No newline at end of file diff --git a/ibm/operator_collection_sdk/tests/integration/integration.sh b/ibm/operator_collection_sdk/tests/integration/integration.sh new file mode 100755 index 00000000..5dcd71d2 --- /dev/null +++ b/ibm/operator_collection_sdk/tests/integration/integration.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash + +# (c) Copyright IBM Corp. 2024 +# Apache License, Version 2.0 (see https://opensource.org/licenses/Apache-2.0) + +function parseCommandLine { + for arg in "$@"; do + case $arg in + -i=*|--install-collection=*) + install_collection=${arg#*=} + break + ;; + -i|--install-collection) + install_collection=true + break + ;; + *) + echo "Unrecognized option: $arg" + echo "Usage: [--install-collection=] (alias: [-i=]), default: true" + exit 1; + ;; + esac + done + + if [[ $(echo "${install_collection}" | tr '[:upper:]' '[:lower:]') == "true" ]]; then + install_collection=true + else + install_collection=false + fi +} + + +install_collection=true +parseCommandLine "$@" + + +# get collection root path +cwd=$(pwd) +collection_path="" +if [[ ${cwd} =~ "ibm/operator_collection_sdk" ]]; then + collection_path=$(echo "${cwd}" | sed -e "s/ibm\/operator_collection_sdk.*$/ibm\/operator_collection_sdk/") +else + echo -e "\n\033[1;31m Expected script to be run from somewhere within '~/**/ibm/operator_collection_sdk/*' instead got '${cwd}'\033[00m" + exit 1 +fi + +# Install collection locally if flag is set +if [[ "${install_collection}" == "true" ]]; then + printf "\n\033[1;32m Installing collection locally... \033[00m" + ansible-galaxy collection install "${collection_path}" -f + cd "$cwd" +fi + + +# Populate integration_config.yml file with environment variables +ocpnamespace="" +truncate -s 0 "$collection_path"/tests/integration/integration_config.yml +while read -r line; +do + if [[ "$line" =~ "#" ]]; then + continue + fi + eval 'echo "'"$line"'" >> "'"$collection_path"'"/tests/integration/integration_config.yml' + + # determine ocpnamespace + if [[ "$line" =~ "ocp_namespace" ]]; then + l=$(eval 'echo "'"$line"'"') + + # isolate namespace | trim spaces | make lowercase | replace non-alphanumeric with "-" + ocpnamespace=$(echo "$l" | sed -e 's/^.*:[[:space:]]*//' | sed 's/[[:space:]]*$//g' | tr '[:upper:]' '[:lower:]' | sed -e 's/[^-a-zA-Z0-9]/-/g') + echo 'ocpnamespace: "'"$ocpnamespace"'"' >> "$collection_path"/tests/integration/integration_config.yml + fi +done < "$collection_path/tests/integration/integration_config.yml.template" + + +printf "Setting Up Cluster for Integration Tests...\n" +ANSIBLE_JINJA2_NATIVE=true ansible-playbook "$collection_path"/playbooks/molecule/cluster_setup.yml \ + --extra-vars @"$collection_path"/tests/integration/integration_config.yml + + +# Run Integration Tests +cwd=$(pwd) +exit_status=0 +cd ~/.ansible/collections/ansible_collections/ibm/operator_collection_sdk +ansible-test integration -v --venv +exit_status="$?" +cd "$cwd" + +if [[ ${exit_status} != 0 ]] \ +&& [[ $(echo "$VIRTUAL_ENV" | awk '{print length}') != 0 ]] \ +|| [[ -n "$VIRTUAL_ENV" ]]; then + echo -e "\n\033[1;31m Hint: Is your virtual environment still on? If so, try turning it off (deactivate). \033[00m" + echo -e '\033[1;31m "'"$VIRTUAL_ENV"'" \033[00m\n' + exit "$exit_status" +fi + + +echo "Tearing Down Cluster Environment after Integration Tests..." +ANSIBLE_JINJA2_NATIVE=true ansible-playbook "$collection_path"/playbooks/molecule/cluster_clean.yml \ + --extra-vars @"$collection_path"/tests/integration/integration_config.yml + +_exit_status="$?" +if [[ ${_exit_status} != 0 ]]; then + echo -e "\n\033[1;31m Failed to tear down cluster environment after Integration Tests! Review logs. \033[00m" +fi + +exit "$exit_status" \ No newline at end of file diff --git a/ibm/operator_collection_sdk/tests/integration/integration_clean.sh b/ibm/operator_collection_sdk/tests/integration/integration_clean.sh new file mode 100755 index 00000000..2d99139e --- /dev/null +++ b/ibm/operator_collection_sdk/tests/integration/integration_clean.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +# (c) Copyright IBM Corp. 2024 +# Apache License, Version 2.0 (see https://opensource.org/licenses/Apache-2.0) + +# get collection root path +function parseCommandLine { + for arg in "$@"; do + case $arg in + --collection-root=*) + collection_root=${arg#*=} + break + ;; + *) + echo "Unrecognized option: $arg" + echo "Usage: [--collection-root=], default: ''" + exit 1; + ;; + esac + done + + if [[ "$collection_root" == "" ]]; then + echo -e "\n\033[1;31m Error: Must supply path to --collection-root, i.e. '--collection-root=~/**/ibm/operator_collection_sdk' \033[00m" + exit 1 + fi +} + +parseCommandLine "$@" + +# Populate integration_config.yml file with environment variables +ocpnamespace="" +truncate -s 0 "$collection_root"/tests/integration/integration_config.yml +while read -r line; +do + if [[ "$line" =~ "#" ]]; then + continue + fi + eval 'echo "'"$line"'" >> "'"$collection_root"'"/tests/integration/integration_config.yml' + + # determine ocpnamespace + if [[ "$line" =~ "ocp_namespace" ]]; then + l=$(eval 'echo "'"$line"'"') + + # isolate namespace | trim spaces | make lowercase | replace non-alphanumeric with "-" + ocpnamespace=$(echo "$l" | sed -e 's/^.*:[[:space:]]*//' | sed 's/[[:space:]]*$//g' | tr '[:upper:]' '[:lower:]' | sed -e 's/[^-a-zA-Z0-9]/-/g') + echo 'ocpnamespace: "'"$ocpnamespace"'"' >> "$collection_root"/tests/integration/integration_config.yml + fi +done < "$collection_root/tests/integration/integration_config.yml.template" + + +echo "Tearing Down Cluster Environment after Integration Tests..." +ANSIBLE_JINJA2_NATIVE=true ansible-playbook "$collection_root"/playbooks/molecule/cluster_clean.yml \ + --extra-vars @"$collection_root"/tests/integration/integration_config.yml + +exit_status="$?" +if [[ ${exit_status} != 0 ]]; then + echo -e "\n\033[1;31m Failed to tear down cluster environment after Integration Tests! Review logs. \033[00m" +fi + +exit "$exit_status" \ No newline at end of file diff --git a/ibm/operator_collection_sdk/tests/integration/integration_config.yml.template b/ibm/operator_collection_sdk/tests/integration/integration_config.yml.template new file mode 100644 index 00000000..59c38216 --- /dev/null +++ b/ibm/operator_collection_sdk/tests/integration/integration_config.yml.template @@ -0,0 +1,11 @@ +# +# Copyright 2024 IBM Inc. All rights reserved +# SPDX-License-Identifier: Apache2.0 +# +# Leave a trailing newline at the end of this file. +# Do not add secrets/tokens to this file. +--- +ocp_namespace: ${OCP_NAMESPACE:-ocsdk-integration} +zoscb_release: ${ZOSCB_RELEASE:-ibm-zoscb.v2.2.5} +zoscloudbroker_name: zoscloudbroker +subscription_name: ibm-zoscb diff --git a/ibm/operator_collection_sdk/tests/integration/integration_setup.sh b/ibm/operator_collection_sdk/tests/integration/integration_setup.sh new file mode 100755 index 00000000..21c396c9 --- /dev/null +++ b/ibm/operator_collection_sdk/tests/integration/integration_setup.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# (c) Copyright IBM Corp. 2024 +# Apache License, Version 2.0 (see https://opensource.org/licenses/Apache-2.0) + +# get collection root path +function parseCommandLine { + for arg in "$@"; do + case $arg in + --collection-root=*) + collection_root=${arg#*=} + break + ;; + *) + echo "Unrecognized option: $arg" + echo "Usage: [--collection-root=], default: ''" + exit 1; + ;; + esac + done + + if [[ "$collection_root" == "" ]]; then + echo -e "\n\033[1;31m Error: Must supply path to --collection-root, i.e. '--collection-root=~/**/ibm/operator_collection_sdk' \033[00m" + exit 1 + fi +} + +parseCommandLine "$@" + +# Populate integration_config.yml file with environment variables +ocpnamespace="" +truncate -s 0 "$collection_root"/tests/integration/integration_config.yml +while read -r line; +do + if [[ "$line" =~ "#" ]]; then + continue + fi + eval 'echo "'"$line"'" >> "'"$collection_root"'"/tests/integration/integration_config.yml' + + # determine ocpnamespace + if [[ "$line" =~ "ocp_namespace" ]]; then + l=$(eval 'echo "'"$line"'"') + + # isolate namespace | trim spaces | make lowercase | replace non-alphanumeric with "-" + ocpnamespace=$(echo "$l" | sed -e 's/^.*:[[:space:]]*//' | sed 's/[[:space:]]*$//g' | tr '[:upper:]' '[:lower:]' | sed -e 's/[^-a-zA-Z0-9]/-/g') + echo 'ocpnamespace: "'"$ocpnamespace"'"' >> "$collection_root"/tests/integration/integration_config.yml + fi +done < "$collection_root/tests/integration/integration_config.yml.template" + + +printf "Setting Up Cluster for Integration Tests...\n" +ANSIBLE_JINJA2_NATIVE=true ansible-playbook "$collection_root"/playbooks/molecule/cluster_setup.yml \ + --extra-vars @"$collection_root"/tests/integration/integration_config.yml + +exit "$?" \ No newline at end of file diff --git a/ibm/operator_collection_sdk/tests/integration/requirements.txt b/ibm/operator_collection_sdk/tests/integration/requirements.txt new file mode 100644 index 00000000..4b1a0d62 --- /dev/null +++ b/ibm/operator_collection_sdk/tests/integration/requirements.txt @@ -0,0 +1,2 @@ +kubernetes +pyaml \ No newline at end of file diff --git a/ibm/operator_collection_sdk/tests/integration/requirements.yml b/ibm/operator_collection_sdk/tests/integration/requirements.yml new file mode 100644 index 00000000..e1ae55f6 --- /dev/null +++ b/ibm/operator_collection_sdk/tests/integration/requirements.yml @@ -0,0 +1,2 @@ +collections: + - kubernetes.core diff --git a/ibm/operator_collection_sdk/tests/integration/targets/oc_zoscloudbroker/playbook.yml b/ibm/operator_collection_sdk/tests/integration/targets/oc_zoscloudbroker/playbook.yml new file mode 100644 index 00000000..b2f4eb93 --- /dev/null +++ b/ibm/operator_collection_sdk/tests/integration/targets/oc_zoscloudbroker/playbook.yml @@ -0,0 +1,13 @@ +# +# Copyright 2024 IBM Inc. All rights reserved +# SPDX-License-Identifier: Apache2.0 +# + +--- +- hosts: localhost + gather_facts: false + vars_files: + - ../../integration_config.yml + roles: + - role: ../oc_zoscloudbroker + \ No newline at end of file diff --git a/ibm/operator_collection_sdk/tests/integration/targets/oc_zoscloudbroker/runme.sh b/ibm/operator_collection_sdk/tests/integration/targets/oc_zoscloudbroker/runme.sh new file mode 100755 index 00000000..06623c43 --- /dev/null +++ b/ibm/operator_collection_sdk/tests/integration/targets/oc_zoscloudbroker/runme.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +set -eux +ansible-playbook playbook.yml "$@" \ No newline at end of file diff --git a/ibm/operator_collection_sdk/tests/integration/targets/oc_zoscloudbroker/tasks/main.yml b/ibm/operator_collection_sdk/tests/integration/targets/oc_zoscloudbroker/tasks/main.yml new file mode 100644 index 00000000..9e4778d2 --- /dev/null +++ b/ibm/operator_collection_sdk/tests/integration/targets/oc_zoscloudbroker/tasks/main.yml @@ -0,0 +1,94 @@ +# +# Copyright 2024 IBM Inc. All rights reserved +# SPDX-License-Identifier: Apache2.0 +# + +--- +- name: Test oc_zoscloudbroker module + block: + - name: Delete ZosCloudBroker if it exists + kubernetes.core.k8s: + state: absent + api_version: zoscb.ibm.com/v2beta1 + kind: ZosCloudBroker + name: "{{ zoscloudbroker_name }}" + namespace: "{{ ocpnamespace }}" + wait: true + wait_sleep: 5 + wait_timeout: 140 + ignore_errors: true + + - name: Create ZosCloudBroker + ibm.operator_collection_sdk.oc_zoscloudbroker: + state: present + name: "{{ zoscloudbroker_name }}" + namespace: "{{ ocpnamespace }}" + accept_license: true + multi_namespace: true + log_level: trace + labels: + - mylabel=testlabel + galaxy_configuration: + enabled: false + url: "www.example.com" + storage: + enabled: false + configure: false + + - name: Validating ZosCloudBroker installed successfully + kubernetes.core.k8s_info: + api_version: zoscb.ibm.com/v2beta1 + kind: ZosCloudBroker + name: "{{ zoscloudbroker_name }}" + namespace: "{{ ocpnamespace }}" + register: zoscb_results + until: "zoscb_results.resources[0].status.phase == 'Successful'" + retries: 30 + delay: 5 + + - name: Assert ZosCloudBroker fields are accurate + ansible.builtin.assert: + that: + - zoscb_results.resources[0].spec.galaxyConfig.enabled == false + - zoscb_results.resources[0].spec.galaxyConfig.galaxyURL == 'www.example.com' + - zoscb_results.resources[0].metadata.labels.mylabel == 'testlabel' + - zoscb_results.resources[0].spec.logLevel == 'trace' + - zoscb_results.resources[0].spec.storage.enabled == false + - zoscb_results.resources[0].spec.storage.configure == false + + - name: Delete ZosCloudBroker + ibm.operator_collection_sdk.oc_zoscloudbroker: + state: absent + name: "{{ zoscloudbroker_name }}" + namespace: "{{ ocpnamespace }}" + register: zcb_results + + - name: Validate ZosCloudBroker is deleted + kubernetes.core.k8s_info: + api_version: zoscb.ibm.com/v2beta1 + kind: ZosCloudBroker + name: "{{ zoscloudbroker_name }}" + namespace: "{{ ocpnamespace }}" + register: zoscb_results + + - name: Assert ZosCloudBroker is deleted + ansible.builtin.assert: + that: + - zoscb_results.resources | length == 0 + + always: + - name: Delete ZosCloudBoker + kubernetes.core.k8s: + state: absent + api_version: zoscb.ibm.com/v2beta1 + kind: ZosCloudBroker + name: "{{ zoscloudbroker_name }}" + namespace: "{{ ocpnamespace }}" + wait: true + wait_sleep: 5 + wait_timeout: 140 + ignore_errors: true + when: zoscloudbroker_name is defined + + - name: Revert Changes to Cluster Environment + include_tasks: revert_changes.yml diff --git a/ibm/operator_collection_sdk/tests/integration/targets/oc_zoscloudbroker/tasks/revert_changes.yml b/ibm/operator_collection_sdk/tests/integration/targets/oc_zoscloudbroker/tasks/revert_changes.yml new file mode 100644 index 00000000..0aa10f09 --- /dev/null +++ b/ibm/operator_collection_sdk/tests/integration/targets/oc_zoscloudbroker/tasks/revert_changes.yml @@ -0,0 +1,28 @@ +# +# Copyright 2024 IBM Inc. All rights reserved +# SPDX-License-Identifier: Apache2.0 +# + +--- +- block: + - name: Create ZosCloudBoker + kubernetes.core.k8s: + state: present + definition: + apiVersion: zoscb.ibm.com/v2beta1 + kind: ZosCloudBroker + metadata: + name: "{{ zoscloudbroker_name }}" + namespace: "{{ ocpnamespace }}" + spec: + catalogResources: {} + license: + accept: true + logLevel: trace + managerResources: {} + storage: + configure: false + enabled: false + size: 5Gi + volumeMode: Filesystem + uiResources: {} \ No newline at end of file diff --git a/ibm/operator_collection_sdk/tests/sanity/ignore-2.16.txt b/ibm/operator_collection_sdk/tests/sanity/ignore-2.16.txt index 838cbdbb..47791684 100644 --- a/ibm/operator_collection_sdk/tests/sanity/ignore-2.16.txt +++ b/ibm/operator_collection_sdk/tests/sanity/ignore-2.16.txt @@ -1,3 +1,7 @@ +plugins/modules/oc_zoscloudbroker.py validate-modules:parameter-type-not-in-doc # "enabled" var is erroneously flagged +plugins/modules/oc_zoscloudbroker.py validate-modules:undocumented-parameter # "enabled" var is erroneously flagged +plugins/modules/oc_zoscloudbroker.py validate-modules:return-syntax-error # "metadata" return field varies wildly, can't document +plugins/modules/oc_zoscloudbroker.py validate-modules:missing-gplv3-license # ignore gplv3 license, we use apache playbooks/templates/endpoint.yml yamllint!skip playbooks/templates/suboperatorconfig.yml yamllint!skip playbooks/templates/operatorcollection.yml yamllint!skip \ No newline at end of file diff --git a/ibm/operator_collection_sdk/tests/sanity/ignore-2.9.txt b/ibm/operator_collection_sdk/tests/sanity/ignore-2.9.txt new file mode 100644 index 00000000..dd850b5c --- /dev/null +++ b/ibm/operator_collection_sdk/tests/sanity/ignore-2.9.txt @@ -0,0 +1,4 @@ +plugins/modules/oc_zoscloudbroker.py validate-modules:parameter-type-not-in-doc # "enabled" var is erroneously flagged +plugins/modules/oc_zoscloudbroker.py validate-modules:undocumented-parameter # "enabled" var is erroneously flagged +plugins/modules/oc_zoscloudbroker.py validate-modules:return-syntax-error # "metadata" return field varies wildly, can't document +plugins/modules/oc_zoscloudbroker.py validate-modules:missing-gplv3-license # ignore gplv3 license, we use apache \ No newline at end of file diff --git a/ibm/operator_collection_sdk/tests/sanity/sanity.sh b/ibm/operator_collection_sdk/tests/sanity/sanity.sh new file mode 100755 index 00000000..0471f756 --- /dev/null +++ b/ibm/operator_collection_sdk/tests/sanity/sanity.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +# (c) Copyright IBM Corp. 2024 +# Apache License, Version 2.0 (see https://opensource.org/licenses/Apache-2.0) + +function parseCommandLine { + for arg in "$@"; do + case $arg in + -i=*|--install-collection=*) + install_collection=${arg#*=} + break + ;; + -i|--install-collection) + install_collection=true + break + ;; + *) + echo "Unrecognized option: $arg" + echo "Usage: [--install-collection=] (alias: [-i=]), default: true" + exit 1; + ;; + esac + done + + if [[ $(echo "${install_collection}" | tr '[:upper:]' '[:lower:]') == "true" ]]; then + install_collection=true + else + install_collection=false + fi +} + +install_collection=true +parseCommandLine "$@" + +if [[ "${install_collection}" == "true" ]]; then + cwd=$(pwd) + if [[ ${cwd} =~ "ibm/operator_collection_sdk" ]]; then + printf "\n\033[1;32m Installing collection locally... \033[00m" + collection_path=$(echo "${cwd}" | sed -e "s/ibm\/operator_collection_sdk.*$/ibm\/operator_collection_sdk/g") + ansible-galaxy collection install "${collection_path}" -f + else + echo -e "\n\033[1;31m Expected script to be run from somewhere within '~/**/ibm/operator_collection_sdk/*' instead got '${cwd}'\033[00m" + exit 1 + fi +fi + +# Run Sanity Tests +cd ~/.ansible/collections/ansible_collections/ibm/operator_collection_sdk +ansible-test sanity -v --venv +exit_status="$?" + +if [[ ${exit_status} != 0 ]] \ +&& [[ $(echo "$VIRTUAL_ENV" | awk '{print length}') != 0 ]] \ +|| [[ -n "$VIRTUAL_ENV" ]]; then + echo -e "\n\033[1;31m Hint: Is your virtual environment still on? If so, try turning it off (deactivate). \033[00m" + echo -e '\033[1;31m "'"$VIRTUAL_ENV"'" \033[00m\n' + exit "$exit_status" +fi + +exit "$exit_status" \ No newline at end of file diff --git a/ibm/operator_collection_sdk/tests/unit/__init__.py b/ibm/operator_collection_sdk/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ibm/operator_collection_sdk/tests/unit/modules/__init__.py b/ibm/operator_collection_sdk/tests/unit/modules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ibm/operator_collection_sdk/tests/unit/modules/test_oc_zoscloudbroker.py b/ibm/operator_collection_sdk/tests/unit/modules/test_oc_zoscloudbroker.py new file mode 100644 index 00000000..965a64cd --- /dev/null +++ b/ibm/operator_collection_sdk/tests/unit/modules/test_oc_zoscloudbroker.py @@ -0,0 +1,152 @@ +# (c) Copyright IBM Corp. 2024 +# Apache License, Version 2.0 (see https://opensource.org/licenses/Apache-2.0) +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import unittest +from unittest.mock import patch +from ansible.module_utils.basic import AnsibleModule + +from plugins.modules import oc_zoscloudbroker +from tests.unit.utils.mock import ( + error_equal, + fail_json, + exit_json, + AnsibleFailJson, + AnsibleExitJson, + set_module_args, + get_bin_path +) + + +class TestValidateParams(unittest.TestCase): + mock_module_params = dict( + state="present", # present, absent + name="zoscloudbroker", + namespace="fake-namespace", + accept_license=True, + labels=[], + multi_namespace_suboperators=True, + log_level="debug", # info, debug, trace + ansible_galaxy_configuration=dict( + enabled="true", + url="https://galaxy.ansible.com" + ), + storage=dict( + configure=False, + enabled=False, + volume_access_mode="ReadWriteMany", + storage_size="5Gi", + storage_class="fake-storage-class", + volume_mode="Filesystem", + persistent_volume_claim="zoscloudbroker" + ) + ) + + def setUp(self): + self.mock_module_helper = patch.multiple( + AnsibleModule, + exit_json=exit_json, + fail_json=fail_json, + get_bin_path=get_bin_path + ) + + self.mock_module_helper.start() + self.addCleanup(self.mock_module_helper.stop) + + def test_module_fails_when_no_args_passed(self): + with self.assertRaises(AnsibleFailJson): + set_module_args({}) + oc_zoscloudbroker.create_and_validate_module() + + def test_module_fails_when_storage_enabled_and_configure_true(self): + with self.assertRaises(AnsibleFailJson) as result: + args = {**self.mock_module_params} + args["storage"]["enabled"] = True + args["storage"]["configure"] = True + set_module_args(args) + oc_zoscloudbroker.create_and_validate_module() + + assert ( + result.exception.args[0]["msg"] + == "Invalid parameters; Storage parameters 'configure' and 'enabled' cannot both be true." + ) + + def test_module_fails_when_storage_enabled_and_pcv_missing(self): + with self.assertRaises(AnsibleFailJson) as result: + args = {**self.mock_module_params} + args["storage"] = {"enabled": True} + set_module_args(args) + oc_zoscloudbroker.create_and_validate_module() + + assert ( + error_equal( + result.exception.args[0]["msg"], + "Missing required argument 'persistent_volume_claim'. persistent_volume_claim must be \ + suppled when argument storage.enabled is True.")) + + def test_module_created_when_storage_enabled_pcv_present(self): + with self.assertRaises(AnsibleExitJson): + args = {**self.mock_module_params} + args["storage"]["enabled"] = True + set_module_args(args, check_mode=True) + oc_zoscloudbroker.main() + + def test_module_fails_when_storage_configured_required_args_missing(self): + with self.assertRaises(AnsibleFailJson) as result: + args = {**self.mock_module_params} + args["storage"] = {"configure": True} + set_module_args(args) + oc_zoscloudbroker.create_and_validate_module() + + assert ( + error_equal( + result.exception.args[0]["msg"], + "Missing required argument 'storage_class'. The following arguments must be suppled when \ + argument storage.configure is True: storage_class.")) + + def test_module_created_when_storage_configured_required_args_present(self): + with self.assertRaises(AnsibleExitJson): + args = {**self.mock_module_params} + args["storage"]["configure"] = True + set_module_args(args, check_mode=True) + oc_zoscloudbroker.main() + + def test_module_validates_and_constructs_valid_labels_arg(self): + args = {**self.mock_module_params} + args["labels"] = ["TestLabel1=LabelValue1", "TestLabel2=LabelValue2"] + set_module_args(args) + ignore1, ignore2, params = oc_zoscloudbroker.create_and_validate_module() + + labels = params["labels"] + assert ("TestLabel1" in labels and labels["TestLabel1"] == "LabelValue1") + assert ("TestLabel2" in labels and labels["TestLabel2"] == "LabelValue2") + + def test_module_fails_when_labels_arg_invalid(self): + with self.assertRaises(AnsibleFailJson) as result: + misshapen_label = "This=Is=A=Misshapen=Label" + + args = {**self.mock_module_params} + args["labels"] = [misshapen_label] + set_module_args(args) + oc_zoscloudbroker.create_and_validate_module() + + assert ( + error_equal( + result.exception.args[0]["msg"], + "Recieved label with misshapen form: '{}'. Each label in the list should take the \ + form 'key=value'.".format(misshapen_label))) + + with self.assertRaises(AnsibleFailJson) as result: + misshapen_label = "AMisshapenLabel" + + args["labels"] = [misshapen_label] + set_module_args(args) + oc_zoscloudbroker.create_and_validate_module() + + assert ( + error_equal( + result.exception.args[0]["msg"], + "Recieved label with misshapen form: '{}'. Each label in the list should take the \ + form 'key=value'.".format(misshapen_label))) diff --git a/ibm/operator_collection_sdk/tests/unit/requirements.txt b/ibm/operator_collection_sdk/tests/unit/requirements.txt new file mode 100644 index 00000000..d4a4e0aa --- /dev/null +++ b/ibm/operator_collection_sdk/tests/unit/requirements.txt @@ -0,0 +1,3 @@ +pytest +pyyaml +kubernetes \ No newline at end of file diff --git a/ibm/operator_collection_sdk/tests/unit/unit.sh b/ibm/operator_collection_sdk/tests/unit/unit.sh new file mode 100755 index 00000000..b509c8dd --- /dev/null +++ b/ibm/operator_collection_sdk/tests/unit/unit.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +# (c) Copyright IBM Corp. 2024 +# Apache License, Version 2.0 (see https://opensource.org/licenses/Apache-2.0) + +function parseCommandLine { + for arg in "$@"; do + case $arg in + -i=*|--install-collection=*) + install_collection=${arg#*=} + break + ;; + -i|--install-collection) + install_collection=true + break + ;; + *) + echo "Unrecognized option: $arg" + echo "Usage: [--install-collection=] (alias: [-i=]), default: true" + exit 1; + ;; + esac + done + + if [[ $(echo "${install_collection}" | tr '[:upper:]' '[:lower:]') == "true" ]]; then + install_collection=true + else + install_collection=false + fi +} + +install_collection=true +parseCommandLine "$@" + +if [[ "${install_collection}" == "true" ]]; then + cwd=$(pwd) + if [[ ${cwd} =~ "ibm/operator_collection_sdk" ]]; then + printf "\n\033[1;32m Installing collection locally... \033[00m" + collection_path=$(echo "${cwd}" | sed -e "s/ibm\/operator_collection_sdk.*$/ibm\/operator_collection_sdk/g") + ansible-galaxy collection install "${collection_path}" -f + else + echo -e "\n\033[1;31m Expected script to be run from somewhere within '~/**/ibm/operator_collection_sdk/*' instead got '${cwd}'\033[00m" + exit 1 + fi +fi + +# Run Unit Tests +cd ~/.ansible/collections/ansible_collections/ibm/operator_collection_sdk +ansible-test units -v --venv +exit_status="$?" + +if [[ ${exit_status} != 0 ]] \ +&& [[ $(echo "$VIRTUAL_ENV" | awk '{print length}') != 0 ]] \ +|| [[ -n "$VIRTUAL_ENV" ]]; then + echo -e "\n\033[1;31m Hint: Is your virtual environment still on? If so, try turning it off (deactivate). \033[00m" + echo -e '\033[1;31m "'"$VIRTUAL_ENV"'" \033[00m\n' + exit "$exit_status" +fi + +exit "$exit_status" \ No newline at end of file diff --git a/ibm/operator_collection_sdk/tests/unit/utils/__init__.py b/ibm/operator_collection_sdk/tests/unit/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ibm/operator_collection_sdk/tests/unit/utils/mock.py b/ibm/operator_collection_sdk/tests/unit/utils/mock.py new file mode 100644 index 00000000..1685f179 --- /dev/null +++ b/ibm/operator_collection_sdk/tests/unit/utils/mock.py @@ -0,0 +1,49 @@ +import json + +from ansible.module_utils import basic +from ansible.module_utils.common.text.converters import to_bytes + + +def error_equal(error1: str, error2: str) -> bool: + return str(error1).replace(" ", "") == str(error2).replace(" ", "") + + +def set_module_args(args, check_mode=False): + """prepare arguments so that they will be picked up during module creation""" + anisble_module_args = {"ANSIBLE_MODULE_ARGS": args} + if check_mode: + anisble_module_args["ANSIBLE_MODULE_ARGS"]["_ansible_check_mode"] = True + args = json.dumps(anisble_module_args) + basic._ANSIBLE_ARGS = to_bytes(args) + + +class AnsibleExitJson(Exception): + """Exception class to be raised by module.exit_json and caught by the test case""" + pass + + +class AnsibleFailJson(Exception): + """Exception class to be raised by module.fail_json and caught by the test case""" + pass + + +def exit_json(*args, **kwargs): + """function to patch over exit_json; package return data into an exception""" + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + +def fail_json(*args, **kwargs): + """function to patch over fail_json; package return data into an exception""" + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + +def get_bin_path(self, arg, required=False): + """Mock AnsibleModule.get_bin_path""" + if arg.endswith('my_command'): + return '/usr/bin/my_command' + else: + if required: + fail_json(msg='%r not found !' % arg)