├── .gitignore ├── Dockerfile ├── LICENSE ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── action.yml ├── docs ├── .nojekyll ├── README.md ├── _navbar.md ├── _sidebar.md ├── action.md ├── config.md ├── en │ ├── README.md │ ├── _navbar.md │ ├── _sidebar.md │ ├── action.md │ ├── config.md │ ├── installation.md │ ├── quick_start.md │ └── usage.md ├── icon │ └── ROS.svg ├── index.html ├── installation.md ├── quick_start.md └── usage.md ├── entrypoint.sh ├── iact3 ├── __init__.py ├── __main__.py ├── cli.py ├── cli_modules │ ├── __init__.py │ ├── base.py │ ├── cost.py │ ├── delete.py │ ├── list.py │ ├── policy.py │ ├── preview.py │ ├── test.py │ └── validate.py ├── config.py ├── exceptions.py ├── generate_params.py ├── logger.py ├── main.py ├── plugin │ ├── __init__.py │ ├── base_plugin.py │ ├── ecs.py │ ├── error_code.py │ ├── oss.py │ ├── ros.py │ └── vpc.py ├── report │ ├── __init__.py │ ├── generate_reports.py │ └── html.css ├── stack.py ├── termial_print.py ├── testing │ ├── __init__.py │ ├── base.py │ └── ros_stack.py └── util.py ├── iact3_outputs ├── iact3-default-cn-hangzhou-b72443d4-cn-hangzhou.txt ├── iact3-default-cn-hangzhou-eac0248e-cn-hangzhou.txt ├── index.html └── test-failed-cost-result.json ├── requirements-dev.txt ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── common.py ├── data │ ├── .iact3.yml │ ├── ecs_instance.template.json │ ├── failed_config.yml │ ├── failed_cost_config.iact3.yaml │ ├── failed_template.yaml │ ├── failed_test_validate_config.yml │ ├── failed_validate_template.yml │ ├── full_config.yml │ ├── index.html │ ├── invalid_template.yml │ ├── real.iact3.yml │ ├── simple_template.yml │ ├── test_config.iact3.yaml │ ├── test_global_config.yml │ ├── test_project_config.yml │ ├── tf │ │ ├── main.tf │ │ ├── modules │ │ │ └── vpc │ │ │ │ └── main.tf │ │ └── test.template.xxx │ ├── timeout_config.yml │ └── timeout_template.yml ├── plugin │ ├── __init__.py │ ├── test_base.py │ ├── test_ecs.py │ ├── test_oss.py │ ├── test_ros.py │ └── test_vpc.py ├── test_cli.py ├── test_config.py ├── test_cost.py ├── test_generate_param.py ├── test_policy.py ├── test_preview.py ├── test_report.py ├── test_run.py ├── test_terminalprinter.py └── test_validate.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/ 3 | include/ 4 | share/ 5 | lib/ 6 | dist/ 7 | .cache/ 8 | .Python 9 | .venv/ 10 | .idea/ 11 | iact3.egg-info/ 12 | .DS_Store 13 | /tests/data/iact3_outputs/ 14 | alibabacloud_ros_iact3.egg-info/ 15 | /tests/iact3_outputs/ 16 | .tox/ 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | MAINTAINER YUNXIU 3 | # VOLUME /templates ./ 4 | LABEL org.opencontainers.image.title="iact3-action" 5 | LABEL version="1.0" 6 | COPY "entrypoint.sh" "/entrypoint.sh" 7 | COPY "requirements.txt" "/requirements.txt" 8 | COPY "./iact3" "/iact3" 9 | RUN chmod +x /entrypoint.sh 10 | RUN apt-get update && apt-get install -y gcc && apt-get install -y jq 11 | RUN pip install -r /requirements.txt 12 | ENTRYPOINT ["/entrypoint.sh"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt requirements-dev.txt 2 | include requirements-dev.txt 3 | recursive-include iact3/report *.css -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IaC Template Testing Tool 2 | 3 | Iact3(IaC Template Testing Tool) is a tool that tests [Alibaba Cloud ROS(Resource Orchestration Service)](https://www.alibabacloud.com/help/resource-orchestration-service) templates and [Terraform](https://developer.hashicorp.com/terraform). It deploys your template in multiple Alibaba Cloud Regions and generates a report for each region via a simple configuration file. 4 | 5 | # Requirements 6 | Python 3.7+ 7 | 8 | # Installation 9 | 10 | `pip install alibabacloud-ros-iact3` 11 | 12 | # Document 13 | 14 | Fantastic documentation is available at: 15 | [English](https://aliyun.github.io/alibabacloud-ros-tool-iact3/#/en) | 16 | [中文版](https://aliyun.github.io/alibabacloud-ros-tool-iact3). 17 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Iact3 Action' 2 | description: 'GitHub Action for Alibabacloud Ros Tool Iact3' 3 | author: 'yunxiu' 4 | inputs: 5 | access_key_id: 6 | description: 'ALIBABA_CLOUD_ACCESS_KEY_ID' 7 | access_key_secret: 8 | description: 'ALIBABA_CLOUD_ACCESS_KEY_SECRET' 9 | templates: 10 | description: 'test templates path' 11 | required: true 12 | type: 13 | description: 'iact3 subcommand(test/validate)' 14 | required: true 15 | runs: 16 | using: 'docker' 17 | image: 'Dockerfile' -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-ros-tool-iact3/64e93057e82e634cd79ee5c4e2d761ef31fe1e03/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Iact3 是什么 2 | [Iact3](https://github.com/aliyun/alibabacloud-ros-tool-iact3) 的全名是IaC模板测试工具(Infrastructure as Code Template Test Tool),其中IaC(Infrastructure as Code)表示通过代码而不是手动流程来管理和配置基础设施,常见的IaC工具有[Terraform](https://developer.hashicorp.com/terraform)和阿里云[ROS](https://www.alibabacloud.com/help/zh/resource-orchestration-service)等。 3 | 4 | **Iact3**支持同时在多个阿里云地域中使用多个参数测试Terraform和ROS模板,并且为每个测试生成报告。 5 | 6 | ## 支持 7 | [![Feature Request](https://img.shields.io/badge/Open%20Issues-Feature%20Request-green.svg)](https://github.com/aliyun/alibabacloud-ros-tool-iact3/issues/new/choose) 8 | [![Report Bugs](https://img.shields.io/badge/Open%20Issue-Report%20Bug-red.svg)](https://github.com/aliyun/alibabacloud-ros-tool-iact3/issues/new/choose) 9 | 10 | ## GitHub 11 | 12 | [![GitHub stars](https://img.shields.io/github/stars/aliyun/alibabacloud-ros-tool-iact3.svg?style=social&label=Stars)](https://github.com/aliyun/alibabacloud-ros-tool-iact3) 13 | [![GitHub issues](https://img.shields.io/github/issues/aliyun/alibabacloud-ros-tool-iact3.svg)](https://github.com/aliyun/alibabacloud-ros-tool-iact3/issues) 14 | [![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/aliyun/alibabacloud-ros-tool-iact3.svg)](https://github.com/aliyun/alibabacloud-ros-tool-iact3/issues?q=is%3Aissue+is%3Aclosed) 15 | [![GitHub pull requests](https://img.shields.io/github/issues-pr/aliyun/alibabacloud-ros-tool-iact3.svg)](https://github.com/aliyun/alibabacloud-ros-tool-iact3/pulls) 16 | [![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/aliyun/alibabacloud-ros-tool-iact3.svg)](https://github.com/aliyun/alibabacloud-ros-tool-iact3/pulls?q=is%3Apr+is%3Aclosed) 17 | -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | 2 | * [中文](/) 3 | * [en](/en/) -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - [简介](README.md) 2 | - [快速开始](quick_start.md) 3 | - [安装](installation.md) 4 | - [配置](config.md) 5 | - [使用](usage.md) 6 | - [GitHub Action](action.md) 7 | -------------------------------------------------------------------------------- /docs/action.md: -------------------------------------------------------------------------------- 1 | # Iact3 GitHub Action 2 | 3 | Iact3 Action是用于测试[阿里云ROS模版](https://www.alibabacloud.com/help/zh/resource-orchestration-service)的Github Action。使用于阿里云ROS模板仓库ros-templates的Iact3-test Workflow中,用途是使用[Iact3](https://github.com/aliyun/alibabacloud-ros-tool-iact3)工具测试合并至仓库的ROS模板是否合规及是否可以成功部署。 4 | 5 | [ros-templates](https://github.com/aliyun/ros-templates)仓库提供众多阿里云ROS模板的示例和最佳实践资源,包括资源级模板示例、综合模版示例、面向复杂场景的模板最佳实践、基于Transform语法的模板、阿里云文档模板与计算巢最佳实践模板。 6 | 7 | ## 使用方法 8 | ```yaml 9 | - name: Test templates 10 | uses: aliyun/alibabacloud-ros-tool-iact3@master 11 | with: 12 | templates: "template1.xml template2.xml" 13 | access_key_id: ${{ secrets.ACCESS_KEY_ID }} 14 | access_key_secret: ${{ secrets.ACCESS_KEY_SECRET }} 15 | type: "test" 16 | ``` 17 | ## Action 输入 18 | | 名称 | 描述 | 19 | |-------------------|---------------------------| 20 | | templates | 以空格分割的需要测试的模板与配置文件路径 | 21 | | access_key_id | 阿里云账户key_id | 22 | | access_key_secret | 阿里云账户key_secret | 23 | | type | 检测方式 [ validate \| test ] | 24 | 25 | ## Action 输出 26 | * `status` - `success/fail` 代表测试模板是否全部通过Iact3 Action测试 27 | 28 | ## Action测试流程 29 | ### 测试对象 30 | 当输入参数`type`为`validate`时,测试对象为输入参数`templates`中所有的ROS模板。 31 | 32 | 当输入参数`type`为`test`时,测试对象为输入参数`templates`中所有包含的ROS模板,及`templates`中包含的配置文件对应的ROS模板。当某个配置文件对应的ROS模板不存在于仓库时,将跳过对此配置文件和对应模板的测试。 33 | 34 | ### 配置文件 35 | 当输入参数`type`为`test`时,若被测试模板的对应位置有配置文件,则会根据配置文件对模板进行部署测试。模板对应的配置文件必须满足以下条件: 36 | * 配置文件名称需为模板名称,后缀需为`.iact3.[yml|yaml]` 37 | * 配置文件位置固定为在`iact3-config/`目录下和模板同路径位置处(`name.[yml|yaml]` 对应 `iact3-config/name.iact3.[yml|yaml]` ) 38 | * 配置文件中`project`配置项`name`需为`test-{模板名}` 39 | * 配置文件中可以不包含`template_config:template_location`项,如包含,模版路径需使用相对ros-template仓库根目录的相对路径 40 | 41 | ### 测试方式 42 | 当输入参数`type`为`validate`时,使用`iact3 validate`命令校验模板合法性。 43 | 44 | 当输入参数`type`为`test`时,对带有配置文件的模板使用`iact3 test run`命令进行部署测试,其余模板使用`iact3 validate`命令校验合法性。 45 | 46 | ### 测试结果 47 | Iact3 Action对输入参数`templates`中涉及的全部模板和配置文件对应的模板进行测试,如全部模板都通过测试,则输出`status=success`至workflow并返回退出状态码`0`,反之则输出`status=fail`且返回退出状态码`1`。 48 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # 配置文件 2 | 3 | Iact3 可以使用两种配置文件进行测试: 4 | 1. 全局配置文件`~/.iact3.yml` 5 | 2. 项目配置文件 `/.iact3.yml` 6 | 7 | 每种配置文件支持三层配置项:`general`,`project`,`tests`。其中,`tests`配置项是必须的。 8 | 9 | ## general 配置项 10 | `general`配置中可以包含: 11 | - `auth` 阿里云认证配置 12 | ```json 13 | { 14 | "name": "default", 15 | "location": "~/.aliyun/config.json" 16 | } 17 | ``` 18 | 19 | - `oss_config` Oss Bucket配置,包括BucketName、BucketRegion等 20 | ```json 21 | { 22 | "bucket_name": "", 23 | "bucket_region": "", 24 | "object_prefix": "", 25 | "callback_params": { 26 | "callback_url": "", 27 | "callback_host": "", 28 | "callback_body": "", 29 | "callback_body_type": "", 30 | "callback_var_params": "" 31 | } 32 | } 33 | ``` 34 | - `parameters` 要传递给模板的参数键值 35 | ```json 36 | { 37 | "vpc_id": "", 38 | "vsw_id": "" 39 | } 40 | ``` 41 | ## project 配置项 42 | `project` 配置项中可以包含: 43 | - `name`: 项目名称 44 | - `regions`: 阿里云地域列表 45 | - `parameters`: 要传递给模板的参数键值 46 | - `tags`: 标签 47 | - `role_name`: 角色名称 48 | - `template_config`: 模板配置信息 49 | ```json 50 | { 51 | "template_location": "myTemplate/", 52 | "template_url": "oss://xxx", 53 | "template_body": "", 54 | "template_id": "", 55 | "template_version": "" 56 | } 57 | ``` 58 | 59 | ## tests 配置项 60 | `tests` 配置项中可以包含: 61 | - `name`: 项目名称 62 | - `regions`: 阿里云地域列表 63 | - `parameters`: 要传递给模板的参数键值 64 | - `tags`: 标签 65 | - `role_name`: 角色名称 66 | - `template_config`: 模板配置信息 67 | ```json 68 | { 69 | "template_location": "myTemplate/", 70 | "template_url": "oss://xxx", 71 | "template_body": "", 72 | "template_id": "", 73 | "template_version": "" 74 | } 75 | ``` 76 | 77 | ## 优先级 78 | 除`parameters`里的配置外,具有相同键的更具体的配置优先。 79 | > 这种参数处理方式的原理在于,可以在项目之外的系统级别上对值进行覆盖,这样就可以避免将这些参数添加到源代码项目中。像 VPC 详细信息、密钥对或 API 密钥等账户特定的参数可以在每个主机上定义,从而避免将其添加到源代码控制中。 80 | 81 | 例如,当全局配置文件`~/.iact3.yml` 内容如下: 82 | ```yaml 83 | general: 84 | oss_config: 85 | bucket_name: global-bucket 86 | parameters: 87 | KeyPair: my-global-ecs-key-pair 88 | ``` 89 | 项目配置文件如下: 90 | ```yaml 91 | project: 92 | name: my-project 93 | regions: 94 | - cn-hangzhou 95 | oss_config: 96 | bucket_name: project-bucket 97 | tests: 98 | default: 99 | template_config: 100 | template_url: "oss://xxx" 101 | regions: 102 | - cn-beijing 103 | parameters: 104 | KeyPair: my-test-ecs-key-pair 105 | ``` 106 | 最终Iact3测试使用的配置如下所示: 107 | ```yaml 108 | tests: 109 | default: 110 | template_config: 111 | template_url: "oss://xxx" 112 | regions: 113 | - cn-beijing 114 | oss_config: 115 | bucket_name: project-bucket 116 | parameters: 117 | KeyPair: my-test-ecs-key-pair 118 | ``` 119 | 可以注意到,`backet_name`和`regions`取了更具体配置中的值,而`KeyPair`取了更通用配置中的值。 120 | 121 | ## 伪参数 122 | 如果参数是以下2种情况时,可以通过`$[iact3-auto]`伪参数自动获取可用参数. 123 | 1. 参数对应的资源属性支持ROS [GetTemplateParameterConstraints](https://www.alibabacloud.com/help/en/resource-orchestration-service/latest/api-ros-2019-09-10-gettemplateparameterconstraints) 接口。 124 | 2. 名称具有特定含义的参数。 例如,`VpcId`表示虚拟私有云的id,`$[iact3-auto]`会自动在当前账户的当前地域随机获取一个vpcId。 目前支持的有此类参数有: 125 | 1. 名称满足正则`r"(\w*)vpc(_|)id(_|)(\d*)"`的参数,会自动随机获取当前区域的VpcId。 126 | 2. 名称满足正则`r"(\w*)v(_|)switch(_|)id(_|)(\d*)"`的参数,会自动随机获取当前区域的VswitchId。 如果同时有参数名称满足正则`r"(\w*)zone(_|)id(_|)(\d*)"`,则会查询对应可用区的VswitchId。 127 | 3. 名称满足正则`r"(\w*)security(_|)group(_id|id)(_|)(\d*)"`的参数,会自动随机获取当前区域的SecurityGroupId。 128 | 4. 名称满足正则`r"(\w*)name(_|)(\d*)"`的参数,会自动生成一个以`iact3-`开头的随机字符串。 129 | 5. 名称满足正则`r"(\w*)password(_|)(\d*)"`的参数,会自动生成密码。 130 | 6. 名称满足正则`r"(\w*)uuid(_|)(\d*)"`的参数,会自动生成一个uuid。 -------------------------------------------------------------------------------- /docs/en/README.md: -------------------------------------------------------------------------------- 1 | ## What is Iact3 2 | The full name of [Iact3](https://github.com/aliyun/alibabacloud-ros-tool-iact3) is IaC Template Test Tool (Infrastructure as Code Template Test Tool), where IaC (Infrastructure as Code) means to manage and configure infrastructure through code instead of manual processes. Common IaC tools include [Terraform](https://developer.hashicorp.com/terraform) and [Alibaba Cloud ROS](https://www.alibabacloud.com/help/zh/resource-orchestration-service), etc. 3 | 4 | ## Support 5 | [![Feature Request](https://img.shields.io/badge/Open%20Issues-Feature%20Request-green.svg)](https://github.com/aliyun/alibabacloud-ros-tool-iact3/issues/new/choose) 6 | [![Report Bugs](https://img.shields.io/badge/Open%20Issue-Report%20Bug-red.svg)](https://github.com/aliyun/alibabacloud-ros-tool-iact3/issues/new/choose) 7 | 8 | ## GitHub 9 | 10 | [![GitHub stars](https://img.shields.io/github/stars/aliyun/alibabacloud-ros-tool-iact3.svg?style=social&label=Stars)](https://github.com/aliyun/alibabacloud-ros-tool-iact3) 11 | [![GitHub issues](https://img.shields.io/github/issues/aliyun/alibabacloud-ros-tool-iact3.svg)](https://github.com/aliyun/alibabacloud-ros-tool-iact3/issues) 12 | [![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/aliyun/alibabacloud-ros-tool-iact3.svg)](https://github.com/aliyun/alibabacloud-ros-tool-iact3/issues?q=is%3Aissue+is%3Aclosed) 13 | [![GitHub pull requests](https://img.shields.io/github/issues-pr/aliyun/alibabacloud-ros-tool-iact3.svg)](https://github.com/aliyun/alibabacloud-ros-tool-iact3/pulls) 14 | [![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/aliyun/alibabacloud-ros-tool-iact3.svg)](https://github.com/aliyun/alibabacloud-ros-tool-iact3/pulls?q=is%3Apr+is%3Aclosed) 15 | -------------------------------------------------------------------------------- /docs/en/_navbar.md: -------------------------------------------------------------------------------- 1 | 2 | * [中文](/) 3 | * [en](/en/) -------------------------------------------------------------------------------- /docs/en/_sidebar.md: -------------------------------------------------------------------------------- 1 | - [README](en/README.md) 2 | - [QuickStart](en/quick_start.md) 3 | - [Installation](en/installation.md) 4 | - [Config](en/config.md) 5 | - [Usage](en/usage.md) 6 | - [GitHub Action](en/action.md) 7 | -------------------------------------------------------------------------------- /docs/en/action.md: -------------------------------------------------------------------------------- 1 | # Iact3 GitHub Action 2 | 3 | Iact3 Action is a Github Action aming at testing the Alibaba Cloud [ROS](https://www.alibabacloud.com/help/zh/resource-orchestration-service) templates. This action is used in the Iact3-test Workflow of ros-templates repository. Its purpose is to use the [Iact3](https://github.com/aliyun/alibabacloud-ros-tool-iact3) tool to test whether the ROS templates in pull request can be successfully deployed. 4 | 5 | The [ros-templates](https://github.com/aliyun/ros-templates) repository provides many examples and best practice resources of Alibaba Cloud ROS templates, including resource-level template examples, comprehensive template examples, best practices for complex scenarios, templates based on Transform syntax, Alibaba Cloud document templates, and Compute Nest's best templates. 6 | ## Usage 7 | ```yaml 8 | - name: Test templates 9 | uses: aliyun/alibabacloud-ros-tool-iact3@master 10 | with: 11 | templates: "template1.xml template2.xml" 12 | access_key_id: ${{ secrets.ACCESS_KEY_ID }} 13 | access_key_secret: ${{ secrets.ACCESS_KEY_SECRET }} 14 | type: "test" 15 | ``` 16 | ## Action Inputs 17 | | 名称 | 描述 | 18 | |-------------------|------------------------------------------------------------| 19 | | templates | space separated paths of templates and configuration files | 20 | | access_key_id | key_id of Alibaba Cloud account | 21 | | access_key_secret | key_secret of Alibaba Cloud account | 22 | | type | test type [ validate \| test ] | 23 | 24 | ## Action Output 25 | * `status` - `success/fail` Indicates whether the testing templates had all passed the Iact3 Action. 26 | 27 | ## Action Testing Process 28 | ### Test Objects 29 | When the input parameter `type` is `validate`, the test objects are all ROS templates in the input parameter `templates`. 30 | 31 | When the input parameter `type` is `test`, the test objects are all ROS templates included in the input parameter `templates`, and the ROS templates corresponding to the configuration files included in `templates`. When the ROS template corresponding to a configuration file does not exist in the repository, the test for this configuration file and the corresponding template will be skipped. 32 | 33 | ### Config file 34 | When the input parameter `type` is `test`, if there is a configuration file in the corresponding location of the tested template, the template will be deployed according to the configuration file in test. The configuration file corresponding to the template must meet the following conditions: 35 | * The configuration file name must be same as template name, and the suffix must be `.iact3.[yml|yaml]` 36 | * The location of the configuration file should be fixed at the same path as the template under `iact3-config/` directory (`name.[yml|yaml]` corresponds to `iact3-config/name.iact3.[yml,yaml]`) 37 | * The `project` configuration item `name` in the configuration file needs to be `test-{template name}` 38 | * The `template_config:template_location` may not be included in the configuration file. If it is included, the template path needs to use a relative path relative to the root directory of the ros-template repository. 39 | 40 | ### Test Method 41 | When the input parameter `type` is `validate`, Iact3 Action use the `iact3 validate` command to verify the validity of the template. 42 | 43 | When the input parameter `type` is `test`, Iact3 Action use the `iact3 test run` command to deploy and test the template with the configuration file, and use the `iact3 validate` command to verify the validity of other templates. 44 | ### Test Result 45 | If all the templates pass the test, Iact3 Action will output `status=success` to the workflow and return the exit status code `0`, otherwise it will output `status=fail` and return exit status code `1`. -------------------------------------------------------------------------------- /docs/en/config.md: -------------------------------------------------------------------------------- 1 | 2 | # Configuration files 3 | There are 2 config files which can be used to set behaviors. 4 | 5 | 1. Global config file, located in `~/.iact3.yml` 6 | 2. Project config file, located in `/.iact3.yml` 7 | 8 | Each configuration file supports three-tier configuration, which includes `general`, `project` and `tests`, and `tests` is required. 9 | 10 | ## general configuration item 11 | 12 | - `auth` Aliyun authentication section. 13 | ```json 14 | { 15 | "name": "default", 16 | "location": "~/.aliyun/config.json" 17 | } 18 | ``` 19 | 20 | - `oss_config` Oss bucket configuration, include BucketName, BucketRegion and etc. 21 | ```json 22 | { 23 | "bucket_name": "", 24 | "bucket_region": "", 25 | "object_prefix": "", 26 | "callback_params": { 27 | "callback_url": "", 28 | "callback_host": "", 29 | "callback_body": "", 30 | "callback_body_type": "", 31 | "callback_var_params": "" 32 | } 33 | } 34 | ``` 35 | 36 | - `parameters` Parameter key-values to pass to template. 37 | ```json 38 | { 39 | "vpc_id": "", 40 | "vsw_id": "" 41 | } 42 | ``` 43 | 44 | ## project configuration item 45 | 46 | - `name` Project Name 47 | - `regions` List of aliyun regions. 48 | - `parameters` Parameter key-values to pass to template. 49 | - `tags` Tags 50 | - `role_name` Role name 51 | - `template_config` Template config 52 | ```json 53 | { 54 | "template_location": "myTemplate/", 55 | "template_url": "oss://xxx", 56 | "template_body": "", 57 | "template_id": "", 58 | "template_version": "" 59 | } 60 | ``` 61 | 62 | ## tests configuration item 63 | 64 | - `name` Project Name 65 | - `regions` List of aliyun regions. 66 | - `parameters` Parameter key-values to pass to template. 67 | - `tags` Tags 68 | - `role_name` Role name 69 | - `template_config` Template config 70 | ```json 71 | { 72 | "template_location": "myTemplate/", 73 | "template_url": "oss://xxx", 74 | "template_body": "", 75 | "template_id": "", 76 | "template_version": "" 77 | } 78 | ``` 79 | 80 | # Precedence 81 | 82 | Except the parameters section, more specific config with the same key takes precedence. 83 | 84 | > The rationale behind having parameters function this way is so that values can be overridden at a system level outside a project, that is likely committed to source control. parameters that define account specific things like VPC details, Key Pairs, or secrets like API keys can be defined per host outside of source control. 85 | 86 | For example, consider this global config in `~/.iact3.yml` 87 | 88 | ```yaml 89 | general: 90 | oss_config: 91 | bucket_name: global-bucket 92 | parameters: 93 | KeyPair: my-global-ecs-key-pair 94 | ``` 95 | 96 | and this project config 97 | ```yaml 98 | project: 99 | name: my-project 100 | regions: 101 | - cn-hangzhou 102 | oss_config: 103 | bucket_name: project-bucket 104 | tests: 105 | default: 106 | template_config: 107 | template_url: "oss://xxx" 108 | regions: 109 | - cn-beijing 110 | parameters: 111 | KeyPair: my-test-ecs-key-pair 112 | ``` 113 | Would result in this effective test configuration: 114 | 115 | ```yaml 116 | tests: 117 | default: 118 | template_config: 119 | template_url: "oss://xxx" 120 | regions: 121 | - cn-beijing 122 | oss_config: 123 | bucket_name: project-bucket 124 | parameters: 125 | KeyPair: my-test-ecs-key-pair 126 | ``` 127 | 128 | Notice that `bucket_name` and `regions` took the most specific value and `KeyPair` the most general. 129 | 130 | # Pseudo Parameters 131 | 132 | You can automatically get the available parameters through the `$[iact3-auto]` pseudo-parameter if the parameter is the following 2 cases 133 | 1. The resource attribute corresponding to the parameter supports the ROS [GetTemplateParameterConstraints](https://www.alibabacloud.com/help/en/resource-orchestration-service/latest/gettemplateparameterconstraints) interface. 134 | 2. Parameters whose name itself has a specific meaning. For example, `VpcId` means the id of virtual private cloud and `$[iact3-auto]` will automatically obtain a vpcId randomly in the current region of the current account. Currently supported are as follows: 135 | 1. Satisfying the regularity `r"(\w*)vpc(_|)id(_|)(\d*)"` will automatically and randomly obtain the VpcId in the current region. 136 | 2. Satisfying the regularity `r"(\w*)v(_|)switch(_|)id(_|)(\d*)"` will automatically and randomly obtain the VswitchId in the current region. If there is a parameter whose name satisfies the regularity `r"(\w*)zone(_|)id(_|)(\d*)"`, it will query the VswitchId of the corresponding availability zone 137 | 3. Satisfying the regularity `r"(\w*)security(_|)group(_id|id)(_|)(\d*)"` will automatically and randomly obtain the SecurityGroupId in the current region. 138 | 4. Satisfying the regularity `r"(\w*)name(_|)(\d*)"` will automatically generate a random string starting with `iact3-`. 139 | 5. Satisfying the regularity `r"(\w*)password(_|)(\d*)"` will automatically generate a password. 140 | 6. Satisfying the regularity `r"(\w*)uuid(_|)(\d*)"` will automatically generate an uuid. 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /docs/en/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | 4 | 5 | ## Requirements 6 | ![Prerequisites](https://img.shields.io/badge/Prerequisites-pip-blue.svg) 7 | ![Python 3.7+](https://img.shields.io/badge/Python-3.7+-blue.svg) 8 | 9 | The iact3 is run on requires access to an Alibaba Cloud account, this can be done by any of the following mechanisms: 10 | 1. AliyunCli default configuration file (`~/.aliyun/config.json`) 11 | 2. Environment variables (`ALIBABA_CLOUD_ACCESS_KEY_ID` and `ALIBABA_CLOUD_ACCESS_KEY_SECRET`) 12 | 3. The ini configuration file defined by the environment variable ALIBABA_CLOUD_CREDENTIALS_FILE 13 | 4. Alibaba Cloud SDK Credentials default configuration file (`~/.alibabacloud/credentials.ini` or `~/.aliyun/credentials.ini`) 14 | 15 | ## Install CLI 16 | `pip install alibabacloud-ros-iact3` -------------------------------------------------------------------------------- /docs/en/quick_start.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | ### Requirements 3 | Install Python 3.7 or above. 4 | 5 | ### Install CLI 6 | Execute the following command to install: 7 | 8 | ```bash 9 | pip install alibabacloud-ros-iact3 10 | ``` 11 | 12 | ## Configuration 13 | 14 | ### Authentication 15 | 16 | The iact3 is run on requires access to an Alibaba Cloud account, this can be done by any of the following mechanisms: 17 | 18 | 1. AliyunCli default configuration file (~/.aliyun/config.json) 19 | 2. Environment variables (ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET) 20 | 3. The ini configuration file defined by the environment variable ALIBABA_CLOUD_CREDENTIALS_FILE 21 | 4. Alibaba Cloud SDK Credentials default configuration file (~/.alibabacloud/credentials.ini or ~/.aliyun/credentials.ini) 22 | 23 | ### Configuration Files 24 | 25 | To use iact3, you need to prepare a configuration file, an example of a configuration file: 26 | 27 | ```yaml 28 | project: 29 | name: my-first-test 30 | template_config: 31 | template_location: ~/ecs.yaml 32 | regions: 33 | - cn-hangzhou 34 | - cn-beijing 35 | tests: 36 | test-name-1: 37 | parameters: 38 | InstanceType: ecs.g6e.large 39 | test-name-2: 40 | parameters: 41 | InstanceType: ecs.c6.large 42 | ``` 43 | 44 | The above configuration file is explained as follows: 45 | 46 | - `project` Indicates the basic information of the project item, including: 47 | - `name` Used to specify the configuration name 48 | - `template_config` Template for specifying the role of the configuration file 49 | - `regions` It is used to specify the region for the configuration file, and supports specifying multiple regions in the form of a list 50 | - `tests` Indicates the test case information, the configuration file contains `test-name-1` and `test-name-2` two test cases 51 | - `parameters` Used to specify the parameter values in the template 52 | 53 | 54 | For more configuration-related content, please refer to the [Config](en/config.md) section 55 | 56 | ## Cli Command 57 | Go to the directory where the configuration file is located and execute the CLI command. 58 | 59 | *ps1: iact3 will search for the configuration file `.iact3.yml` in the current directory, or you can specify a configuration file in any location by `-c` or `--config`.* 60 | 61 | *ps2: If there is no configuration template in the configuration file, iact3 will search for a file ending with `.template.[json|yaml|yml]` in the current directory as a template file.* 62 | 63 | ### Template Testing 64 | Tests whether IaC templates are able to successfully launch. 65 | 66 | ```bash 67 | iact3 test run 68 | ``` 69 | 70 | ### Create Base Resources 71 | Create the basic resources required for testing in a specified region, including a VPC instance, a security group, and a VSwitch instance for each availability zone in the current region. 72 | 73 | ```bash 74 | iact3 base create 75 | ``` 76 | 77 | ### Get Template Estimate Cost 78 | 79 | Give the price of the templates. 80 | 81 | ```bash 82 | iact3 cost 83 | ``` 84 | 85 | ### Validate Template 86 | Validate the templates. 87 | 88 | ```bash 89 | iact3 validate 90 | ``` 91 | 92 | ### Preview Template Stack 93 | Preview the resource stack information to be created by the template, and verify the accuracy of the template resources. 94 | ```bash 95 | iact3 preview [options] 96 | ``` 97 | 98 | 99 | ### Get Template Policy 100 | Get policies of the templates. 101 | 102 | ```bash 103 | iact3 policy 104 | ``` 105 | 106 | For more command-line related content, please refer to the [Usage](en/usage.md) section. -------------------------------------------------------------------------------- /docs/en/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | Iact3 adopts a similar cli command structure to `git` with a `iact3 command subcommand --flag` style. The cli is also designed to be the simplest if run from the root of a project. Let's have a look at equivalent command to run a test: 4 | 5 | cd into the project root and type test run 6 | ```shell 7 | cd ./demo 8 | iact3 test run 9 | ``` 10 | 11 | or run it from anywhere by providing the path to the project root 12 | ```shell 13 | iact3 test run --project-path ./demo 14 | ``` 15 | 16 | ## Template Test 17 | ### Cli Command 18 | ```bash 19 | iact3 test [subcommand] [options] 20 | ``` 21 | The cli is self documenting by using `--help` or `-h`, the most common command is `iact3 test`. 22 | Before testing template, the configuration file needs to be set up correctly. See [configuration docs](./config.md) for more details 23 | 24 | ### Subcommands 25 | 26 | - `clean`: Manually clean up the stacks which were created by iact3. 27 | - `list`: List stacks which were created by iact3 for all regions. 28 | - `params`: Generate pseudo parameters. 29 | - `run`: Tests whether IaC templates are able to successfully launch. 30 | 31 | ### Options 32 | - `-t, --template`: path to a template 33 | - `-c, --config-file`: path to a config file 34 | - `-o, --output-directory`: path to an output directory 35 | - `-r, --regions`: comma separated list of regions to test in 36 | - `--test-names`: comma separated list of tests to run 37 | - `--no-delete `: don't delete stacks after test is complete 38 | - `--project-path`: root path of the project relative to config file, template file and output file 39 | - `--keep-failed`: do not delete failed stacks 40 | - `--dont-wait-for-delete`: exits immediately after calling delete stack 41 | - `-g, --generate-parameters`: generate pseudo parameters 42 | - `-l, --log-format`: comma separated list of log format (xml,json) 43 | 44 | 45 | ## Get Template Estimate Cost 46 | ### Cli Command 47 | ```bash 48 | iact3 cost [options] 49 | ``` 50 | The cli is self documenting by using `--help` or `-h`, the most common command is `iact3 cost` 51 | 52 | ### Options 53 | - `-t, --template`: path to a template 54 | - `-c, --config-file`: path to a config file 55 | - `-r, --regions`: comma separated list of regions 56 | 57 | ## Validate Template 58 | ### Cli Command 59 | ```bash 60 | iact3 validate [options] 61 | ``` 62 | The cli is self documenting by using `--help` or `-h`, the most common command is `iact3 validate` 63 | 64 | ### Options 65 | - `-t, --template`: path to a template 66 | - `-c, --config-file`: path to a config file 67 | 68 | ## Preview Template Stack 69 | ### Cli Command 70 | ```bash 71 | iact3 preview [options] 72 | ``` 73 | The cli is self documenting by using `--help` or `-h`, the most common command is `iact3 preview` 74 | 75 | ### Options 76 | - `-t, --template`: path to a template 77 | - `-c, --config-file`: path to a config file 78 | - `-r, --regions`: comma separated list of regions 79 | 80 | ## Get Template Policy 81 | ### Cli Command 82 | ```bash 83 | iact3 policy [options] 84 | ``` 85 | The cli is self documenting by using `--help` or `-h`, the most common command is `iact3 policy` 86 | 87 | ### Options 88 | - `-t, --template`: path to a template 89 | - `-c, --config-file`: path to a config file 90 | 91 | 92 | -------------------------------------------------------------------------------- /docs/icon/ROS.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Iact3 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # 安装 2 | ## 前置条件 3 | ![Prerequisites](https://img.shields.io/badge/Prerequisites-pip-blue.svg) 4 | ![Python 3.7+](https://img.shields.io/badge/Python-3.7+-blue.svg) 5 | 6 | iact3 运行需要访问阿里云帐户,可以通过以下任一机制来完成: 7 | 1. 使用AliyunCli默认配置文件 (`~/.aliyun/config.json`) 8 | 2. 配置环境变量(`ALIBABA_CLOUD_ACCESS_KEY_ID` 和 `ALIBABA_CLOUD_ACCESS_KEY_SECRET`) 9 | 3. 使用环境变量`ALIBABA_CLOUD_CREDENTIALS_FILE`中定义的ini配置文件 10 | 4. 使用阿里云SDK凭证默认配置文件(`~/.alibabacloud/credentials.ini`或`~/.aliyun/credentials.ini`) 11 | 12 | ## 安装CLI 13 | ```bash 14 | pip install alibabacloud-ros-iact3 15 | ``` -------------------------------------------------------------------------------- /docs/quick_start.md: -------------------------------------------------------------------------------- 1 | ## 安装 2 | ### 前置条件 3 | 安装Python 3.7 或以上版本 4 | 5 | ### 安装CLI 6 | 执行以下命令进行安装: 7 | 8 | ```bash 9 | pip install alibabacloud-ros-iact3 10 | ``` 11 | 12 | ## 配置 13 | 14 | ### 身份验证 15 | 16 | 使用 iact3 需要配置阿里云账号,可以通过任一一种方式配置: 17 | 18 | 1. 使用 AliyunCli 默认配置文件 (`~/.aliyun/config.json`) 19 | 2. 配置环境变量(`ALIBABA_CLOUD_ACCESS_KEY_ID` 和 `ALIBABA_CLOUD_ACCESS_KEY_SECRET`) 20 | 3. 使用环境变量`ALIBABA_CLOUD_CREDENTIALS_FILE`中定义的ini配置文件 21 | 4. 使用阿里云SDK凭证默认配置文件(`~/.alibabacloud/credentials.ini`或`~/.aliyun/credentials.ini`) 22 | 23 | 24 | ### 配置文件 25 | 26 | 使用 iact3 需要准备配置文件,配置文件示例: 27 | 28 | ```yaml 29 | project: 30 | name: my-first-test 31 | template_config: 32 | template_location: ~/ecs.yaml 33 | regions: 34 | - cn-hangzhou 35 | - cn-beijing 36 | tests: 37 | test-name-1: 38 | parameters: 39 | InstanceType: ecs.g6e.large 40 | test-name-2: 41 | parameters: 42 | InstanceType: ecs.c6.large 43 | ``` 44 | 45 | 上述配置文件解释如下: 46 | 47 | - `project` 表示项目的基本信息,包括: 48 | - `name` 用于指定配置名称 49 | - `template_config` 用于指定配置文件作用的模板 50 | - `regions` 用于指定配置文件作用的地域,支持以列表形式指定多个地域 51 | - `tests` 表示测试用例信息,该配置文件包含了`test-name-1`和`test-name-2`两个测试用例 52 | - `parameters` 用于指定模板中参数值 53 | 54 | 55 | 更多配置相关的内容请参考[配置](config.md)部分内容。 56 | 57 | ## 开始使用 58 | 进入到配置文件所在目录,执行 CLI 命令。 59 | 60 | *注 1:iact3会在当前目录下查找`.iact3.yml`的配置文件,也可以通过`-c`或`--config`指定任意位置的配置文件。* 61 | 62 | *注 2:如果配置文件中没有配置模板,iact3会在当前目录查找以`.template.[json|yaml|yml]`结尾的文件作为模板文件。* 63 | 64 | ### 模板测试 65 | 测试 IaC 模板是否能够成功创建。 66 | 67 | ```bash 68 | iact3 test run 69 | ``` 70 | 71 | ### 创建基础资源 72 | 在指定地域创建测试所需要的基础资源,包括一个VPC实例、一个安全组和当前地域所有可用区各一个VSwitch实例。 73 | 74 | ```bash 75 | iact3 base create 76 | ``` 77 | 78 | ### 模版询价 79 | 80 | 查询用模板创建资源时需要支付的价格。 81 | 82 | ```bash 83 | iact3 cost 84 | ``` 85 | 86 | ### 模板校验 87 | 校验模板的合法性。 88 | 89 | ```bash 90 | iact3 validate 91 | ``` 92 | 93 | ### 模版预览 94 | 预览模板将要创建的资源栈信息,验证模板资源的准确性。 95 | ```bash 96 | iact3 preview 97 | ``` 98 | 99 | 100 | ### 策略查询 101 | 查询模板所需的策略信息。 102 | 103 | ```bash 104 | iact3 policy 105 | ``` 106 | 107 | 更多命令行相关的内容请参考[使用](usage.md)部分内容。 108 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # 使用 2 | 3 | Iact3 采用与 `git` 类似的 cli 命令结构,使用 `iact3 command [subcommand] --flag` 结构。 4 | Iact3 可以直接在项目的根目录运行或通过添加项目路径参数在任意位置运行测试。 Iact3最常见的用途是执行模版测试功能,执行此操作的命令如下: 5 | 6 | 在项目的根目录运行: 7 | ```bash 8 | cd ./demo 9 | iact3 test run 10 | ``` 11 | 在任意位置通过项目路径参数运行: 12 | ```bash 13 | iact3 test run --project-path ./demo 14 | ``` 15 | 16 | 可以通过`--help`参数查看iact3支持的命令、子命令和参数详情。 17 | 18 | ## 模版测试 19 | 测试项目中的模板和配置是否可以成功创建。在测试前,需要正确设置配置文件。请参阅[配置文档](./config.md)了解更多详细信息。 20 | ### 命令 21 | ```bash 22 | iact3 test [subcommand] [options] 23 | ``` 24 | ### 子命令 25 | - `clean`: 清理 Iact3 创建的堆栈 26 | - `list`: 列出 Iact3 为所有地域创建的堆栈 27 | - `params`: 生成并展示模板伪参数 28 | - `run`: 测试 IaC 模板是否能够成功创建 29 | 30 | ### 参数 31 | 支持如下可选项: 32 | - `-t, --template`: 模板的路径 33 | - `-c, --config-file`: 配置文件的路径 34 | - `-o, --output-directory`: 输出目录的路径 35 | - `-r, --regions`: 以逗号分隔的要测试的地域列表 36 | - `--test-names`: 以逗号分隔的要运行的测试列表 37 | - `--no-delete `: 测试完成后不删除堆栈 38 | - `--project-path`: 带有模板和配置文件的项目根路径 39 | - `--keep-failed`: 不删除失败的堆栈 40 | - `--dont-wait-for-delete`: 调用删除堆栈后立即退出 41 | - `-g, --generate-parameters`: 生成伪参数 42 | - `-l, --log-format`: 以逗号分隔的测试日志格式列表(支持xml、json) 43 | 44 | ## 模版询价 45 | 查询用模板创建资源时需要支付的价格。 46 | ### 命令 47 | ```bash 48 | iact3 cost [options] 49 | ``` 50 | ### 参数 51 | - `-t, --template`: 模板的路径 52 | - `-c, --config-file`: 配置文件的路径 53 | - `-r, --regions`: 以逗号分隔的要测试的地域列表 54 | 55 | ## 模板校验 56 | 校验模板的合法性。 57 | ### 命令 58 | ```bash 59 | iact3 validate [options] 60 | ``` 61 | ### 参数 62 | - `-t, --template`: 模板的路径 63 | - `-c, --config-file`: 配置文件的路径 64 | 65 | ## 模版预览 66 | 预览模板将要创建的资源栈信息,验证模板资源的准确性。 67 | ### 命令 68 | ```bash 69 | iact3 preview [options] 70 | ``` 71 | ### 参数 72 | - `-t, --template`: 模板的路径 73 | - `-c, --config-file`: 配置文件的路径 74 | - `-r, --regions`: 以逗号分隔的要测试的地域列表 75 | 76 | ## 策略查询 77 | 查询模板所需的策略信息。 78 | ### 命令 79 | ```bash 80 | iact3 policy [options] 81 | ``` 82 | ### 参数 83 | - `-t, --template`: 模板的路径 84 | - `-c, --config-file`: 配置文件的路径 85 | 86 | ## 查看帮助信息 87 | ### 命令 88 | ```bash 89 | iact3 -h 90 | iact3 command -h 91 | iact3 command subcommand -h 92 | ``` -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export ALIBABA_CLOUD_ACCESS_KEY_ID=$INPUT_ACCESS_KEY_ID 4 | export ALIBABA_CLOUD_ACCESS_KEY_SECRET=$INPUT_ACCESS_KEY_SECRET 5 | 6 | pass_test=1 7 | 8 | 9 | if [ "$INPUT_TYPE" = "validate" ]; then 10 | for file in $INPUT_TEMPLATES 11 | do 12 | if [[ "$file" == .github* ]]; then 13 | continue 14 | fi 15 | if [[ "$file" == .DS_Store* ]]; then 16 | continue 17 | fi 18 | if [[ "$file" != *.yml ]] && [[ "$file" != *.yaml ]] ; then 19 | continue 20 | fi 21 | 22 | echo -e "\n------Testing $file------" 23 | if [[ "$file" != iact3-config/* ]]; then 24 | python -m iact3 validate -t "$file" >> output.txt 2>&1 25 | cat output.txt 26 | if ! grep -q "LegalTemplate" output.txt; then 27 | pass_test=0 28 | fi 29 | rm -rf output.txt 30 | fi 31 | done 32 | if [ $pass_test -eq 1 ] 33 | then 34 | echo "status=success" >> $GITHUB_OUTPUT 35 | exit 0 36 | else 37 | echo "status=fail" >> $GITHUB_OUTPUT 38 | exit 1 39 | fi 40 | fi 41 | 42 | 43 | declare -a template_prefix_files=() 44 | 45 | for file in $INPUT_TEMPLATES 46 | do 47 | if [[ "$file" == .github* ]]; then 48 | continue 49 | fi 50 | if [[ "$file" == .DS_Store* ]]; then 51 | continue 52 | fi 53 | 54 | is_test_run=0 55 | echo -e "\n------Testing $file------" 56 | if [[ "$file" == iact3-config/* ]]; then 57 | #config file 58 | template_file_prefix=${file#iact3-config/} 59 | template_file_prefix=${template_file_prefix%.*} 60 | template_file_prefix=${template_file_prefix%.*} 61 | template_file_path="" 62 | 63 | if [[ " ${template_prefix_files[@]} " =~ " ${template_file_prefix} " ]]; then 64 | continue 65 | fi 66 | if [ -f ${template_file_prefix}.yml ]; then 67 | template_file_path=${template_file_prefix}.yml 68 | is_test_run=1 69 | echo "iact3 test run -t $template_file_path -c $file" 70 | python -m iact3 test run -t "$template_file_path" -c "$file" > /dev/null 71 | elif [ -f ${template_file_prefix}.yaml ]; then 72 | template_file_path=${template_file_prefix}.yaml 73 | is_test_run=1 74 | echo "iact3 test run -t $template_file_path -c $file" 75 | python -m iact3 test run -t "$template_file_path" -c "$file" > /dev/null 76 | else 77 | echo "$file has no template file. Skip testing." 78 | fi 79 | 80 | template_prefix_files+=("$template_file_prefix") 81 | else 82 | #template file 83 | config_file_prefix=iact3-config/${file%.*}.iact3 84 | config_file_path="" 85 | template_file_prefix=${file%.*} 86 | if [[ " ${template_prefix_files[@]} " =~ " ${template_file_prefix} " ]] 87 | then 88 | continue 89 | fi 90 | if [ -f ${config_file_prefix}.yml ]; then 91 | config_file_path=${config_file_prefix}.yml 92 | echo "iact3 test run -t $file -c $config_file_path" 93 | is_test_run=1 94 | python -m iact3 test run -t "$file" -c "$config_file_path" > /dev/null 95 | elif [ -f ${config_file_prefix}.yaml ]; then 96 | config_file_path=${config_file_prefix}.yaml 97 | echo "iact3 test run -t $file -c $config_file_path" 98 | is_test_run=1 99 | python -m iact3 test run -t "$file" -c "$config_file_path" > /dev/null 100 | else 101 | echo "iact3 validate -t $file" 102 | python -m iact3 validate -t "$file" >> output.txt 2>&1 103 | echo $file 104 | cat output.txt 105 | if ! grep -q "LegalTemplate" output.txt; then 106 | pass_test=0 107 | fi 108 | rm -rf output.txt 109 | fi 110 | template_prefix_files+=("$template_file_prefix") 111 | fi 112 | 113 | if [ $is_test_run -eq 1 ]; then 114 | test_name=$(basename $template_file_prefix) 115 | test_name="test-${test_name}" 116 | cat iact3_outputs/${test_name}-result.json 117 | test_result=$(jq '.Result' iact3_outputs/${test_name}-result.json) 118 | if [[ $test_result != "\"Success\"" ]]; then 119 | pass_test=0 120 | fi 121 | fi 122 | 123 | done 124 | 125 | if [ $pass_test -eq 1 ] 126 | then 127 | echo "status=success" >> $GITHUB_OUTPUT 128 | exit 0 129 | else 130 | echo "status=fail" >> $GITHUB_OUTPUT 131 | exit 1 132 | fi -------------------------------------------------------------------------------- /iact3/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-ros-tool-iact3/64e93057e82e634cd79ee5c4e2d761ef31fe1e03/iact3/__init__.py -------------------------------------------------------------------------------- /iact3/__main__.py: -------------------------------------------------------------------------------- 1 | from iact3.main import sync_run 2 | 3 | 4 | if __name__ == "__main__": 5 | sync_run() -------------------------------------------------------------------------------- /iact3/cli_modules/__init__.py: -------------------------------------------------------------------------------- 1 | from .delete import Delete 2 | from .list import List 3 | from .test import Test 4 | from .base import Base 5 | from .cost import Cost 6 | from .validate import Validate 7 | from .preview import Preview 8 | from .policy import Policy 9 | -------------------------------------------------------------------------------- /iact3/cli_modules/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import uuid 3 | 4 | from iact3.cli import CliCore 5 | from iact3.cli_modules import Delete, List 6 | from iact3.config import TestConfig 7 | from iact3.plugin.ros import StackPlugin 8 | from iact3.stack import Stacker 9 | from iact3.termial_print import TerminalPrinter 10 | 11 | BASIC_RESOURCE_TEMPLATE = ''' 12 | ROSTemplateFormatVersion: '2015-09-01' 13 | Resources: 14 | SecurityGroup: 15 | Type: ALIYUN::ECS::SecurityGroup 16 | Properties: 17 | SecurityGroupIngress: 18 | - Priority: 1 19 | IpProtocol: all 20 | NicType: internet 21 | SourceCidrIp: 0.0.0.0/0 22 | PortRange: '-1/-1' 23 | VpcId: 24 | Ref: Vpc 25 | SecurityGroupName: iact3 26 | VSwitch: 27 | Type: ALIYUN::ECS::VSwitch 28 | Count: 29 | Fn::Length: 30 | Fn::GetAZs: 31 | Ref: ALIYUN::Region 32 | Properties: 33 | VSwitchName: 34 | Fn::Sub: 35 | - iact3-${zone} 36 | - zone: 37 | Fn::Select: 38 | - Ref: ALIYUN::Index 39 | - Fn::GetAZs: 40 | Ref: ALIYUN::Region 41 | VpcId: 42 | Ref: Vpc 43 | CidrBlock: 44 | Fn::Sub: 192.168.${ALIYUN::Index}.0/24 45 | ZoneId: 46 | Fn::Select: 47 | - Ref: ALIYUN::Index 48 | - Fn::GetAZs: 49 | Ref: ALIYUN::Region 50 | Vpc: 51 | Type: ALIYUN::ECS::VPC 52 | Properties: 53 | VpcName: iact3 54 | CidrBlock: 192.168.0.0/16 55 | ''' 56 | 57 | BASIC_RESOURCE_NAME = 'basic-resource' 58 | BASIC_RESOURCE_TAG = {'iact3-project-name': BASIC_RESOURCE_NAME} 59 | 60 | 61 | class Base: 62 | ''' 63 | Create or delete or list basic resources which includes vpc, 64 | security group and several switches for testing 65 | ''' 66 | 67 | @staticmethod 68 | @CliCore.longform_param_required('project_path') 69 | async def create(regions: str = None, config_file: str = None, project_path: str = None) -> None: 70 | ''' 71 | Create basic resources for testing 72 | :param regions: comma separated list of regions to create 73 | :param config_file: path to a config file 74 | :param project_path: root path of the project relative to config file 75 | ''' 76 | configs = await Base._get_config(regions, config_file, project_path) 77 | stacker = Stacker( 78 | BASIC_RESOURCE_NAME, 79 | configs, 80 | uid=uuid.uuid4() 81 | ) 82 | await stacker.create_stacks() 83 | printer = TerminalPrinter() 84 | await printer.report_test_progress(stacker=stacker) 85 | 86 | @staticmethod 87 | @CliCore.longform_param_required('project_path') 88 | async def delete(regions: str = None, config_file: str = None, project_path: str = None) -> None: 89 | ''' 90 | Delete basic resources for testing 91 | :param regions: comma separated list of regions to delete 92 | :param config_file: path to a config file 93 | :param project_path: root path of the project relative to config file 94 | ''' 95 | await Delete.create(regions, config_file, project_path, tags=BASIC_RESOURCE_TAG) 96 | 97 | @staticmethod 98 | @CliCore.longform_param_required('project_path') 99 | async def list(regions: str = None, config_file: str = None, project_path: str = None) -> None: 100 | ''' 101 | List basic resources for testing 102 | :param regions: comma separated list of regions to list 103 | :param config_file: path to a config file 104 | :param project_path: root path of the project relative to config file 105 | ''' 106 | await List.create(regions, config_file, project_path, tags=BASIC_RESOURCE_TAG) 107 | 108 | @staticmethod 109 | async def _get_config(regions: str, config_file: str = None, project_path: str = None): 110 | credential = List.get_credential(config_file, project_path) 111 | kwargs = dict( 112 | template_config={'template_body': BASIC_RESOURCE_TEMPLATE} 113 | ) 114 | 115 | if regions: 116 | regions = regions.split(',') 117 | else: 118 | region_plugin = StackPlugin(region_id='cn-hangzhou', credential=credential) 119 | regions = await region_plugin.get_regions() 120 | 121 | results = [] 122 | for region in regions: 123 | config = TestConfig.from_dict(kwargs) 124 | config.test_name = BASIC_RESOURCE_NAME 125 | config.auth.credential = credential 126 | config.region = region 127 | results.append(config) 128 | return results 129 | -------------------------------------------------------------------------------- /iact3/cli_modules/cost.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from iact3.cli import CliCore 5 | from iact3.cli_modules.delete import Delete 6 | from iact3.cli_modules.list import List 7 | from iact3.config import DEFAULT_CONFIG_FILE 8 | from iact3.testing.ros_stack import StackTest 9 | 10 | LOG = logging.getLogger(__name__) 11 | 12 | class Cost: 13 | ''' 14 | Give the price of the templates. 15 | ''' 16 | 17 | def __init__(self, template: str = None, 18 | config_file: str = DEFAULT_CONFIG_FILE, 19 | regions: str = None): 20 | ''' 21 | :param template: path to a template 22 | :param config_file: path to a config file 23 | :param regions: comma separated list of regions 24 | ''' 25 | self.template = template 26 | self.config_file = config_file 27 | self.regions = regions 28 | 29 | 30 | @classmethod 31 | async def create(cls, template: str = None, 32 | config_file: str = None, 33 | regions: str = None, 34 | tags: dict = None): 35 | tests = await StackTest.from_file( 36 | template=template, 37 | project_config_file=config_file, 38 | regions=regions 39 | ) 40 | LOG.info(f'start querying templates costs.') 41 | await StackTest.get_stacks_price(tests) 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /iact3/cli_modules/delete.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | from iact3.cli_modules.list import List 5 | from iact3.stack import Stacks, Stack, Stacker 6 | from iact3.termial_print import TerminalPrinter 7 | 8 | LOG = logging.getLogger(__name__) 9 | LIMIT = 10 10 | 11 | 12 | class Delete: 13 | ''' 14 | Manually clean up the stacks which were created by Iact3 15 | ''' 16 | 17 | def __init__(self, regions: str = None, config_file: str = None, project_path: str = None): 18 | ''' 19 | :param regions: comma separated list of regions to delete from, default will scan all regions 20 | :param config_file: path to a config file 21 | :param project_path: root path of the project relative to config file 22 | ''' 23 | self.regions = regions 24 | self.config_file = config_file 25 | self.project_path = project_path 26 | 27 | @classmethod 28 | async def create(cls, regions: str = None, 29 | config_file: str = None, 30 | project_path: str = None, 31 | tags: dict = None, 32 | stack_id: str = None): 33 | credential = List.get_credential(config_file, project_path) 34 | all_stacks = await List.create(regions, config_file, project_path, tags, stack_id=stack_id) 35 | if not all_stacks: 36 | LOG.info('can not find stack to delete.') 37 | return 38 | LOG.info('Start delete above stacks') 39 | printer = TerminalPrinter() 40 | 41 | for i in range(0, len(all_stacks), LIMIT): 42 | stacks = Stacks() 43 | stacks += [Stack.from_stack_response(stack, credential=credential) for stack in all_stacks[i: i+LIMIT]] 44 | stacker = Stacker.from_stacks(stacks) 45 | await stacker.delete_stacks() 46 | await printer.report_test_progress(stacker) 47 | -------------------------------------------------------------------------------- /iact3/cli_modules/list.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import logging 4 | 5 | from iact3.cli import CliCore 6 | from iact3.config import BaseConfig, DEFAULT_CONFIG_FILE, DEFAULT_PROJECT_ROOT 7 | from iact3.generate_params import IAC_NAME 8 | from iact3.plugin.ros import StackPlugin 9 | from iact3.stack import SYS_TAGS 10 | 11 | LOG = logging.getLogger(__name__) 12 | 13 | 14 | class List: 15 | ''' 16 | List stacks which were created by Iact3 for all regions. 17 | ''' 18 | @CliCore.longform_param_required('project_path') 19 | def __init__(self, regions: str = None, config_file: str = None, project_path: str = None): 20 | ''' 21 | :param regions: comma separated list of regions to delete from, default will scan all regions 22 | :param config_file: path to a config file 23 | :param project_path: root path of the project relative to config file 24 | ''' 25 | self.regions = regions 26 | self.config_file = config_file 27 | self.project_path = project_path 28 | 29 | @classmethod 30 | async def create(cls, regions: str = None, 31 | config_file: str = None, 32 | project_path: str = None, 33 | tags: dict = None, 34 | stack_id: str = None): 35 | credential = cls.get_credential(config_file, project_path) 36 | if regions: 37 | regions = regions.split(',') 38 | else: 39 | region_plugin = StackPlugin(region_id='cn-hangzhou', credential=credential) 40 | regions = await region_plugin.get_regions() 41 | list_tasks = [] 42 | if tags: 43 | tags.update(SYS_TAGS) 44 | else: 45 | tags = SYS_TAGS 46 | for region in regions: 47 | stack_plugin = StackPlugin(region_id=region, credential=credential) 48 | list_tasks.append( 49 | asyncio.create_task(stack_plugin.fetch_all_stacks(tags, stack_id=stack_id)) 50 | ) 51 | stacks = await asyncio.gather(*list_tasks) 52 | all_stacks, project_length, test_length, stack_name_length = cls._get_all_stacks(stacks) 53 | if not all_stacks: 54 | LOG.info('can not find any stack.') 55 | return 56 | header = f'ProjectName{" "*project_length}TestName{" "*test_length}StackName{" "*stack_name_length}Region' 57 | LOG.info(header) 58 | column = '{} {} {} {}' 59 | for stack in all_stacks: 60 | project_name = cls._format_name(stack['ProjectName'], project_length) 61 | test_name = cls._format_name(stack['TestName'], test_length) 62 | stack_name = cls._format_name(stack['StackName'], stack_name_length) 63 | LOG.info(column.format(project_name, test_name, stack_name, stack['RegionId'])) 64 | return all_stacks 65 | 66 | @classmethod 67 | def _get_all_stacks(cls, stacks): 68 | all_stacks = [] 69 | longest_project_name = '' 70 | longest_test_name = '' 71 | longest_stack_name = '' 72 | for region_stacks in stacks: 73 | for stack in region_stacks: 74 | stack_name = stack['StackName'] 75 | if len(stack_name) > len(longest_stack_name): 76 | longest_stack_name = stack_name 77 | tags = stack['Tags'] 78 | for tag in tags: 79 | if tag['Key'] == f'{IAC_NAME}-test-name': 80 | test_name = tag['Value'] 81 | if len(test_name) > len(longest_test_name): 82 | longest_test_name = test_name 83 | stack['TestName'] = test_name 84 | elif tag['Key'] == f'{IAC_NAME}-project-name': 85 | project_name = tag['Value'] 86 | if len(project_name) > len(longest_project_name): 87 | longest_project_name = project_name 88 | stack['ProjectName'] = project_name 89 | elif tag['Key'] == f'{IAC_NAME}-id': 90 | stack['TestId'] = tag['Value'] 91 | all_stacks.append(stack) 92 | 93 | return all_stacks, len(longest_project_name), len(longest_test_name), len(longest_stack_name) 94 | 95 | @classmethod 96 | def _format_name(cls, name, length): 97 | if len(name) < length: 98 | name += f'{" " * (length - len(name))}' 99 | return name 100 | 101 | @classmethod 102 | def get_credential(cls, config_file: str = None, project_path: str = None): 103 | base_config = BaseConfig.create( 104 | project_config_file=config_file or DEFAULT_CONFIG_FILE, 105 | project_path=project_path or DEFAULT_PROJECT_ROOT, 106 | fail_ok=True 107 | ) 108 | return base_config.get_credential() 109 | -------------------------------------------------------------------------------- /iact3/cli_modules/policy.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import asyncio 4 | 5 | from iact3.cli import CliCore 6 | from iact3.cli_modules.delete import Delete 7 | from iact3.cli_modules.list import List 8 | 9 | from iact3.testing.ros_stack import StackTest 10 | from iact3.config import BaseConfig, PROJECT, REGIONS, TEMPLATE_CONFIG, TESTS, TestConfig, IAC_NAME, \ 11 | DEFAULT_PROJECT_ROOT, OssConfig, Auth, TEMPLATE_LOCATION, DEFAULT_CONFIG_FILE, DEFAULT_OUTPUT_DIRECTORY 12 | from iact3.exceptions import Iact3Exception 13 | 14 | from iact3.config import TemplateConfig 15 | from iact3.plugin.ros import StackPlugin 16 | from iact3.termial_print import TerminalPrinter 17 | 18 | LOG = logging.getLogger(__name__) 19 | 20 | class Policy: 21 | ''' 22 | Get policies of the templates. 23 | ''' 24 | 25 | def __init__(self, template: str = None, 26 | config_file: str = DEFAULT_CONFIG_FILE, 27 | regions: str = None): 28 | ''' 29 | :param template: path to a template 30 | :param config_file: path to a config file 31 | :param regions: comma separated list of regions 32 | ''' 33 | self.template = template 34 | self.config_file = config_file 35 | self.regions = regions 36 | 37 | 38 | @classmethod 39 | async def create(cls, template: str = None, 40 | config_file: str = None, 41 | regions: str = None): 42 | 43 | args = {} 44 | if regions: 45 | args[REGIONS] = regions.split(',') 46 | 47 | project_path = DEFAULT_PROJECT_ROOT 48 | 49 | 50 | if template: 51 | template_config = TemplateConfig(template_location=template) 52 | template_args = template_config.generate_template_args() 53 | plugin = StackPlugin(region_id=None, credential=None) 54 | else: 55 | base_config = BaseConfig.create( 56 | project_config_file=config_file or DEFAULT_CONFIG_FILE, 57 | args={PROJECT: args}, 58 | project_path=project_path 59 | ) 60 | test_config = base_config.tests.pop(next(iter(base_config.tests))) 61 | 62 | credential = test_config.auth.credential 63 | template_config = test_config.template_config 64 | template_args = template_config.generate_template_args() 65 | plugin = StackPlugin(region_id=None, credential=credential) 66 | 67 | LOG.info(f'start getting policies of the templates.') 68 | 69 | policies = await plugin.generate_template_policy( 70 | **template_args 71 | ) 72 | TerminalPrinter._display_policies(policies=policies) -------------------------------------------------------------------------------- /iact3/cli_modules/preview.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import asyncio 4 | 5 | from iact3.cli import CliCore 6 | from iact3.cli_modules.delete import Delete 7 | from iact3.cli_modules.list import List 8 | 9 | from iact3.testing.ros_stack import StackTest 10 | from iact3.config import BaseConfig, PROJECT, REGIONS, TEMPLATE_CONFIG, TESTS, TestConfig, IAC_NAME, \ 11 | DEFAULT_PROJECT_ROOT, OssConfig, Auth, TEMPLATE_LOCATION, DEFAULT_CONFIG_FILE, DEFAULT_OUTPUT_DIRECTORY 12 | from iact3.exceptions import Iact3Exception 13 | 14 | from iact3.config import TemplateConfig 15 | from iact3.plugin.ros import StackPlugin 16 | from iact3.termial_print import TerminalPrinter 17 | 18 | LOG = logging.getLogger(__name__) 19 | 20 | class Preview: 21 | ''' 22 | Preview resources of templates. 23 | ''' 24 | 25 | def __init__(self, template: str = None, 26 | config_file: str = DEFAULT_CONFIG_FILE, 27 | regions: str = None): 28 | ''' 29 | :param template: path to a template 30 | :param config_file: path to a config file 31 | :param regions: comma separated list of regions 32 | :return: None 33 | ''' 34 | self.template = template 35 | self.config_file = config_file 36 | self.regions = regions 37 | 38 | @classmethod 39 | async def create(cls, template: str = None, 40 | config_file: str = None, 41 | regions: str = None, 42 | tags: dict = None): 43 | tests = await StackTest.from_file( 44 | template=template, 45 | project_config_file=config_file, 46 | regions=regions 47 | ) 48 | LOG.info(f'start previewing templates.') 49 | await StackTest.preview_stacks_result(tests) -------------------------------------------------------------------------------- /iact3/cli_modules/test.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from iact3.cli import CliCore 5 | from iact3.cli_modules.delete import Delete 6 | from iact3.cli_modules.list import List 7 | from iact3.config import DEFAULT_CONFIG_FILE 8 | from iact3.testing.ros_stack import StackTest 9 | 10 | LOG = logging.getLogger(__name__) 11 | 12 | 13 | class Test: 14 | ''' 15 | Performs functional tests on IaC templates. 16 | ''' 17 | 18 | @staticmethod 19 | @CliCore.longform_param_required('no_delete') 20 | @CliCore.longform_param_required('project_path') 21 | @CliCore.longform_param_required('test_names') 22 | @CliCore.longform_param_required('keep_failed') 23 | @CliCore.longform_param_required('dont_wait_for_delete') 24 | @CliCore.longform_param_required('failed') 25 | async def run(template: str = None, 26 | config_file: str = None, 27 | output_directory: str = None, 28 | regions: str = None, 29 | test_names: str = None, 30 | no_delete: bool = False, 31 | project_path: str = None, 32 | keep_failed: bool = False, 33 | dont_wait_for_delete: bool = False, 34 | generate_parameters: bool = False, 35 | log_format: str = None 36 | ) -> None: 37 | ''' 38 | tests whether IaC templates are able to successfully launch 39 | :param template: path to a template 40 | :param config_file: path to a config file 41 | :param output_directory: path to an output directory 42 | :param regions: comma separated list of regions to test in 43 | :param test_names: comma separated list of tests to run 44 | :param no_delete: don't delete stacks after test is complete 45 | :param project_path: root path of the project relative to config file, template file and output file 46 | :param keep_failed: do not delete failed stacks 47 | :param dont_wait_for_delete: exits immediately after calling delete stack 48 | :param generate_parameters: generate pseudo parameters 49 | :param log_format: comma separated list of log format (xml,json) 50 | :return: None 51 | ''' 52 | # todo --failed param 53 | tests = await StackTest.from_file( 54 | template=template, 55 | project_config_file=config_file, 56 | no_delete=no_delete, 57 | regions=regions, 58 | project_path=project_path, 59 | keep_failed=keep_failed, 60 | dont_wait_for_delete=dont_wait_for_delete, 61 | test_names=test_names 62 | ) 63 | if generate_parameters: 64 | Test._get_parameters(tests) 65 | return 66 | 67 | async with tests: 68 | await tests.report(output_directory, project_path, log_format) 69 | 70 | @staticmethod 71 | @CliCore.longform_param_required('stack_id') 72 | async def clean(regions: str = None, stack_id: str = None): 73 | ''' 74 | Manually clean up the stacks which were created by Iact3 75 | :param regions: comma separated list of regions to delete from, default will scan all regions 76 | :param stack_id: stack_id to delete from, default will scan all regions 77 | ''' 78 | await Delete.create(regions, stack_id=stack_id) 79 | 80 | @staticmethod 81 | async def list(regions: str = None): 82 | ''' 83 | List stacks which were created by Iact3 for all regions 84 | :param regions: comma separated list of regions to delete from, default will scan all regions 85 | ''' 86 | await List.create(regions) 87 | 88 | @staticmethod 89 | async def params(template: str = None, 90 | config_file: str = DEFAULT_CONFIG_FILE, 91 | regions: str = None): 92 | ''' 93 | Generate pseudo parameters 94 | :param template: path to a template 95 | :param config_file: path to a config file 96 | :param regions: comma separated list of regions 97 | ''' 98 | tests = await StackTest.from_file( 99 | template=template, 100 | project_config_file=config_file, 101 | regions=regions 102 | ) 103 | Test._get_parameters(tests) 104 | 105 | @staticmethod 106 | def _get_parameters(tests: StackTest): 107 | all_configs = tests.configs 108 | parameters = [ 109 | { 110 | 'TestName': con.name, 111 | 'TestRegion': con.region, 112 | 'Parameters': getattr(con.error, 'message', 'GetParameterError') if con.error else con.parameters 113 | } for con in all_configs 114 | ] 115 | LOG.info(json.dumps(parameters, sort_keys=True, indent=4, separators=(',', ': '), ensure_ascii=False)) 116 | return parameters 117 | -------------------------------------------------------------------------------- /iact3/cli_modules/validate.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import asyncio 4 | 5 | from iact3.cli import CliCore 6 | from iact3.cli_modules.delete import Delete 7 | from iact3.cli_modules.list import List 8 | 9 | from iact3.testing.ros_stack import StackTest 10 | from iact3.config import BaseConfig, PROJECT, REGIONS, TEMPLATE_CONFIG, TESTS, TestConfig, IAC_NAME, \ 11 | DEFAULT_PROJECT_ROOT, OssConfig, Auth, TEMPLATE_LOCATION, DEFAULT_CONFIG_FILE, DEFAULT_OUTPUT_DIRECTORY 12 | from iact3.exceptions import Iact3Exception 13 | 14 | from iact3.config import TemplateConfig 15 | from iact3.plugin.ros import StackPlugin 16 | from iact3.termial_print import TerminalPrinter 17 | 18 | LOG = logging.getLogger(__name__) 19 | 20 | class Validate: 21 | ''' 22 | Validate the templates. 23 | ''' 24 | 25 | def __init__(self, template: str = None, 26 | config_file: str = DEFAULT_CONFIG_FILE, 27 | regions: str = None): 28 | ''' 29 | :param template: path to a template 30 | :param config_file: path to a config file 31 | :param regions: comma separated list of regions 32 | ''' 33 | self.template = template 34 | self.config_file = config_file 35 | self.regions = regions 36 | 37 | 38 | @classmethod 39 | async def create(cls, template: str = None, 40 | config_file: str = None, 41 | regions: str = None): 42 | 43 | args = {} 44 | if regions: 45 | args[REGIONS] = regions.split(',') 46 | 47 | project_path = DEFAULT_PROJECT_ROOT 48 | 49 | 50 | if template: 51 | template_config = TemplateConfig(template_location=template) 52 | template_args = template_config.generate_template_args() 53 | plugin = StackPlugin(region_id=None, credential=None) 54 | else: 55 | base_config = BaseConfig.create( 56 | project_config_file=config_file or DEFAULT_CONFIG_FILE, 57 | args={PROJECT: args}, 58 | project_path=project_path 59 | ) 60 | test_config = base_config.tests.pop(next(iter(base_config.tests))) 61 | 62 | credential = test_config.auth.credential 63 | template_config = test_config.template_config 64 | template_args = template_config.generate_template_args() 65 | plugin = StackPlugin(region_id=None, credential=credential) 66 | 67 | LOG.info(f'start validating template.') 68 | 69 | template_validation = await plugin.validate_template( 70 | **template_args 71 | ) 72 | TerminalPrinter._display_validation(template_validation=template_validation) -------------------------------------------------------------------------------- /iact3/exceptions.py: -------------------------------------------------------------------------------- 1 | class Iact3Exception(Exception): 2 | """Raised when iact3 experiences a fatal error""" 3 | 4 | def __init__(self, message, code=None): 5 | self.code = code or 'Iact3Exception' 6 | self.message = message 7 | 8 | 9 | class InvalidActionError(Iact3Exception): 10 | """Exception raised for error when invalid action is supplied 11 | 12 | Attributes: 13 | expression -- input expression in which the error occurred 14 | """ 15 | 16 | def __init__(self, expression): 17 | self.expression = expression 18 | super().__init__(expression) 19 | -------------------------------------------------------------------------------- /iact3/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | LOG = logging.getLogger(__name__) 4 | 5 | 6 | class PrintMsg: 7 | header = "\x1b[1;41;0m" 8 | highlight = "\x1b[0;30;47m" 9 | name_color = "\x1b[0;37;44m" 10 | aqua = "\x1b[0;30;46m" 11 | green = "\x1b[0;30;42m" 12 | white = "\x1b[0;30;47m" 13 | orange = "\x1b[0;30;43m" 14 | red = "\x1b[0;30;41m" 15 | rst_color = "\x1b[0m" 16 | blod = "\033[1m" 17 | text_red = "\033[31m" 18 | text_green = "\033[32m" 19 | text_red_background_write = "\033[31;47m" 20 | CRITICAL = "{}[FATAL ]{} : ".format(red, rst_color) 21 | ERROR = "{}[ERROR ]{} : ".format(red, rst_color) 22 | DEBUG = "{}[DEBUG ]{} : ".format(aqua, rst_color) 23 | PASS = "{}[PASS ]{} : ".format(green, rst_color) 24 | INFO = "{}[INFO ]{} : ".format(white, rst_color) 25 | WARNING = "{}[WARN ]{} : ".format(orange, rst_color) 26 | left_top = "\u250f" 27 | right_top = "\u2513" 28 | left = "\u2523" 29 | left_bottom = "\u2517" 30 | right = "\u252B" 31 | right_bottom = "\u251B" 32 | top = "\u2501" 33 | 34 | 35 | class AppFilter(logging.Filter): 36 | def filter(self, record): 37 | default = PrintMsg.INFO 38 | record.color_loglevel = getattr(PrintMsg, record.levelname, default) 39 | return True 40 | 41 | 42 | def init_cli_logger(loglevel=None, log_prefix=None, logger=None): 43 | if logger: 44 | log = logger 45 | else: 46 | log = logging.getLogger(__package__) 47 | 48 | if log.hasHandlers(): 49 | for handler in log.handlers: 50 | log.removeHandler(handler) 51 | 52 | cli_handler = logging.StreamHandler() 53 | fmt = "%(asctime)s %(color_loglevel)s%(message)s" 54 | 55 | if log_prefix: 56 | fmt = f"%(asctime)s {log_prefix} %(color_loglevel)s%(message)s" 57 | 58 | formatter = logging.Formatter(fmt) 59 | cli_handler.setFormatter(formatter) 60 | cli_handler.addFilter(AppFilter()) 61 | log.addHandler(cli_handler) 62 | if loglevel: 63 | loglevel = getattr(logging, loglevel.upper(), 20) 64 | log.setLevel(loglevel) 65 | return log 66 | -------------------------------------------------------------------------------- /iact3/main.py: -------------------------------------------------------------------------------- 1 | import signal 2 | import sys 3 | 4 | from pkg_resources import get_distribution 5 | 6 | from iact3 import cli_modules 7 | from iact3.cli import CliCore, GLOBAL_ARGS, _get_log_level 8 | from iact3.generate_params import IAC_PACKAGE_NAME, IAC_NAME 9 | from iact3.logger import init_cli_logger 10 | from iact3.util import exit_with_code 11 | 12 | LOG = init_cli_logger(loglevel="ERROR") 13 | DESCRIPTION = 'Infrastructure as Code Templates Validation Test.' 14 | DEFAULT_PROFILE = '.' 15 | 16 | 17 | def sync_run(): 18 | """ 19 | Run the CLI synchronously. 20 | """ 21 | import asyncio 22 | from iact3.exceptions import Iact3Exception 23 | if sys.version_info[0] == 3 and sys.version_info[1] >= 7: 24 | loop = asyncio.new_event_loop() 25 | asyncio.set_event_loop(loop) 26 | try: 27 | loop.run_until_complete(run()) 28 | finally: 29 | loop.close() 30 | else: 31 | raise Iact3Exception("Please use Python 3.7+") 32 | 33 | 34 | async def run(): 35 | signal.signal(signal.SIGINT, _sigint_handler) 36 | log_level = _setup_logging(sys.argv) 37 | args = sys.argv[1:] 38 | if not args: 39 | args.append('-h') 40 | try: 41 | version = get_installed_version() 42 | cli = CliCore(IAC_NAME, cli_modules, DESCRIPTION, version, GLOBAL_ARGS.ARGS) 43 | cli.parse(args) 44 | _default_profile = cli.parsed_args.__dict__.get('_profile') 45 | if _default_profile: 46 | GLOBAL_ARGS.profile = _default_profile 47 | 48 | _log_prefix = cli.parsed_args.__dict__.get('_log_prefix') 49 | if _log_prefix: 50 | GLOBAL_ARGS.log_prefix = _log_prefix 51 | init_cli_logger(log_prefix=_log_prefix, logger=LOG) 52 | await cli.run() 53 | except Exception as e: 54 | LOG.error( 55 | '%s %s', e.__class__.__name__, str(e), exc_info=_print_tracebacks(log_level) 56 | ) 57 | exit_with_code(1) 58 | 59 | 60 | def _setup_logging(args, exit_func=exit_with_code): 61 | log_level = _get_log_level(args, exit_func=exit_func) 62 | LOG.setLevel(log_level) 63 | return log_level 64 | 65 | 66 | def _print_tracebacks(log_level): 67 | return log_level == 'DEBUG' 68 | 69 | 70 | def get_installed_version(): 71 | try: 72 | return get_distribution(IAC_PACKAGE_NAME).version 73 | except Exception: 74 | return '[local source] no pip module installed' 75 | 76 | 77 | def _sigint_handler(signum, frame): 78 | LOG.debug(f'SIGNAL {signum} caught at {frame}') 79 | exit_with_code(1) 80 | -------------------------------------------------------------------------------- /iact3/plugin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-ros-tool-iact3/64e93057e82e634cd79ee5c4e2d761ef31fe1e03/iact3/plugin/__init__.py -------------------------------------------------------------------------------- /iact3/plugin/base_plugin.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import asyncio 3 | import logging 4 | import os 5 | 6 | from Tea.core import TeaCore 7 | from Tea.exceptions import TeaException 8 | from Tea.model import TeaModel 9 | from alibabacloud_credentials import providers 10 | from alibabacloud_credentials.client import Client 11 | from alibabacloud_credentials.utils import auth_util 12 | from alibabacloud_credentials.utils.auth_constant import HOME 13 | from alibabacloud_tea_openapi.models import Config 14 | from alibabacloud_tea_util.models import RuntimeOptions 15 | from oslo_utils import importutils 16 | 17 | from iact3.plugin import error_code 18 | from iact3.util import pascal_to_snake 19 | 20 | LOG = logging.getLogger(__name__) 21 | 22 | DEFAULT_INI_CREDENTIAL_FILE_PATHS = [ 23 | os.path.join(HOME, '.alibabacloud/credentials.ini'), 24 | os.path.join(HOME, '.aliyun/credentials.ini') 25 | ] 26 | 27 | 28 | ASYNC_FLAG = '_async' 29 | RUNTIME_OPTIONS_FLAG = '_with_options' 30 | REQUEST_SUFFIX = 'Request' 31 | 32 | 33 | class CredentialsProvider(providers.DefaultCredentialsProvider): 34 | def __init__(self): 35 | super().__init__() 36 | self.user_configuration_providers = [ 37 | providers.EnvironmentVariableCredentialsProvider(), 38 | providers.ProfileCredentialsProvider(path=self._get_credential_file_path()) 39 | ] 40 | role_name = auth_util.environment_ECSMeta_data 41 | if role_name is not None: 42 | self.user_configuration_providers.append(providers.EcsRamRoleCredentialProvider(role_name)) 43 | self.user_configuration_providers.append(providers.CredentialsUriProvider()) 44 | 45 | def _get_credential_file_path(self): 46 | if auth_util.environment_credentials_file: 47 | return auth_util.environment_credentials_file 48 | for path in DEFAULT_INI_CREDENTIAL_FILE_PATHS: 49 | if os.path.exists(path): 50 | return path 51 | 52 | 53 | class CredentialClient(Client): 54 | 55 | def __init__(self, config=None): 56 | if config is None: 57 | provider = CredentialsProvider() 58 | self.cloud_credential = provider.get_credentials() 59 | return 60 | super(CredentialClient, self).__init__(config) 61 | 62 | 63 | class TeaSDKPlugin(metaclass=abc.ABCMeta): 64 | 65 | product = None 66 | 67 | def __init__(self, 68 | region_id: str, 69 | credential: CredentialClient = None, 70 | config_kwargs: dict = None, 71 | endpoint: str = None): 72 | self.region_id = region_id 73 | if not credential: 74 | credential = CredentialClient() 75 | self.credential = credential 76 | 77 | if not config_kwargs: 78 | config_kwargs = {} 79 | config_kwargs.update( 80 | region_id=region_id, 81 | credential=credential 82 | ) 83 | if endpoint: 84 | config_kwargs.update(endpoint=endpoint) 85 | 86 | self.config = Config(**config_kwargs) 87 | self.endpoint = self.config.endpoint 88 | self._client = None 89 | self.runtime_option = RuntimeOptions(**self.runtime_kwargs()) 90 | 91 | @abc.abstractmethod 92 | def api_client(self): 93 | raise NotImplementedError 94 | 95 | @abc.abstractmethod 96 | def models_path(self, action_name): 97 | raise NotImplementedError 98 | 99 | def runtime_kwargs(self): 100 | return { 101 | 'autoretry': True, 102 | 'max_attempts': 3 103 | } 104 | 105 | @property 106 | def client(self): 107 | if not self._client: 108 | client = self.api_client()(self.config) 109 | if not self.endpoint: 110 | self.endpoint = getattr(client, '_endpoint', '') 111 | return self.api_client()(self.config) 112 | return self._client 113 | 114 | async def send_request(self, request_name: str, ignoreException: bool=False, **kwargs) -> dict: 115 | request = self._build_request(request_name, **kwargs) 116 | api_name = self._get_api_name(request_name) 117 | action_name = self._get_action_name(api_name) 118 | func = getattr(self.client, action_name) 119 | try: 120 | resp = await func(request, self.runtime_option) 121 | except TeaException as ex: 122 | LOG.debug(f'plugin exception: {self.product} {self.endpoint} {api_name} {request.to_map()} {ex.data}') 123 | if ignoreException: 124 | return ex.data 125 | raise ex 126 | if not isinstance(resp, TeaModel): 127 | LOG.error(f'plugin response: {self.product} {self.endpoint} {api_name} {request.to_map()} {resp}') 128 | raise TeaException(dict( 129 | code=error_code.UNKNOWN_ERROR, 130 | message='The response of TeaSDK is not TeaModel.', 131 | data=resp 132 | )) 133 | resp = TeaCore.to_map(resp) 134 | LOG.debug(f'plugin response: {self.product} {self.endpoint} {api_name} {request.to_map()} {resp}') 135 | return resp.get('body', {}) 136 | 137 | def _get_api_name(self, request_name): 138 | if request_name.endswith(REQUEST_SUFFIX): 139 | suffix_len = len(REQUEST_SUFFIX) 140 | api_name = request_name[:-suffix_len] 141 | else: 142 | api_name = request_name 143 | return api_name 144 | 145 | def _get_action_name(self, api_name): 146 | action_name = pascal_to_snake(api_name) 147 | if not action_name.endswith(RUNTIME_OPTIONS_FLAG): 148 | action_name = f'{action_name}{RUNTIME_OPTIONS_FLAG}' 149 | if not action_name.endswith(ASYNC_FLAG): 150 | action_name = f'{action_name}{ASYNC_FLAG}' 151 | return action_name 152 | 153 | def _build_request(self, request_name, **kwargs): 154 | if 'RegionId' not in kwargs: 155 | kwargs['RegionId'] = self.region_id 156 | if not request_name.endswith('Request'): 157 | request_name = f'{request_name}Request' 158 | class_path = self.models_path(request_name) 159 | request = importutils.import_class(class_path)() 160 | request = request.from_map(kwargs) 161 | return request 162 | 163 | @staticmethod 164 | def _convert_tags(tags: dict, kwargs, tag_key='Tags'): 165 | if not tags: 166 | return 167 | assert isinstance(tags, dict) 168 | kwargs[tag_key] = [dict(Key=k, Value=v) for k, v in tags.items() if v is not None] 169 | 170 | PAGE_NUMBER, PAGE_SIZE, TOTAL_COUNT, TOTAL_PAGES, TOTAL = \ 171 | 'PageNumber', 'PageSize', 'TotalCount', 'TotalPages', 'Total' 172 | PAGE_OUTER_KEY = None 173 | 174 | async def fetch_all(self, request, kwargs, *keys): 175 | kwargs = kwargs.copy() 176 | if self.PAGE_SIZE not in kwargs: 177 | kwargs[self.PAGE_SIZE] = 50 178 | 179 | ex = [] 180 | result = [] 181 | 182 | # first fetch 183 | kwargs[self.PAGE_NUMBER] = 1 184 | resp = await self.send_request(request, **kwargs) 185 | if self.PAGE_OUTER_KEY: 186 | resp = resp.get(self.PAGE_OUTER_KEY) 187 | values = self._get_from_resp(resp, *keys) 188 | result.extend(values) 189 | 190 | # calculate total pages 191 | if self.TOTAL_COUNT in resp: 192 | total_pages = (resp[self.TOTAL_COUNT] - 1) // kwargs[self.PAGE_SIZE] + 1 193 | elif self.TOTAL in resp: 194 | total_pages = (resp[self.TOTAL] - 1) // kwargs[self.PAGE_SIZE] + 1 195 | else: 196 | total_pages = resp[self.TOTAL_PAGES] 197 | 198 | if total_pages <= 1: 199 | return result 200 | 201 | # concurrent fetch 202 | async def fetch_one_page(page, params): 203 | try: 204 | params = params.copy() 205 | params[self.PAGE_NUMBER] = page 206 | resp = await self.send_request(request, **params) 207 | if self.PAGE_OUTER_KEY: 208 | resp = resp.get(self.PAGE_OUTER_KEY) 209 | values = self._get_from_resp(resp, *keys) 210 | result.extend(values) 211 | except Exception as e: 212 | LOG.error(e) 213 | ex.append(e) 214 | 215 | tasks = [] 216 | for i in range(2, total_pages + 1): 217 | task = asyncio.create_task(fetch_one_page(i, kwargs)) 218 | tasks.append(task) 219 | await asyncio.gather(*tasks) 220 | 221 | if ex: 222 | raise ex[0] 223 | 224 | return result 225 | 226 | @staticmethod 227 | def _get_from_resp(resp, *keys): 228 | values = [] 229 | last_index = len(keys) - 1 230 | for i, key in enumerate(keys): 231 | if i == last_index: 232 | resp = resp.get(key, []) 233 | values.extend(resp) 234 | else: 235 | resp = resp.get(key, {}) 236 | return values 237 | -------------------------------------------------------------------------------- /iact3/plugin/ecs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from alibabacloud_ecs20140526.client import Client 3 | 4 | from iact3.plugin.base_plugin import TeaSDKPlugin 5 | 6 | 7 | class EcsBasePlugin(TeaSDKPlugin): 8 | product = 'ECS' 9 | 10 | def api_client(self): 11 | return Client 12 | 13 | def models_path(self, action_name): 14 | return 'alibabacloud_ecs20140526.models.{}'.format(action_name) 15 | 16 | def runtime_kwargs(self): 17 | return { 18 | 'autoretry': True, 19 | 'max_attempts': 3, 20 | 'read_timeout': 60000, 21 | 'connect_timeout': 60000 22 | } 23 | 24 | 25 | class EcsPlugin(EcsBasePlugin): 26 | 27 | async def get_security_group(self, vpc_id: str = None, security_group_id: str = None): 28 | kwargs = dict( 29 | VpcId=vpc_id, 30 | SecurityGroupIds=security_group_id 31 | ) 32 | sgs = await self.fetch_all('DescribeSecurityGroups', kwargs, 'SecurityGroups', 'SecurityGroup') 33 | for sg in sgs: 34 | if not sg['ServiceManaged']: 35 | return sg 36 | -------------------------------------------------------------------------------- /iact3/plugin/error_code.py: -------------------------------------------------------------------------------- 1 | COMMON_RETRY_EXCEPTIONS = ( 2 | CONNECTION_TIMEOUT, INTERNAL_ERROR, SERVICE_UNAVAILABLE, UNKNOWN_ERROR, LAST_TOKEN_PROCESSING, 3 | CONNECTION_ERROR, CONNECTION_ABORTED, REQUEST_UNKNOWN_TIMEOUT, TOKEN_PROCESSING, SIGNATURE_NONCE_USED, 4 | IDEMPOTENT_PROCESSING, UPPER_IDEMPOTENT_PROCESSING, OPERATION_FAILED_LAST_TOKEN_PROCESSING, 5 | BAD_GATEWAY 6 | ) = ( 7 | 'ConnectionTimeout', 'InternalError', 'ServiceUnavailable', 'UnknownError', 'LastTokenProcessing', 8 | 'ConnectionError', 'ConnectionAborted', 'RequestUnknownTimeout', 'TOKEN_PROCESSING', 'SignatureNonceUsed', 9 | 'IdempotentProcessing', 'IDEMPOTENCE_PROCESSING', 'OperationFailed.LastTokenProcessing', 10 | 'BadGateway' 11 | ) 12 | 13 | BASE_RETRY_EXCEPTIONS = ( 14 | CONNECTION_ERROR, SERVICE_UNAVAILABLE, TOKEN_PROCESSING, SIGNATURE_NONCE_USED, 15 | IDEMPOTENT_PROCESSING, LAST_TOKEN_PROCESSING, BAD_GATEWAY 16 | ) 17 | 18 | THROTTLING_RETRY_EXCEPTIONS = ( 19 | THROTTLING, 20 | THROTTLING_USER, 21 | THROTTLING_API, 22 | ) = ( 23 | 'Throttling', 24 | 'Throttling.User', 25 | 'Throttling.API', 26 | ) 27 | 28 | OTHER_ERRORS = ( 29 | UNKNOWN, CONNECT_ERROR 30 | ) = ( 31 | 'Unknown', 'ConnectError' 32 | ) 33 | -------------------------------------------------------------------------------- /iact3/plugin/oss.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | 4 | import oss2 5 | from alibabacloud_credentials import credentials 6 | from retrying import retry 7 | 8 | from iact3.plugin.base_plugin import CredentialClient 9 | 10 | 11 | def retry_on_exception(exception): 12 | return isinstance(exception, oss2.exceptions.RequestError) 13 | 14 | 15 | class OssPlugin: 16 | 17 | def __init__(self, region_id: str, 18 | bucket_name: str, 19 | endpoint: str = None, 20 | credential: CredentialClient = None, 21 | **kwargs): 22 | self.region_id = region_id 23 | self.auth = self._get_auth(credential) 24 | if not endpoint: 25 | endpoint = f'https://oss-{self.region_id}.aliyuncs.com' 26 | self.endpoint = endpoint 27 | self.client = oss2.Bucket( 28 | self.auth, self.endpoint, bucket_name, 29 | app_name='iact3', 30 | connect_timeout=30, 31 | **kwargs) 32 | 33 | def _get_auth(self, cred: CredentialClient = None): 34 | cred_client = CredentialClient() if cred is None else cred 35 | credential = cred_client.cloud_credential 36 | if isinstance(credential, credentials.AccessKeyCredential): 37 | auth = oss2.Auth( 38 | credential.access_key_id, 39 | credential.access_key_secret 40 | ) 41 | elif isinstance(credential, credentials.StsCredential): 42 | auth = oss2.StsAuth( 43 | credential.access_key_id, 44 | credential.access_key_secret, 45 | credential.security_token 46 | ) 47 | else: 48 | auth = oss2.AnonymousAuth() 49 | return auth 50 | 51 | @staticmethod 52 | def _encode_callback(callback_params): 53 | cb_str = json.dumps(callback_params).strip() 54 | return oss2.compat.to_string(base64.b64encode(oss2.compat.to_bytes(cb_str))) 55 | 56 | @retry(retry_on_exception=retry_on_exception, stop_max_attempt_number=3, wait_fixed=5) 57 | def put_object_with_string(self, object_name: str, strings: str, 58 | callback_params: dict = None, callback_var_params: dict = None): 59 | params = {} 60 | if callback_params: 61 | params['x-oss-callback'] = self._encode_callback(callback_params) 62 | if callback_var_params: 63 | params['x-oss-callback-var'] = self._encode_callback(callback_var_params) 64 | if params: 65 | self.client.put_object(object_name, strings, params) 66 | else: 67 | self.client.put_object(object_name, strings) 68 | 69 | @retry(retry_on_exception=retry_on_exception, stop_max_attempt_number=3, wait_fixed=5) 70 | def put_local_file(self, object_name: str, local_file: str): 71 | self.client.put_object_from_file(object_name, local_file) 72 | 73 | @retry(retry_on_exception=retry_on_exception, stop_max_attempt_number=3, wait_fixed=5) 74 | def object_exists(self, object_name: str): 75 | return self.client.object_exists(object_name) 76 | 77 | @retry(retry_on_exception=retry_on_exception, stop_max_attempt_number=3, wait_fixed=5) 78 | def get_object_content(self, object_name: str): 79 | return self.client.get_object(object_name) 80 | 81 | @retry(retry_on_exception=retry_on_exception, stop_max_attempt_number=3, wait_fixed=5) 82 | def get_object_meta(self, object_name: str): 83 | return self.client.get_object_meta(object_name) 84 | 85 | @retry(retry_on_exception=retry_on_exception, stop_max_attempt_number=3, wait_fixed=5) 86 | def bucket_exist(self): 87 | try: 88 | self.client.get_bucket_info() 89 | except Exception as ex: 90 | if isinstance(ex, oss2.exceptions.NoSuchBucket): 91 | return False 92 | raise 93 | return True 94 | -------------------------------------------------------------------------------- /iact3/plugin/ros.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from Tea.exceptions import TeaException 4 | from alibabacloud_ros20190910.client import Client as ROSClient 5 | 6 | from iact3.plugin.base_plugin import TeaSDKPlugin 7 | 8 | 9 | class ROSPlugin(TeaSDKPlugin): 10 | 11 | product = 'ROS' 12 | 13 | def api_client(self): 14 | return ROSClient 15 | 16 | def models_path(self, action_name): 17 | return 'alibabacloud_ros20190910.models.{}'.format(action_name) 18 | 19 | def runtime_kwargs(self): 20 | return { 21 | 'autoretry': True, 22 | 'max_attempts': 3, 23 | 'read_timeout': 60000, 24 | 'connect_timeout': 60000 25 | } 26 | 27 | 28 | class StackPlugin(ROSPlugin): 29 | 30 | IGNORE_ERRORS = ('StackNotFound',) 31 | 32 | @staticmethod 33 | def _convert_parameters(parameters: dict, kwargs: dict): 34 | if not parameters: 35 | return 36 | assert isinstance(parameters, dict) 37 | 38 | json_dumps_param = {} 39 | for k, v in parameters.items(): 40 | json_dumps_param[k] = json.dumps(v) if isinstance(v, (list, dict)) else v 41 | 42 | kwargs['Parameters'] = [dict(ParameterKey=k, ParameterValue=v) for k, v in json_dumps_param.items() 43 | if v is not None] 44 | 45 | @staticmethod 46 | def _convert_notification_urls(notification_urls, kwargs): 47 | if not notification_urls: 48 | return 49 | assert isinstance(notification_urls, list) 50 | kwargs['NotificationUrls'] = notification_urls 51 | 52 | async def create_stack(self, stack_name: str, template_body: str = None, 53 | parameters: dict = None, timeout_in_minutes: int = None, 54 | client_token=None, disable_rollback=None, 55 | stack_policy_url=None, stack_policy_body=None, 56 | notification_urls=None, deletion_protection=None, 57 | create_option=None, tags=None, 58 | ram_role_name=None, template_id=None, 59 | template_version=None, resource_group_id=None, 60 | template_url=None): 61 | kwargs = dict( 62 | StackName=stack_name, 63 | TemplateBody=template_body, 64 | TimeoutInMinutes=timeout_in_minutes, 65 | ClientToken=client_token, 66 | DisableRollback=disable_rollback, 67 | StackPolicyURL=stack_policy_url, 68 | StackPolicyBody=stack_policy_body, 69 | NotificationURLs=notification_urls, 70 | DeletionProtection=deletion_protection, 71 | CreateOption=create_option, 72 | RamRoleName=ram_role_name, 73 | TemplateId=template_id, 74 | TemplateVersion=template_version, 75 | ResourceGroupId=resource_group_id, 76 | TemplateURL=template_url 77 | ) 78 | self._convert_parameters(parameters, kwargs) 79 | self._convert_notification_urls(notification_urls, kwargs) 80 | self._convert_tags(tags, kwargs) 81 | result = await self.send_request('CreateStackRequest', **kwargs) 82 | return result['StackId'] 83 | 84 | async def delete_stack(self, stack_id, retain_all_resources=None): 85 | kwargs = dict( 86 | StackId=stack_id, 87 | RetainAllResources=retain_all_resources 88 | ) 89 | try: 90 | return await self.send_request('DeleteStackRequest', **kwargs) 91 | except TeaException as ex: 92 | if ex.code not in self.IGNORE_ERRORS: 93 | raise 94 | 95 | async def get_stack(self, stack_id, client_token=None, output_option=None): 96 | kwargs = dict( 97 | StackId=stack_id, 98 | ClientToken=client_token, 99 | OutputOption=output_option, 100 | ) 101 | try: 102 | return await self.send_request('GetStackRequest', **kwargs) 103 | except TeaException as ex: 104 | if ex.code not in self.IGNORE_ERRORS: 105 | raise 106 | 107 | async def list_stacks(self, stack_id=None, stack_name=None): 108 | request_kwargs = dict( 109 | StackId=stack_id, 110 | StackName=[stack_name] if stack_name else None 111 | ) 112 | result = await self.send_request('ListStacksRequest', **request_kwargs) 113 | return result.get('Stacks') 114 | 115 | async def fetch_all_stacks(self, tags, stack_id=None): 116 | kwargs = {'StackId': stack_id} 117 | self._convert_tags(tags, kwargs, tag_key='Tag') 118 | return await self.fetch_all('ListStacksRequest', kwargs, 'Stacks') 119 | 120 | async def list_stack_resources(self, stack_id): 121 | kwargs = dict( 122 | StackId=stack_id 123 | ) 124 | result = await self.send_request('ListStackResourcesRequest', **kwargs) 125 | return result.get('Resources') 126 | 127 | async def get_stack_resource(self, stack_id, logical_resource_id, show_resource_attributes=False, 128 | resource_attributes=None): 129 | kwargs = dict( 130 | StackId=stack_id, 131 | LogicalResourceId=logical_resource_id 132 | ) 133 | if show_resource_attributes: 134 | kwargs.update( 135 | ShowResourceAttributes='true' 136 | ) 137 | elif resource_attributes: 138 | kwargs.update( 139 | ResourceAttributes=resource_attributes, 140 | ) 141 | return await self.send_request('GetStackResourceRequest', **kwargs) 142 | 143 | async def list_stack_events(self, stack_id): 144 | kwargs = dict( 145 | StackId=stack_id 146 | ) 147 | return await self.fetch_all('ListStackEventsRequest', kwargs, 'Events') 148 | 149 | async def get_regions(self) -> list: 150 | response = await self.send_request('DescribeRegionsRequest') 151 | return [region['RegionId'] for region in response['Regions'] or []] 152 | 153 | async def get_parameter_constraints(self, template_body: str = None, 154 | template_url: str = None, template_id: str = None, 155 | template_version: str = None, parameters_key_filter: list = None, 156 | parameters: dict = None, parameters_order: list = None, 157 | client_token: str = None): 158 | kwargs = dict( 159 | TemplateBody=template_body, 160 | TemplateURL=template_url, 161 | TemplateId=template_id, 162 | TemplateVersion=template_version, 163 | ParametersKeyFilter=parameters_key_filter, 164 | ParametersOrder=parameters_order, 165 | ClientToken=client_token 166 | ) 167 | self._convert_parameters(parameters, kwargs) 168 | result = await self.send_request('GetTemplateParameterConstraints', **kwargs) 169 | return result['ParameterConstraints'] 170 | 171 | async def get_template(self, template_id: str, template_version: str = None): 172 | kwargs = dict( 173 | TemplateId=template_id, 174 | TemplateVersion=template_version 175 | ) 176 | return await self.send_request('GetTemplate', **kwargs) 177 | 178 | async def get_template_estimate_cost(self, template_body: str = None, 179 | parameters: dict = None, region_id: str = None, 180 | template_url: str = None): 181 | kwargs = dict( 182 | TemplateBody=template_body, 183 | TemplateURL=template_url, 184 | RegionId=region_id 185 | ) 186 | self._convert_parameters(parameters, kwargs) 187 | result = await self.send_request('GetTemplateEstimateCostRequest', **kwargs) 188 | return result['Resources'] 189 | 190 | async def validate_template(self, template_body: str = None, 191 | region_id: str = None, template_url: str = None): 192 | kwargs = dict( 193 | TemplateBody=template_body, 194 | TemplateURL=template_url, 195 | RegionId=region_id 196 | ) 197 | result = await self.send_request('ValidateTemplateRequest', ignoreException=True, **kwargs) 198 | return result 199 | 200 | async def preview_stack(self, template_body: str = None, 201 | parameters: dict = None, region_id: str = None, 202 | template_url: str = None, stack_name: str = None): 203 | kwargs = dict( 204 | TemplateBody=template_body, 205 | TemplateURL=template_url, 206 | RegionId=region_id, 207 | StackName=stack_name 208 | ) 209 | self._convert_parameters(parameters, kwargs) 210 | result = await self.send_request('PreviewStackRequest', **kwargs) 211 | return result['Stack']['Resources'] 212 | 213 | async def generate_template_policy(self, template_body: str = None, template_url: str = None): 214 | kwargs = dict( 215 | TemplateBody=template_body, 216 | TemplateURL=template_url 217 | ) 218 | result = await self.send_request('GenerateTemplatePolicyRequest', ignoreException=True, **kwargs) 219 | return result['Policy'] -------------------------------------------------------------------------------- /iact3/plugin/vpc.py: -------------------------------------------------------------------------------- 1 | from alibabacloud_vpc20160428.client import Client 2 | 3 | from iact3.plugin.base_plugin import TeaSDKPlugin 4 | 5 | 6 | class VpcBasePlugin(TeaSDKPlugin): 7 | 8 | product = 'VPC' 9 | 10 | def api_client(self): 11 | return Client 12 | 13 | def models_path(self, action_name): 14 | return 'alibabacloud_vpc20160428.models.{}'.format(action_name) 15 | 16 | 17 | class VpcPlugin(VpcBasePlugin): 18 | 19 | async def get_one_vpc(self, vpc_id: str = None): 20 | kwargs = dict(VpcId=vpc_id, PageSize=50) 21 | response = await self.send_request('DescribeVpcsRequest', **kwargs) 22 | vpcs = response['Vpcs']['Vpc'] 23 | for vpc in vpcs: 24 | if vpc['VSwitchIds']['VSwitchId']: 25 | return vpc 26 | 27 | async def get_one_vswitch(self, vpc_id: str = None, vsw_id: str = None, zone_id: str = None): 28 | kwargs = dict(VpcId=vpc_id, VSwitchId=vsw_id, ZoneId=zone_id) 29 | response = await self.send_request('DescribeVSwitchesRequest', **kwargs) 30 | vsws = response['VSwitches']['VSwitch'] 31 | for vsw in vsws: 32 | if vsw['AvailableIpAddressCount'] > 1: 33 | return vsw 34 | -------------------------------------------------------------------------------- /iact3/report/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-ros-tool-iact3/64e93057e82e634cd79ee5c4e2d761ef31fe1e03/iact3/report/__init__.py -------------------------------------------------------------------------------- /iact3/report/html.css: -------------------------------------------------------------------------------- 1 | /*** author: Tony Vattathil avattathil@gmail.com ***/ 2 | /*** license: Apache 2.0 ***/ 3 | 4 | @import url(https://fonts.googleapis.com/css?family=Roboto:400,500,700,300,100); 5 | @import url(https://fonts.googleapis.com/css?family=Comfortaa:700); 6 | html {} body { 7 | background-color: #ecf0f1; 8 | font-family: "Roboto", helvetica, arial, sans-serif; 9 | font-size: 12px; 10 | font-weight: 200; 11 | text-rendering: optimizeLegibility; 12 | margin: 0; 13 | padding: 1px; 14 | } 15 | div.iact3-logo { 16 | display: block; 17 | margin: auto; 18 | width: 100%; 19 | border: 0; 20 | padding: 0px; 21 | } 22 | div.table-title { 23 | background: #ecf0f1; 24 | display: block; 25 | margin: auto; 26 | max-width: 100%; 27 | width: 100%; 28 | } 29 | div.header-table-fill { 30 | display: block; 31 | margin: 0; 32 | max-width: 100%; 33 | } 34 | .iact3-logo h3 { 35 | color: orange; 36 | font-size: 35px; 37 | margin: 0; 38 | width: 100%; 39 | text-align: right; 40 | font-family: Comfortaa; 41 | text-shadow: #ffffff 0px 1px 1px; 42 | } 43 | .test-info h3 { 44 | color: #3498db; 45 | font-size: 15px; 46 | margin: 0; 47 | width: 100%; 48 | text-align: center; 49 | font-family: Comfortaa; 50 | text-shadow: #ffffff 0px 0px 0px; 51 | } 52 | 53 | .test-footer td { 54 | background-color: #ecf0f1; 55 | padding: 4px; 56 | margin: auto; 57 | width: 100%; 58 | border-top: 2px solid #3498db; 59 | } 60 | .test-footer h3 { 61 | background-color: #ecf0f1; 62 | padding: 2px; 63 | margin: auto; 64 | width: 100%; 65 | border-top: 1px solid black; 66 | } 67 | .iact3-logo td { 68 | padding: 0px; 69 | margin: auto; 70 | width: 100%; 71 | padding: 5px; 72 | } 73 | .table-title h3 { 74 | font-size: 20px; 75 | font-weight: 400; 76 | font-style: normal; 77 | font-family: "Roboto", helvetica, arial, sans-serif; 78 | } 79 | .header-table-fill { 80 | border-radius: 3px; 81 | border-collapse: collapse; 82 | margin: auto; 83 | max-width: 100%; 84 | padding: 0px; 85 | width: 100%; 86 | } 87 | .header-table-fill th { 88 | background: #ecf0f1; 89 | border-bottom: 4px solid #3498db; 90 | margin: auto; 91 | width: 100%; 92 | padding: 5px; 93 | } 94 | .header-table-fill tr { 95 | border: 0; 96 | border-right: none; 97 | } 98 | .header-table-fill td { 99 | color: #933333; 100 | font-size: 14px; 101 | border: 0; 102 | } 103 | .table-fill { 104 | background: white; 105 | border-radius: 3px; 106 | border-collapse: collapse; 107 | margin: auto; 108 | max-width: 98%; 109 | width: 100%; 110 | box-shadow: 0 8px 10px rgba(0, 0, 0, 0.2); 111 | } 112 | a { 113 | text-decoration: none; 114 | } 115 | th { 116 | color: #D5DDE5; 117 | background: #1b1e24; 118 | border-bottom: 4px solid #9ea7af; 119 | border-right: 1px solid #343a45; 120 | font-size: 12px; 121 | font-weight: 100; 122 | padding: 8px; 123 | text-align: left; 124 | text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); 125 | vertical-align: middle; 126 | } 127 | th:first-child { 128 | border-top-left-radius: 3px; 129 | } 130 | th:last-child { 131 | border-top-right-radius: 3px; 132 | border-right: none; 133 | } 134 | tr { 135 | border-top: 1px solid #C1C3D1; 136 | border-bottom: 1px solid #C1C3D1; 137 | color: #666B85; 138 | font-size: 16px; 139 | font-weight: normal; 140 | text-shadow: 0 1px 1px rgba(256, 256, 256, 0.1); 141 | } 142 | tr:first-child { 143 | border-top: none; 144 | } 145 | tr:last-child { 146 | border-bottom: none; 147 | } 148 | tr:last-child td:first-child { 149 | border-bottom-left-radius: 3px; 150 | } 151 | tr:last-child td:last-child { 152 | border-bottom-right-radius: 3px; 153 | } 154 | td { 155 | background: #ffffff; 156 | padding: 12px; 157 | text-align: left; 158 | vertical-align: middle; 159 | font-weight: 300; 160 | font-size: 12px; 161 | text-shadow: -1px -1px 1px rgba(0, 0, 0, 0.1); 162 | border-right: 1px solid #C1C3D1; 163 | } 164 | td.test-green { 165 | text-align: center; 166 | background-color: #98FF98; 167 | } 168 | td.test-red { 169 | text-align: center; 170 | background-color: #FCB3BC; 171 | } 172 | td:last-child { 173 | border-right: 0px; 174 | } 175 | th.text-left { 176 | text-align: left; 177 | } 178 | th.text-center { 179 | text-align: center; 180 | } 181 | th.text-right { 182 | text-align: right; 183 | } 184 | td.text-left { 185 | text-align: left; 186 | } 187 | td.text-center { 188 | text-align: center; 189 | } 190 | td.text-right { 191 | text-align: right; 192 | } -------------------------------------------------------------------------------- /iact3/testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-ros-tool-iact3/64e93057e82e634cd79ee5c4e2d761ef31fe1e03/iact3/testing/__init__.py -------------------------------------------------------------------------------- /iact3/testing/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import logging 3 | import uuid 4 | from pathlib import Path 5 | from typing import Any, Type, TypeVar, List 6 | 7 | from iact3.config import BaseConfig, PROJECT, REGIONS, TEMPLATE_CONFIG, TestConfig, IAC_NAME, \ 8 | DEFAULT_PROJECT_ROOT, OssConfig, Auth, TEMPLATE_LOCATION, DEFAULT_CONFIG_FILE, DEFAULT_OUTPUT_DIRECTORY 9 | from iact3.exceptions import Iact3Exception 10 | from iact3.plugin.oss import OssPlugin 11 | from iact3.report.generate_reports import ReportBuilder 12 | from iact3.stack import Stacker 13 | from iact3.termial_print import TerminalPrinter 14 | 15 | LOG = logging.getLogger(__name__) 16 | 17 | T = TypeVar("T", bound="Test") 18 | 19 | 20 | class Base(metaclass=abc.ABCMeta): 21 | 22 | def __init__(self, project_name: str, configs: List[TestConfig], 23 | no_delete: bool = False, keep_failed: bool = False, 24 | dont_wait_for_delete: bool = False, rerun_failed: bool = False, 25 | oss_config: OssConfig = None, auth: Auth = None 26 | ): 27 | self.project_name = project_name 28 | self.configs = configs 29 | self.passed: bool = False 30 | self.result: Any = None 31 | self.printer = TerminalPrinter() 32 | self.stacker: Stacker = None 33 | self.uid = uuid.uuid4() 34 | 35 | self.no_delete = no_delete 36 | self.keep_failed = keep_failed 37 | self.dont_wait_for_delete = dont_wait_for_delete 38 | self.rerun_failed = rerun_failed 39 | self.oss_config = oss_config 40 | self.auth = auth 41 | 42 | async def __aenter__(self) -> Any: 43 | LOG.info(f'test {self.uid} start running.') 44 | try: 45 | await self.run() 46 | except BaseException as ex: 47 | await self.clean_up() 48 | raise ex 49 | 50 | return self.result 51 | 52 | async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: 53 | await self.clean_up() 54 | 55 | @classmethod 56 | async def from_file(cls: Type[T], 57 | template: str, 58 | project_config_file: str, 59 | regions: str, 60 | project_path: str = None, 61 | no_delete: bool = False, 62 | keep_failed: bool = False, 63 | dont_wait_for_delete: bool = False, 64 | rerun_failed: bool = False, 65 | test_names: str = None 66 | ) -> T: 67 | args = {} 68 | if regions: 69 | args[REGIONS] = regions.split(',') 70 | if project_path: 71 | project_root = Path(project_path).expanduser().resolve() 72 | if template: 73 | template = template.lstrip('/') 74 | template_path = project_root / template 75 | args[TEMPLATE_CONFIG] = {TEMPLATE_LOCATION: str(template_path)} 76 | else: 77 | args[TEMPLATE_CONFIG] = {TEMPLATE_LOCATION: str(project_root)} 78 | else: 79 | project_path = DEFAULT_PROJECT_ROOT 80 | if template: 81 | args[TEMPLATE_CONFIG] = {TEMPLATE_LOCATION: template} 82 | 83 | base_config = BaseConfig.create( 84 | project_config_file=project_config_file or DEFAULT_CONFIG_FILE, 85 | args={PROJECT: args}, 86 | project_path=project_path 87 | ) 88 | project_name = base_config.project.name 89 | if not project_name: 90 | raise Iact3Exception('project name should be specified') 91 | configs = await base_config.get_all_configs(test_names) 92 | return cls(project_name, configs, 93 | no_delete=no_delete, 94 | keep_failed=keep_failed, 95 | dont_wait_for_delete=dont_wait_for_delete, 96 | rerun_failed=rerun_failed, 97 | oss_config=base_config.project.oss_config, 98 | auth=base_config.general.auth) 99 | 100 | async def report(self, output_directory, project_path=None, log_format=None): 101 | project_root = Path(project_path).expanduser().resolve() if project_path else DEFAULT_PROJECT_ROOT 102 | output_directory = output_directory or DEFAULT_OUTPUT_DIRECTORY 103 | report_path = project_root / output_directory 104 | report_path.mkdir(exist_ok=True) 105 | reporter = ReportBuilder(self.stacker, report_path) 106 | file_names = await reporter.create_logs(log_format) 107 | index = await reporter.generate_report() 108 | self._upload_to_oss(report_path, index, file_names) 109 | 110 | def _upload_to_oss(self, report_path: Path, index: str, file_names: list): 111 | bucket = self.oss_config.bucket_name 112 | region = self.oss_config.bucket_region 113 | if bucket and region: 114 | LOG.info(f'starting upload reports to oss bucket {bucket} ' 115 | f'which is in {region} region') 116 | oss_prefix = self.oss_config.object_prefix or f'{report_path.name}-{self.uid}' 117 | oss_prefix = f'{IAC_NAME}/{oss_prefix}' 118 | oss_plugin = OssPlugin( 119 | region_id=region, bucket_name=bucket, credential=self.auth.credential) 120 | 121 | for file_name in file_names: 122 | oss_plugin.put_local_file(f'{oss_prefix}/{file_name}', report_path / file_name) 123 | 124 | callback_config = self.oss_config.callback_params 125 | if callback_config.callback_url: 126 | callback_params = { 127 | 'callbackUrl': callback_config.callback_url, 128 | 'callbackHost': callback_config.callback_host, 129 | 'callbackBody': callback_config.callback_body, 130 | 'callbackBodyType': callback_config.callback_body_type, 131 | } 132 | callback_var_params = callback_config.callback_var_params 133 | oss_plugin.put_object_with_string( 134 | f'{oss_prefix}/index.html', index, callback_params, callback_var_params) 135 | else: 136 | oss_plugin.put_object_with_string(f'{oss_prefix}/index.html', index) 137 | 138 | @abc.abstractmethod 139 | async def run(self): 140 | raise NotImplementedError 141 | 142 | @abc.abstractmethod 143 | async def clean_up(self): 144 | raise NotImplementedError 145 | -------------------------------------------------------------------------------- /iact3/testing/ros_stack.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | 4 | from iact3.exceptions import InvalidActionError 5 | from iact3.stack import Stacker 6 | from iact3.testing.base import Base 7 | 8 | from typing import Any, Type, TypeVar, List 9 | 10 | from iact3.termial_print import TerminalPrinter 11 | 12 | LOG = logging.getLogger(__name__) 13 | 14 | T = TypeVar("T", bound="Test") 15 | 16 | class StackTest(Base): 17 | 18 | async def run(self) -> None: 19 | self.stacker = Stacker( 20 | self.project_name, 21 | self.configs, 22 | uid=self.uid 23 | ) 24 | await self.stacker.create_stacks() 25 | await self.printer.report_test_progress(stacker=self.stacker) 26 | self.passed = True 27 | self.result = self.stacker.stacks 28 | 29 | async def clean_up(self) -> None: 30 | ''' 31 | Deletes the Test related resources. 32 | ''' 33 | if self.stacker is None: 34 | LOG.warning('No stacks were created... skipping cleanup.') 35 | return 36 | 37 | if self.no_delete: 38 | return 39 | 40 | if self.keep_failed: 41 | kwargs = {'status': ['CREATE_COMPLETE', 'UPDATE_COMPLETE']} 42 | await self.stacker.delete_stacks(**kwargs) 43 | else: 44 | await self.stacker.delete_stacks() 45 | 46 | if not self.dont_wait_for_delete: 47 | await self.printer.report_test_progress(stacker=self.stacker) 48 | status = self.stacker.status() 49 | if len(status.get('FAILED', {})) > 0: 50 | raise InvalidActionError( 51 | f"One or more stacks failed to create: {status['FAILED']}" 52 | ) 53 | 54 | async def get_stacks_price(self) -> None: 55 | ''' 56 | Get price of templates. 57 | ''' 58 | self.stacker = Stacker( 59 | self.project_name, 60 | self.configs, 61 | uid=self.uid 62 | ) 63 | await self.stacker.get_stacks_price() 64 | 65 | TerminalPrinter._display_price(stacker=self.stacker) 66 | 67 | async def preview_stacks_result(self) -> None: 68 | ''' 69 | Preview resources of templates. 70 | ''' 71 | self.stacker = Stacker( 72 | self.project_name, 73 | self.configs, 74 | uid=self.uid 75 | ) 76 | await self.stacker.preview_stacks_result() 77 | 78 | TerminalPrinter._display_preview_resources(stacker=self.stacker) 79 | 80 | -------------------------------------------------------------------------------- /iact3/util.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import re 4 | import sys 5 | import uuid 6 | import yaml 7 | 8 | LOG = logging.getLogger(__name__) 9 | 10 | 11 | FIRST_CAP_RE = re.compile("(.)([A-Z][a-z]+)") 12 | ALL_CAP_RE = re.compile("([a-z0-9])([A-Z])") 13 | 14 | 15 | def exit_with_code(code, msg=""): 16 | if msg: 17 | LOG.error(msg) 18 | sys.exit(code) 19 | 20 | 21 | def make_dir(path, ignore_exists=True): 22 | path = os.path.abspath(path) 23 | if ignore_exists and os.path.isdir(path): 24 | return 25 | os.makedirs(path) 26 | 27 | 28 | def pascal_to_snake(pascal): 29 | sub = ALL_CAP_RE.sub(r"\1_\2", pascal) 30 | return ALL_CAP_RE.sub(r"\1_\2", sub).lower() 31 | 32 | 33 | def generate_client_token_ex(prefix: str, suffix: str): 34 | if prefix: 35 | t = [prefix] 36 | else: 37 | t = [] 38 | t.append(str(uuid.uuid1())[:-13]) 39 | t.append(suffix) 40 | r = '_'.join(t) 41 | if len(r) > 64: 42 | r = r[:64] 43 | return r 44 | 45 | 46 | ROS_FUNCTION_NAMES = { 47 | "MergeMap", "Sub", "Base64Decode", "Indent", "Base64", "If", "EachMemberIn", "FormatTime", "Length", 48 | "Not", "Replace", "Min", "Equals", "Test", "Split", "Join", "ListMerge", "Or", "ResourceFacade", 49 | "SelectMapList", "MergeMapToList", "Select", "Calculate", "FindInMap", "MarketplaceImage", "GetAZs", 50 | "Any", "Contains", "Add", "Str", "GetAtt", "Base64Encode", "GetStackOutput", "TransformNamespace", "Jq", 51 | "Max", "MemberListToMap", "Index", "Cidr", "GetJsonValue", "Ref", "And", "Avg", "MatchPattern", "Sub" 52 | } 53 | 54 | 55 | class CustomSafeLoader(yaml.SafeLoader): 56 | pass 57 | 58 | 59 | def make_constructor(fun_name): 60 | if fun_name == 'Ref': 61 | tag_name = fun_name 62 | else: 63 | tag_name = 'Fn::{}'.format(fun_name) 64 | 65 | if fun_name == 'GetAtt': 66 | def get_attribute_constructor(loader, node): 67 | if isinstance(node, yaml.ScalarNode): 68 | value = loader.construct_scalar(node) 69 | try: 70 | split_value = value.split('.') 71 | if len(split_value) == 2: 72 | resource, attribute = split_value 73 | elif len(split_value) >= 3: 74 | if split_value[-2] == 'Outputs': 75 | resource = '.'.join(split_value[:-2]) 76 | attribute = '.'.join(split_value[-2:]) 77 | else: 78 | resource = '.'.join(split_value[:-1]) 79 | attribute = split_value[-1] 80 | else: 81 | raise ValueError 82 | return {tag_name: [resource, attribute]} 83 | except ValueError: 84 | raise ValueError('Resolve !GetAtt error. Value: {}'.format(value)) 85 | elif isinstance(node, yaml.SequenceNode): 86 | values = loader.construct_sequence(node) 87 | return {tag_name: values} 88 | else: 89 | value = loader.construct_object(node) 90 | return {tag_name: value} 91 | return get_attribute_constructor 92 | 93 | def constructor(loader, node): 94 | if isinstance(node, yaml.nodes.ScalarNode): 95 | value = loader.construct_scalar(node) 96 | elif isinstance(node, yaml.nodes.SequenceNode): 97 | value = loader.construct_sequence(node) 98 | elif isinstance(node, yaml.nodes.MappingNode): 99 | value = loader.construct_mapping(node) 100 | else: 101 | value = loader.construct_object(node) 102 | return {tag_name: value} 103 | 104 | return constructor 105 | 106 | 107 | for f in ROS_FUNCTION_NAMES: 108 | CustomSafeLoader.add_constructor(f'!{f}', make_constructor(f)) 109 | -------------------------------------------------------------------------------- /iact3_outputs/iact3-default-cn-hangzhou-b72443d4-cn-hangzhou.txt: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | Region: cn-hangzhou 3 | StackName: iact3-default-cn-hangzhou-b72443d4 4 | StackId: 5 | ***************************************************************************** 6 | TestedResult: Failed 7 | ResultReason: 8 | Iact3Exception, can not find any available value for DBInstanceClass in cn-hangzhou 9 | region in [] for default 10 | ***************************************************************************** 11 | ***************************************************************************** 12 | Events: 13 | 14 | ***************************************************************************** 15 | ***************************************************************************** 16 | Resources: 17 | 18 | ***************************************************************************** 19 | ----------------------------------------------------------------------------- 20 | Tested on: Monday, 27. November 2023 10:24AM 21 | ----------------------------------------------------------------------------- 22 | 23 | -------------------------------------------------------------------------------- /iact3_outputs/iact3-default-cn-hangzhou-eac0248e-cn-hangzhou.txt: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | Region: cn-hangzhou 3 | StackName: iact3-default-cn-hangzhou-eac0248e 4 | StackId: 5 | ***************************************************************************** 6 | TestedResult: Failed 7 | ResultReason: 8 | Iact3Exception, can not find any available value for DBInstanceClass in cn-hangzhou 9 | region in [] for default 10 | ***************************************************************************** 11 | ***************************************************************************** 12 | Events: 13 | 14 | ***************************************************************************** 15 | ***************************************************************************** 16 | Resources: 17 | 18 | ***************************************************************************** 19 | ----------------------------------------------------------------------------- 20 | Tested on: Tuesday, 31. October 2023 10:46AM 21 | ----------------------------------------------------------------------------- 22 | 23 | -------------------------------------------------------------------------------- /iact3_outputs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 196 | 197 | Iact3 Report 198 | 199 | 200 | 201 | 202 | 203 | 205 | 208 | 209 | 210 | 211 |
204 |
206 | Tested on: Monday - Nov,27,2023 @ 10:24:03 207 |
212 | 213 |

214 | 215 | 216 | 217 | 218 | 221 | 224 | 227 | 230 | 233 | 234 | 235 | 236 | 237 | 242 | 245 | 248 | 251 | 256 | 257 | 258 |

259 | 260 |

261 |
219 | Test Name 220 | 222 | Tested Region 223 | 225 | Stack Name 226 | 228 | Tested Results 229 | 231 | Test Logs 232 |
238 |

239 | default 240 |

241 |
243 | cn-hangzhou 244 | 246 | iact3-default-cn-hangzhou-b72443d4 247 | 249 | Iact3Exception 250 | 252 | 253 | View Logs 254 | 255 |
-------------------------------------------------------------------------------- /iact3_outputs/test-failed-cost-result.json: -------------------------------------------------------------------------------- 1 | {"Result": "Failed", "Details": [{"TestName": "default", "TestedRegion": "cn-hangzhou", "StackName": "iact3-default-cn-hangzhou-b72443d4", "TestResult": "Iact3Exception", "TestLog": "iact3-default-cn-hangzhou-b72443d4-cn-hangzhou.txt", "Result": "Failed"}]} -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==7.2.0 2 | pytest-asyncio==0.20.3 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==22.1.0 2 | aiohttp>=3.10.2 3 | aiosignal==1.2.0 4 | alibabacloud-credentials==0.3.0 5 | alibabacloud-darabonba-array==0.1.0 6 | alibabacloud-darabonba-encode-util==0.0.1 7 | alibabacloud-darabonba-map==0.0.1 8 | alibabacloud-darabonba-signature-util==0.0.3 9 | alibabacloud-darabonba-string==0.0.4 10 | alibabacloud-ecs20140526==3.0.1 11 | alibabacloud-endpoint-util==0.0.3 12 | alibabacloud-gateway-spi==0.0.1 13 | alibabacloud-openapi-util==0.2.0 14 | alibabacloud-ros20190910==3.2.16 15 | alibabacloud-tea==0.3.0 16 | alibabacloud-tea-openapi==0.3.6 17 | alibabacloud-tea-util==0.3.8 18 | alibabacloud-tea-xml==0.0.2 19 | alibabacloud-vpc20160428==2.0.12 20 | aliyun-python-sdk-core==2.13.36 21 | aliyun-python-sdk-kms==2.16.0 22 | async-timeout==4.0.2 23 | asynctest==0.13.0 24 | attrs>=22.1.0 25 | backports.shutil-get-terminal-size==1.0.0 26 | certifi>=2023.7.22 27 | charset-normalizer==2.1.1 28 | colorama==0.4.6 29 | crcmod==1.7 30 | cryptography>=42.0.4 31 | dataclasses-jsonschema==2.16.0 32 | debtcollector==2.5.0 33 | decorator==5.1.1 34 | dulwich==0.20.50 35 | exceptiongroup==1.0.4 36 | frozenlist==1.3.1 37 | idna==3.7 38 | iniconfig==1.1.1 39 | iso8601==1.1.0 40 | jmespath==0.10.0 41 | jsonschema==4.17.0 42 | multidict==6.0.2 43 | netaddr==0.8.0 44 | netifaces==0.11.0 45 | oslo.i18n==5.1.0 46 | oslo.utils==6.0.1 47 | oss2==2.16.0 48 | packaging>=21.3 49 | pbr==5.11.0 50 | pluggy==1.0.0 51 | pycparser==2.21 52 | pycryptodome==3.19.1 53 | pyparsing==3.0.9 54 | pyrsistent==0.19.2 55 | python-dateutil==2.8.2 56 | pytz==2022.6 57 | PyYAML==6.0 58 | reprint==0.6.0 59 | requests==2.32.0 60 | retrying==1.3.4 61 | six==1.16.0 62 | tabulate==0.9.0 63 | tomli==2.0.1 64 | urllib3>=1.26.18 65 | wrapt==1.14.1 66 | yarl==1.8.1 67 | yattag==1.14.0 68 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import setuptools 3 | 4 | with open("requirements.txt") as fp: 5 | requirements = fp.read().splitlines() 6 | 7 | 8 | with open("README.md") as fp: 9 | long_description = fp.read() 10 | 11 | 12 | setuptools.setup( 13 | name="alibabacloud-ros-iact3", 14 | version="0.1.11", 15 | 16 | description="Iact3 is a tool that tests Terraform and ROS(Resource Orchestration Service) templates.", 17 | long_description=long_description, 18 | long_description_content_type="text/markdown", 19 | 20 | author="AlibabaCloud", 21 | packages=[ 22 | "iact3", 23 | "iact3.cli_modules", 24 | "iact3.plugin", 25 | "iact3.report", 26 | "iact3.testing" 27 | ], 28 | 29 | install_requires=requirements, 30 | 31 | python_requires=">=3.7", 32 | classifiers=[ 33 | "Development Status :: 2 - Pre-Alpha", 34 | "Intended Audience :: Developers", 35 | "License :: OSI Approved :: Apache Software License", 36 | "Programming Language :: Python :: 3.7", 37 | "Programming Language :: Python :: 3.8", 38 | "Topic :: Software Development :: Libraries", 39 | "Topic :: Software Development :: Testing", 40 | "Operating System :: POSIX :: Linux", 41 | "Operating System :: MacOS :: MacOS X ", 42 | ], 43 | entry_points={ 44 | "console_scripts": [ 45 | "iact3 = iact3.__main__:sync_run", 46 | ] 47 | }, 48 | include_package_data=True 49 | ) 50 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-ros-tool-iact3/64e93057e82e634cd79ee5c4e2d761ef31fe1e03/tests/__init__.py -------------------------------------------------------------------------------- /tests/common.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from iact3.logger import init_cli_logger 5 | 6 | import asynctest 7 | 8 | 9 | class BaseTest(asynctest.TestCase): 10 | REGION_ID = 'cn-shanghai' 11 | 12 | DATA_PATH = Path(__file__).parent / 'data' 13 | 14 | def setUp(self) -> None: 15 | init_cli_logger(loglevel='Debug') 16 | 17 | @staticmethod 18 | def _pprint_json(data, ensure_ascii=False): 19 | print(json.dumps(data, sort_keys=True, indent=4, separators=(',', ': '), 20 | ensure_ascii=ensure_ascii)) 21 | -------------------------------------------------------------------------------- /tests/data/.iact3.yml: -------------------------------------------------------------------------------- 1 | project: 2 | name: test123 3 | regions: 4 | - cn-hangzhou 5 | parameters: 6 | Key1: pro 7 | oss_config: 8 | bucket_name: iactvt-beijing 9 | bucket_region: cn-beijing 10 | object_prefix: test-local 11 | tests: 12 | default: 13 | parameters: 14 | Key1: abc 15 | Key2: aaa 16 | regions: 17 | - cn-beijing 18 | other: 19 | parameters: 20 | Key1: aaa 21 | -------------------------------------------------------------------------------- /tests/data/ecs_instance.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "ROSTemplateFormatVersion": "2015-09-01", 3 | "Parameters": { 4 | "ZoneId": { 5 | "Type": "String" 6 | }, 7 | "InstanceType": { 8 | "Type": "String" 9 | }, 10 | "SystemDiskCategory": { 11 | "Type": "String" 12 | }, 13 | "DataDiskCategory": { 14 | "Type": "String" 15 | }, 16 | "VpcId": { 17 | "Type": "String" 18 | }, 19 | "VswitchId": { 20 | "Type": "String" 21 | }, 22 | "CommonName": { 23 | "Type": "String" 24 | }, 25 | "Password": { 26 | "Type": "String" 27 | }, 28 | "InstanceChargeType": { 29 | "Type": "String" 30 | }, 31 | "NetworkType": { 32 | "Type": "String" 33 | }, 34 | "AllocatePublicIP": { 35 | "Type": "String" 36 | }, 37 | "SecurityGroupId": { 38 | "Type": "String" 39 | }, 40 | "ImageId": { 41 | "Type": "String", 42 | "Default": "centos_7" 43 | } 44 | }, 45 | "Resources": { 46 | "Server": { 47 | "Type": "ALIYUN::ECS::InstanceGroup", 48 | "Properties": { 49 | "ImageId": { 50 | "Ref": "ImageId" 51 | }, 52 | "MaxAmount": 2, 53 | "VpcId": { 54 | "Ref": "VpcId" 55 | }, 56 | "VSwitchId": { 57 | "Ref": "VswitchId" 58 | }, 59 | "InstanceName": { 60 | "Ref": "CommonName" 61 | }, 62 | "InstanceType": { 63 | "Ref": "InstanceType" 64 | }, 65 | "ZoneId": { 66 | "Ref": "ZoneId" 67 | }, 68 | "SystemDiskCategory": { 69 | "Ref": "SystemDiskCategory" 70 | }, 71 | "DiskMappings": [ 72 | { 73 | "Category": {"Ref": "DataDiskCategory"}, 74 | "Size": 500 75 | }, 76 | { 77 | "Category": {"Ref": "DataDiskCategory"}, 78 | "Size": 500 79 | } 80 | ], 81 | "Password": { 82 | "Ref": "Password" 83 | }, 84 | "InstanceChargeType": { 85 | "Ref": "InstanceChargeType" 86 | }, 87 | "NetworkType": { 88 | "Ref": "NetworkType" 89 | }, 90 | "AllocatePublicIP": { 91 | "Ref": "AllocatePublicIP" 92 | }, 93 | "SecurityGroupId": { 94 | "Ref": "SecurityGroupId" 95 | } 96 | } 97 | } 98 | }, 99 | "Metadata": { 100 | "ALIYUN::ROS::Interface": { 101 | "ParameterGroups": [ 102 | { 103 | "Parameters": [ 104 | "NetworkType", 105 | "InstanceChargeType", 106 | "ImageId", 107 | "ZoneId", 108 | "InstanceType", 109 | "SystemDiskCategory", 110 | "DataDiskCategory", 111 | "VpcId", 112 | "VswitchId", 113 | "InstanceName", 114 | "Password", 115 | "AllocatePublicIP", 116 | "SecurityGroupId" 117 | ] 118 | } 119 | ] 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /tests/data/failed_config.yml: -------------------------------------------------------------------------------- 1 | project: 2 | name: test123 3 | regions: 4 | - cn-hangzhou 5 | tests: 6 | default: {} -------------------------------------------------------------------------------- /tests/data/failed_cost_config.iact3.yaml: -------------------------------------------------------------------------------- 1 | project: 2 | # auth: 3 | # name: akProfile 4 | # location: ~/.aliyun/config.json 5 | name: test-failed-cost 6 | regions: 7 | - cn-hangzhou 8 | template_config: 9 | template_url: http://test-yx-hangzhou.oss-cn-hangzhou.aliyuncs.com/ipv4.yaml 10 | oss_config: 11 | bucket_name: test-yx-hangzhou 12 | bucket_region: cn-hangzhou 13 | tests: 14 | default: 15 | parameters: 16 | ZoneId: $[iact3-auto] 17 | InstanceType: 'ecs.g6e.large' 18 | Password: $[iact3-auto] 19 | EIPBandwidth: 1 20 | EcsSystemDiskCategory: 'cloud_esd' 21 | 22 | -------------------------------------------------------------------------------- /tests/data/failed_template.yaml: -------------------------------------------------------------------------------- 1 | ROSTemplateFormatVersion: "2015-09-01" 2 | Resources: 3 | vpc: 4 | Type: ALIYUN::ECS::VPC 5 | Properties: 6 | VpcName: iact3 7 | CidrBlock: 10.10.10.10/32 -------------------------------------------------------------------------------- /tests/data/failed_test_validate_config.yml: -------------------------------------------------------------------------------- 1 | project: 2 | name: test123 3 | regions: 4 | - cn-hangzhou 5 | template_config: 6 | template_url: http://1.yaml 7 | tests: 8 | default: {} -------------------------------------------------------------------------------- /tests/data/failed_validate_template.yml: -------------------------------------------------------------------------------- 1 | ROSTemplateFormatVersion: '2015-09-01' 2 | Description: 3 | en: Create an IPv4 VPC, and bind ECS with EIP. 4 | zh-cn: 搭建一个具有IPv4地址块的专有网络,并为专有网络中的云服务器ECS实例绑定一个弹性公网IP。 5 | Parameters: 6 | InstanceType: 7 | Type: String 8 | Description: 9 | zh-cn: 选择VPC可用区下可使用的规格。 10 | en: Fill in the specifications that can be used under the VSwitch availability zone. 11 | Label: 12 | zh-cn: ECS实例规格 13 | en: ECS Instance Type 14 | AssociationProperty: ALIYUN::ECS::Instance::InstanceType 15 | AssociationPropertyMetadata: 16 | ZoneId: ${ZoneId} 17 | Password: 18 | Type: String 19 | Label: 20 | zh-cn: ECS 实例密码 21 | en: ECS Instance Password 22 | Description: 23 | en: Server login password, Length 8-30, must contain three(Capital letters, 24 | lowercase letters, numbers, ()`~!@#$%^&*_-+=|{}[]:;'<>,.?/ Special symbol 25 | in). 26 | zh-cn: 服务器登录密码,长度8-30,必须包含三项(大写字母、小写字母、数字、 ()`~!@#$%^&*_-+=|{}[]:;'<>,.?/ 中的特殊符号)。 27 | ConstraintDescription: '[8, 30] characters, consists of uppercase letter, lowercase 28 | letter and special characters.' 29 | AllowedPattern: '[0-9A-Za-z\_\-\&:;''<>,=%`~!@#\(\)\$\^\*\+\|\{\}\[\]\.\?\/]+$' 30 | MinLength: 8 31 | MaxLength: 30 32 | NoEcho: true 33 | Confirm: true 34 | EcsSystemDiskCategory: 35 | Type: String 36 | Label: 37 | en: System Disk Type 38 | zh-cn: 系统盘类型 39 | Description: 40 | en: 'Optional values:
[cloud_efficiency: 41 | Efficient Cloud Disk]
[cloud_ssd: SSD 42 | Cloud Disk]
[cloud_essd: ESSD Cloud Disk]
[cloud: 43 | Cloud Disk]
[ephemeral_ssd: Local 44 | SSD Cloud Disk]' 45 | zh-cn: '可选值:
[cloud_efficiency: 高效云盘]
[cloud_ssd: SSD云盘]
[cloud_essd: 47 | ESSD云盘]
[cloud: 普通云盘]
[ephemeral_ssd: 48 | 本地SSD盘]' 49 | Default: cloud_ssd 50 | AllowedValues: 51 | - cloud_efficiency 52 | - cloud_ssd 53 | - cloud 54 | - cloud_essd 55 | - ephemeral_ssd 56 | EIPBandwidth: 57 | Type: Number 58 | Label: 59 | en: EIP Bandwidth 60 | zh-cn: 弹性公网带宽 61 | Description: 62 | en: 'EIP Bandwidth, Value range: [1,200], unit: Mbps.' 63 | zh-cn: 弹性公网地址带宽。取值范围:1~200, 单位:Mbps。 64 | Default: 1 65 | MinValue: 1 66 | MaxValue: 200 67 | Resources: 68 | VPC: 69 | Type: ALIYUN::ECS::VPC 70 | Properties: 71 | VpcName: myvpc 72 | CidrBlock: 192.168.0.0/16 73 | VSwitch: 74 | Type: ALIYUN::ECS::VSwitch 75 | Properties: 76 | VpcId: 77 | Ref: VPC 78 | ZoneId: 79 | Ref: ZoneId 80 | CidrBlock: 192.168.0.0/24 81 | SecurityGroup: 82 | Type: ALIYUN::ECS::SecurityGroup 83 | Properties: 84 | VpcId: 85 | Ref: VPC 86 | SecurityGroupName: mysg 87 | SecurityGroupType: normal 88 | SecurityGroupIngress: 89 | - SourceCidrIp: 0.0.0.0/0 90 | PortRange: 22/22 91 | IpProtocol: tcp 92 | - SourceCidrIp: 0.0.0.0/0 93 | PortRange: 3389/3389 94 | IpProtocol: tcp 95 | - SourceCidrIp: 0.0.0.0/0 96 | PortRange: 22/22 97 | IpProtocol: udp 98 | - SourceCidrIp: 0.0.0.0/0 99 | PortRange: 3389/3389 100 | IpProtocol: udp 101 | - SourceCidrIp: 0.0.0.0/0 102 | PortRange: -1/-1 103 | IpProtocol: icmp 104 | EcsInstance: 105 | Type: ALIYUN::ECS::Instance 106 | Properties: 107 | VpcId: 108 | Ref: VPC 109 | VSwitchId: 110 | Ref: VSwitch 111 | ImageId: centos_7 112 | AllocatePublicIP: false 113 | InstanceChargeType: PostPaid 114 | SpotStrategy: NoSpot 115 | InstanceType: 116 | Ref: InstanceType 117 | Password: 118 | Ref: Password 119 | SecurityGroupId: 120 | Ref: SecurityGroup 121 | SystemDiskCategory: 122 | Ref: EcsSystemDiskCategory 123 | SystemDiskSize: 40 124 | EIP: 125 | Type: ALIYUN::VPC::EIP 126 | Properties: 127 | Bandwidth: 128 | Ref: EIPBandwidth 129 | InternetChargeType: PayByTraffic 130 | EipBind: 131 | Type: ALIYUN::VPC::EIPAssociation 132 | Properties: 133 | InstanceId: 134 | Ref: EcsInstance 135 | AllocationId: 136 | Ref: EIP 137 | Outputs: 138 | EcsInstanceId: 139 | Description: The instance id of created ecs instance 140 | Value: 141 | Ref: EcsInstance 142 | EipAddress: 143 | Description: IP address of created EIP. 144 | Value: 145 | Fn::GetAtt: 146 | - EIP 147 | - EipAddress 148 | Metadata: 149 | ALIYUN::ROS::Interface: 150 | ParameterGroups: 151 | - Parameters: 152 | - ZoneId 153 | Label: 154 | default: 'VPC' 155 | - Parameters: 156 | - InstanceType 157 | - EcsSystemDiskCategory 158 | - Password 159 | - EIPBandwidth 160 | Label: 161 | default: 'ECS' 162 | TemplateTags: 163 | - acs:document-help:vpc:在IPv4专有网络中为ECS实例绑定一个弹性公网IP 164 | -------------------------------------------------------------------------------- /tests/data/full_config.yml: -------------------------------------------------------------------------------- 1 | general: 2 | auth: 3 | name: default 4 | location: ~/.aliyun/config.json 5 | oss_config: 6 | bucket_name: iactvt-beijing 7 | bucket_region: cn-beijing 8 | object_prefix: specified_prefix 9 | parameters: 10 | VpcId: '$[iact3-auto]' 11 | VswitchId: '$[iact3-auto]' 12 | SecurityGroupId: '$[iact3-auto]' 13 | tags: 14 | environment: general-test 15 | regions: 16 | - cn-hangzhou 17 | project: 18 | name: iact3-full-test 19 | regions: 20 | - cn-hangzhou 21 | - cn-beijing 22 | - cn-shanghai 23 | parameters: 24 | InstanceChargeType: PostPaid 25 | NetworkType: vpc 26 | AllocatePublicIP: false 27 | InstanceName: '$[iact3-auto]' 28 | Password: '$[iact3-auto]' 29 | template_config: 30 | template_location: ecs_instance.template.json 31 | tags: 32 | iact3-project: iact3-full-test 33 | tests: 34 | failed-test: 35 | parameters: 36 | ZoneId: 'cn-hangzhou-g' 37 | InstanceType: 'ecs.g6.large' 38 | SystemDiskCategory: 'cloud_ssd' 39 | DataDiskCategory: 'cloud_ssd' 40 | regions: 41 | - cn-hangzhou 42 | -------------------------------------------------------------------------------- /tests/data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 196 | 197 | Iact3 Report 198 | 199 | 200 | 201 | 202 | 203 | 205 | 208 | 209 | 210 | 211 |
204 |
206 | Tested on: Tuesday - Feb,07,2023 @ 14:01:24 207 |
212 | 213 |

214 | 215 | 216 | 217 | 218 | 221 | 224 | 227 | 230 | 233 | 234 |

-------------------------------------------------------------------------------- /tests/data/invalid_template.yml: -------------------------------------------------------------------------------- 1 | ROSTemplateFormatVersion: '2015-09-01' 2 | Description: Test ECS NatGateway 3 | Parameters: 4 | VPC: 5 | AssociationProperty: ALIYUN::ECS::VPC::VPCId 6 | Type: String 7 | Label: 8 | zh-cn: 现有VPC的实例ID 9 | en: Existing VPC dInstance ID 10 | VSwitch: 11 | AssociationProperty: ALIYUN::ECS::VSwitch::VSwitchId 12 | Type: String 13 | Label: 14 | zh-cn: 网络交换机ID 15 | en: VSwitch ID 16 | AssociationPropertyMetadata: 17 | VpcId: VPC 18 | Outputs: 19 | NatGatewayId: 20 | Value: 21 | Fn::GetAtt: 22 | - NatGateway 23 | - NatGatewayId 24 | -------------------------------------------------------------------------------- /tests/data/real.iact3.yml: -------------------------------------------------------------------------------- 1 | project: 2 | name: real-test 3 | parameters: 4 | VpcId: $[iact3-auto] 5 | ZoneId: $[iact3-auto] 6 | VSwitchId: $[iact3-auto] 7 | InstancePassword: $[iact3-auto] 8 | InstanceType: $[iact3-auto] 9 | regions: 10 | - cn-beijing 11 | template_config: 12 | template_url: https://iactvt-beijing.oss-cn-beijing.aliyuncs.com/long_text_2022-12-26-17-19-50.txt 13 | tests: 14 | default: 15 | parameters: 16 | SlaveAmount: 4 17 | general: 18 | oss_config: 19 | object_prefix: service 20 | bucket_region: cn-beijing 21 | bucket_name: iactvt-beijing 22 | -------------------------------------------------------------------------------- /tests/data/simple_template.yml: -------------------------------------------------------------------------------- 1 | ROSTemplateFormatVersion: '2015-09-01' 2 | Parameters: 3 | Key1: 4 | Type: String 5 | Default: null 6 | Key2: 7 | Type: String 8 | Default: null 9 | Resources: 10 | sleep: 11 | Type: ALIYUN::ROS::Sleep 12 | Properties: 13 | CreateDuration: 1 14 | DeleteDuration: 1 15 | Outputs: 16 | Key1: 17 | Value: 18 | Ref: Key1 19 | Key2: 20 | Value: 21 | Ref: Key2 -------------------------------------------------------------------------------- /tests/data/test_config.iact3.yaml: -------------------------------------------------------------------------------- 1 | project: 2 | # auth: 3 | # name: akProfile 4 | # location: ~/.aliyun/config.json 5 | name: test-failed-cost 6 | regions: 7 | - cn-hangzhou 8 | template_config: 9 | template_url: http://test-yx-hangzhou.oss-cn-hangzhou.aliyuncs.com/ipv4.yaml 10 | oss_config: 11 | bucket_name: test-yx-hangzhou 12 | bucket_region: cn-hangzhou 13 | tests: 14 | default: 15 | parameters: 16 | ZoneId: $[iact3-auto] 17 | InstanceType: 'ecs.g6.large' 18 | Password: $[iact3-auto] 19 | EIPBandwidth: 1 20 | EcsSystemDiskCategory: 'cloud_essd' 21 | 22 | -------------------------------------------------------------------------------- /tests/data/test_global_config.yml: -------------------------------------------------------------------------------- 1 | general: 2 | auth: 3 | name: default 4 | location: '~/.aliyun/config.json' 5 | oss_config: 6 | bucket_name: example-name 7 | bucket_region: cn-hangzhou 8 | parameters: 9 | Key1: Value1 10 | Key2: Value2 11 | regions: 12 | - cn-hangzhou 13 | - cn-shanghai 14 | tags: 15 | Tag1: value1 16 | Tag2: value2 17 | project: 18 | name: name 19 | parameters: 20 | Key1: Value-new 21 | Key3: value3 22 | regions: 23 | - cn-hangzhou 24 | - cn-beijing 25 | role_name: my-test-role 26 | tags: 27 | Tag1: value-new 28 | Tag3: value3 29 | template_config: 30 | template_location: ros-template/ 31 | tests: 32 | test1: 33 | name: test 34 | parameters: 35 | Key1: Value1-base-test 36 | Key2: value2-base-test 37 | regions: 38 | - cn-hangzhou 39 | - cn-shanghai 40 | tags: 41 | Key: Value-base-test 42 | test2: 43 | parameters: 44 | Key4: value4 45 | -------------------------------------------------------------------------------- /tests/data/test_project_config.yml: -------------------------------------------------------------------------------- 1 | project: 2 | name: unit 3 | parameters: 4 | Key: Value-data1-project 5 | Key1: Value1-data1-project 6 | regions: 7 | - cn-qingdao 8 | - cn-beijing 9 | tags: 10 | Tag1: value1-data1-project 11 | Tag2: value2-data1-project 12 | tests: 13 | test1: 14 | name: unit1 15 | parameters: 16 | Key1: Value1-data1-test 17 | Key2: value2-data1-test 18 | regions: 19 | - cn-shanghai 20 | tags: 21 | Key: Value-data1-test 22 | test3: 23 | name: unit3 24 | regions: 25 | - cn-shanghai 26 | - cn-hangzhou 27 | - cn-beijing 28 | -------------------------------------------------------------------------------- /tests/data/tf/main.tf: -------------------------------------------------------------------------------- 1 | module "my_vpc" { 2 | source = "./modules/vpc" 3 | } 4 | resource "alicloud_vswitch" "vsw" { 5 | vpc_id = "${module.my_vpc.vpc_id}" 6 | cidr_block = "172.16.0.0/21" 7 | availability_zone = "cn-shanghai-b" 8 | } 9 | output "vsw_id" { 10 | value = "${alicloud_vswitch.vsw.id}" 11 | } -------------------------------------------------------------------------------- /tests/data/tf/modules/vpc/main.tf: -------------------------------------------------------------------------------- 1 | resource "alicloud_vpc" "vpc" { 2 | name = "tf_test" 3 | cidr_block = "172.16.0.0/12" 4 | } 5 | output "vpc_id" { 6 | value = "${alicloud_vpc.vpc.id}" 7 | } -------------------------------------------------------------------------------- /tests/data/tf/test.template.xxx: -------------------------------------------------------------------------------- 1 | ROSTemplateFormatVersion: '2015-09-01' 2 | -------------------------------------------------------------------------------- /tests/data/timeout_config.yml: -------------------------------------------------------------------------------- 1 | project: 2 | name: test-failed-cost 3 | regions: 4 | - cn-hangzhou 5 | tests: 6 | default: 7 | parameters: 8 | ZoneId: not-exist 9 | DBInstanceClass: $[iact3-auto] 10 | DBPassword: $[iact3-auto] 11 | -------------------------------------------------------------------------------- /tests/data/timeout_template.yml: -------------------------------------------------------------------------------- 1 | ROSTemplateFormatVersion: '2015-09-01' 2 | Parameters: 3 | ZoneId: 4 | Type: String 5 | DBInstanceClass: 6 | Default: mysql.n2m.small.2c 7 | Type: String 8 | DBPassword: 9 | Type: String 10 | Resources: 11 | RdsDBInstance: 12 | Type: 'ALIYUN::RDS::DBInstance' 13 | Properties: 14 | Engine: MySQL 15 | Category: HighAvailability 16 | DBInstanceStorage: 50 17 | MasterUsername: username 18 | DBInstanceStorageType: cloud_essd 19 | MasterUserPassword: 20 | Ref: DBPassword 21 | ZoneId: 22 | Ref: ZoneId 23 | VpcId: 24 | Ref: Vpc 25 | VSwitchId: 26 | Ref: VSwitch 27 | MasterUserType: Super 28 | EngineVersion: '8.0' 29 | DBInstanceClass: 30 | Ref: DBInstanceClass 31 | SecurityIPList: 0.0.0.0/0 32 | Vpc: 33 | Type: 'ALIYUN::ECS::VPC' 34 | Properties: 35 | CidrBlock: 192.168.0.0/16 36 | VSwitch: 37 | Type: 'ALIYUN::ECS::VSwitch' 38 | Properties: 39 | VpcId: 40 | Ref: Vpc 41 | CidrBlock: 192.168.0.0/24 42 | ZoneId: 43 | Ref: ZoneId 44 | Metadata: 45 | 'ALIYUN::ROS::Interface': 46 | ParameterGroups: 47 | - Parameters: 48 | - ZoneId 49 | - DBInstanceClass 50 | - DBPassword -------------------------------------------------------------------------------- /tests/plugin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-ros-tool-iact3/64e93057e82e634cd79ee5c4e2d761ef31fe1e03/tests/plugin/__init__.py -------------------------------------------------------------------------------- /tests/plugin/test_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | from Tea.core import TeaCore 5 | from alibabacloud_credentials.client import Client 6 | from alibabacloud_tea_openapi.models import Config 7 | from alibabacloud_ros20190910.client import Client as ROSClient 8 | from alibabacloud_ros20190910 import models as ros_models 9 | from alibabacloud_tea_util.models import RuntimeOptions 10 | 11 | from iact3.plugin.base_plugin import CredentialClient 12 | from tests.common import BaseTest 13 | 14 | 15 | class TestBasePlugin(BaseTest): 16 | 17 | def test_credentials_env(self): 18 | ''' 19 | set environment 20 | ''' 21 | # os.environ['ALIBABA_CLOUD_ACCESS_KEY_ID'] = 'test_ak' 22 | # os.environ['ALIBABA_CLOUD_ACCESS_KEY_SECRET'] = 'test_sk' 23 | cred = CredentialClient() 24 | self.assertEqual(cred.cloud_credential.credential_type, 'access_key') 25 | 26 | def get_ros_client(self): 27 | cred = Client() 28 | config = Config(credential=cred) 29 | client = ROSClient(config) 30 | return client 31 | 32 | async def test_list_stacks(self): 33 | client = self.get_ros_client() 34 | request = ros_models.ListStacksRequest(region_id='cn-hangzhou') 35 | runtime_option = RuntimeOptions() 36 | response = await client.list_stacks_with_options_async(request, runtime_option) 37 | response = TeaCore.to_map(response) 38 | print(response) 39 | 40 | -------------------------------------------------------------------------------- /tests/plugin/test_ecs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from iact3.plugin.ecs import EcsPlugin 3 | from tests.common import BaseTest 4 | 5 | 6 | class TestEcsPlugin(BaseTest): 7 | 8 | def setUp(self) -> None: 9 | super(TestEcsPlugin, self).setUp() 10 | self.plugin = EcsPlugin(region_id=self.REGION_ID) 11 | 12 | async def test_get_sg(self): 13 | result = await self.plugin.get_security_group(vpc_id='vpc-2zeh14fqe8g53hoyvxdpv') 14 | self._pprint_json(result) 15 | -------------------------------------------------------------------------------- /tests/plugin/test_oss.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from iact3.config import DEFAULT_OUTPUT_DIRECTORY 4 | from iact3.generate_params import IAC_NAME 5 | from iact3.plugin.oss import OssPlugin 6 | from tests.common import BaseTest 7 | 8 | 9 | class TestOssPlugin(BaseTest): 10 | 11 | def setUp(self) -> None: 12 | super(TestOssPlugin, self).setUp() 13 | self.plugin = OssPlugin(region_id=self.REGION_ID, bucket_name='iact3-beijing') 14 | 15 | def test_put_file(self): 16 | report_path = Path(f'../{DEFAULT_OUTPUT_DIRECTORY}') 17 | for path in report_path.glob(f'{IAC_NAME}*.txt'): 18 | self.plugin.put_local_file(f'local_file_test/{IAC_NAME}/{path.name}', path.resolve()) 19 | 20 | def test_exist(self): 21 | self.plugin.object_exists('') 22 | 23 | -------------------------------------------------------------------------------- /tests/plugin/test_ros.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | 4 | from iact3.plugin.ros import StackPlugin 5 | from tests.common import BaseTest 6 | 7 | 8 | class TestRosPlugin(BaseTest): 9 | 10 | def setUp(self) -> None: 11 | super(TestRosPlugin, self).setUp() 12 | self.plugin = StackPlugin(region_id=self.REGION_ID) 13 | 14 | async def test_create_stack(self): 15 | tpl = { 16 | 'ROSTemplateFormatVersion': '2015-09-01', 17 | 'Parameters': { 18 | 'A': {'Type': 'String'}, 19 | 'B': {'Type': 'Number'} 20 | } 21 | } 22 | params = { 23 | 'A': 'a', 24 | 'B': 1 25 | } 26 | stack_id = await self.plugin.create_stack( 27 | stack_name='ros-test', template_body=json.dumps(tpl), parameters=params) 28 | print(stack_id) 29 | 30 | async def test_get_stack(self): 31 | ret = await self.plugin.get_stack(stack_id='0a37cb51-800c-4b64-98a7-4e352a5b30d2') 32 | self._pprint_json(ret) 33 | 34 | async def test_delete_stack(self): 35 | await self.plugin.delete_stack(stack_id='e6edc4ff-c228-4f37-a622-004ef45fba34') 36 | 37 | async def test_list_stacks(self): 38 | ret = await self.plugin.list_stacks() 39 | self._pprint_json(ret) 40 | 41 | async def test_fetch_all(self): 42 | regions = ['cn-beijing', 'cn-hangzhou'] 43 | tags = {} 44 | tasks = [] 45 | for region in regions: 46 | plugin = StackPlugin(region_id=region) 47 | tasks.append( 48 | asyncio.create_task(plugin.fetch_all_stacks(tags)) 49 | ) 50 | result = await asyncio.gather(*tasks) 51 | print(result) 52 | 53 | async def test_list_and_delete_stacks(self): 54 | regions = ['cn-beijing', 'cn-hangzhou'] 55 | tags = {'CreateBy': 'iact3'} 56 | tasks = [] 57 | for region in regions: 58 | plugin = StackPlugin(region_id=region) 59 | stacks = await plugin.fetch_all_stacks(tags) 60 | tasks = [asyncio.create_task(plugin.delete_stack(stack.get('StackId'))) for stack in stacks] 61 | result = await asyncio.gather(*tasks) 62 | print(result) 63 | 64 | async def test_get_regions(self): 65 | result = await self.plugin.get_regions() 66 | print(result) 67 | 68 | async def test_get_parameters_constraints(self): 69 | tpl = { 70 | 'Parameters': { 71 | 'ZoneId': {'Type': 'String'}, 72 | 'InstanceType': {'Type': 'String'} 73 | }, 74 | 'ROSTemplateFormatVersion': '2015-09-01', 75 | 'Resources': { 76 | 'ECS': { 77 | 'Properties': { 78 | 'ZoneId': {'Ref': 'ZoneId'}, 79 | 'InstanceType': {'Ref': 'InstanceType'}, 80 | 'ImageId': 'CentOs_7' 81 | }, 82 | 'Type': 'ALIYUN::ECS::Instance' 83 | } 84 | } 85 | } 86 | tpl_path = self.DATA_PATH / 'ecs_instance.template.json' 87 | with open(tpl_path, 'r', encoding='utf-8') as file_handle: 88 | tpl = json.load(file_handle) 89 | params1 = { 90 | 'NetworkType': 'vpc', 91 | 'InstanceChargeType': 'PostPaid', 92 | 'ZoneId': 'cn-shanghai-h' 93 | } 94 | result1 = await self.plugin.get_parameter_constraints( 95 | template_body=json.dumps(tpl), parameters_key_filter=['InstanceType'], parameters=params1 96 | ) 97 | self._pprint_json(result1) 98 | params2 = { 99 | 'NetworkType': 'vpc', 100 | 'InstanceChargeType': 'PostPaid', 101 | 'ZoneId': 'cn-shanghai-l' 102 | } 103 | result2 = await self.plugin.get_parameter_constraints( 104 | template_body=json.dumps(tpl), parameters_key_filter=['InstanceType'], parameters=params2 105 | ) 106 | self._pprint_json(result2) 107 | 108 | async def test_get_template(self): 109 | result = await self.plugin.get_template(template_id='fe08e732-0b38-454c-9314-8aa9735cf6bc') 110 | self._pprint_json(result) 111 | 112 | async def test_get_template_estimate_cost(self): 113 | tpl = { 114 | 'Parameters': { 115 | 'ZoneId': {'Type': 'String'}, 116 | 'InstanceType': {'Type': 'String'} 117 | }, 118 | 'ROSTemplateFormatVersion': '2015-09-01', 119 | 'Resources': { 120 | 'ECS': { 121 | 'Properties': { 122 | 'ZoneId': {'Ref': 'ZoneId'}, 123 | 'InstanceType': {'Ref': 'InstanceType'}, 124 | 'ImageId': 'CentOs_7' 125 | }, 126 | 'Type': 'ALIYUN::ECS::Instance' 127 | } 128 | } 129 | } 130 | params = { 131 | 'InstanceType': 'ecs.g6e.large', 132 | 'ZoneId': 'cn-hangzhou-h' 133 | } 134 | region_id = 'cn-hangzhou' 135 | result = await self.plugin.get_template_estimate_cost( 136 | template_body=json.dumps(tpl), parameters=params, region_id=region_id) 137 | self._pprint_json(result) 138 | 139 | async def test_validate_template(self): 140 | tpl = { 141 | 'ROSTemplateFormatVersion': '2015-09-01', 142 | 'Parameters': { 143 | 'A': {'Type': 'String'}, 144 | 'B': {'Type': 'Number'} 145 | } 146 | } 147 | 148 | result = await self.plugin.validate_template(template_body=json.dumps(tpl)) 149 | self._pprint_json(result) 150 | 151 | async def test_preview_stack(self): 152 | tpl = { 153 | "ROSTemplateFormatVersion": "2015-09-01", 154 | "Resources": { 155 | "ElasticIp": { 156 | "Type": "ALIYUN::VPC::EIP", 157 | "Properties": { 158 | "InstanceChargeType": "Postpaid", 159 | "Name": "TestEIP", 160 | "InternetChargeType": "PayByBandwidth", 161 | "Netmode": "public", 162 | "Bandwidth": 5 163 | } 164 | } 165 | } 166 | } 167 | params = {} 168 | region_id = 'cn-shanghai' 169 | result = await self.plugin.preview_stack( 170 | stack_name='ros-test-preview', template_body=json.dumps(tpl), parameters=params, region_id=region_id) 171 | self._pprint_json(result) 172 | 173 | async def test_generate_template_policy(self): 174 | tpl = { 175 | 'Parameters': { 176 | 'ZoneId': {'Type': 'String'}, 177 | 'InstanceType': {'Type': 'String'} 178 | }, 179 | 'ROSTemplateFormatVersion': '2015-09-01', 180 | 'Resources': { 181 | 'ECS': { 182 | 'Properties': { 183 | 'ZoneId': {'Ref': 'ZoneId'}, 184 | 'InstanceType': {'Ref': 'InstanceType'}, 185 | 'ImageId': 'centos_7' 186 | }, 187 | 'Type': 'ALIYUN::ECS::Instance' 188 | } 189 | } 190 | } 191 | 192 | result = await self.plugin.generate_template_policy(template_body=json.dumps(tpl)) 193 | self._pprint_json(result) -------------------------------------------------------------------------------- /tests/plugin/test_vpc.py: -------------------------------------------------------------------------------- 1 | from iact3.plugin.vpc import VpcPlugin 2 | from tests.common import BaseTest 3 | 4 | 5 | class TestVpcPlugin(BaseTest): 6 | 7 | def setUp(self) -> None: 8 | super(TestVpcPlugin, self).setUp() 9 | self.plugin = VpcPlugin(region_id=self.REGION_ID) 10 | 11 | async def test_get_vpc(self): 12 | vpcs = await self.plugin.get_one_vpc() 13 | self._pprint_json(vpcs) 14 | 15 | async def test_get_vsw(self): 16 | vsw = await self.plugin.get_one_vswitch(vpc_id='vpc-mock') 17 | self._pprint_json(vsw) 18 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | 4 | from iact3.main import run 5 | from tests.common import BaseTest 6 | 7 | from io import StringIO 8 | import logging 9 | import logging.handlers 10 | 11 | 12 | class TestConfig(BaseTest): 13 | async def test_main(self): 14 | sys.argv = [ 15 | '', 'test', 'run', '--project-path', str(self.DATA_PATH), 16 | '-t', 'simple_template.yml', '-c', '.iact3.yml', '--no-delete' 17 | ] 18 | await run() 19 | 20 | async def test_list(self): 21 | sys.argv = [ 22 | '', 'test', 'list', 23 | ] 24 | await run() 25 | 26 | async def test_delete(self): 27 | sys.argv = [ 28 | '', 'test', 'clean', 29 | ] 30 | await run() 31 | 32 | async def test_delete_with_stack_id(self): 33 | sys.argv = [ 34 | '', 'test', 'clean', '--stack-id', 'd5ea4c0f-3ec7-416e-afe7-06d41bdf56ba' 35 | ] 36 | await run() 37 | 38 | async def test_params(self): 39 | sys.argv = [ 40 | '', 'test', 'run', '--project-path', str(self.DATA_PATH), 41 | '-t', 'ecs_instance.template.json', '-c', 'full_config.yml', '-g' 42 | ] 43 | await run() 44 | 45 | async def test_debug(self): 46 | sys.argv = [ 47 | '', 'test', 'run', '--project-path', str(self.DATA_PATH), '-c', 'real.iact3.yml' 48 | ] 49 | await run() 50 | 51 | async def test_cost(self): 52 | # 创建一个 MemoryHandler 53 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 54 | 55 | # 获取日志记录器并添加 MemoryHandler 56 | logger = logging.getLogger() 57 | logger.addHandler(memory_handler) 58 | 59 | sys.argv = [ 60 | '', 'cost', '-c', str(self.DATA_PATH / 'real.iact3.yml') 61 | ] 62 | await run() 63 | 64 | # 获取 MemoryHandler 中的日志记录 65 | logs = memory_handler.buffer 66 | 67 | # 检查日志记录是否包含特定的消息 68 | self.assertIn("SlaveConsulServer", logs[4].getMessage()) 69 | self.assertIn("SlaveConsulServer", logs[4].getMessage()) 70 | 71 | async def test_cost_fail(self): 72 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 73 | 74 | logger = logging.getLogger() 75 | logger.addHandler(memory_handler) 76 | 77 | sys.argv = [ 78 | '', 'cost', '-c', str(self.DATA_PATH / 'failed_cost_config.iact3.yaml') 79 | ] 80 | await run() 81 | 82 | logs = memory_handler.buffer 83 | 84 | self.assertIn("StackValidationFailed", logs[2].getMessage()) 85 | self.assertIn("code:", logs[3].getMessage()) 86 | 87 | async def test_validate(self): 88 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 89 | logger = logging.getLogger() 90 | logger.addHandler(memory_handler) 91 | 92 | sys.argv = [ 93 | '', 'validate', '-c', str(self.DATA_PATH / 'real.iact3.yml') 94 | ] 95 | await run() 96 | 97 | logs = memory_handler.buffer 98 | self.assertIn("LegalTemplate Check passed", logs[3].getMessage()) 99 | 100 | async def test_validate_fail(self): 101 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 102 | logger = logging.getLogger() 103 | logger.addHandler(memory_handler) 104 | 105 | sys.argv = [ 106 | '', 'validate', '-t', str(self.DATA_PATH / 'failed_validate_template.yml') 107 | ] 108 | await run() 109 | 110 | logs = memory_handler.buffer 111 | self.assertIn("InvalidTemplate", logs[3].getMessage()) 112 | 113 | async def test_preview(self): 114 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 115 | logger = logging.getLogger() 116 | logger.addHandler(memory_handler) 117 | 118 | sys.argv = [ 119 | '', 'preview', '-c', str(self.DATA_PATH / 'real.iact3.yml') 120 | ] 121 | await run() 122 | 123 | logs = memory_handler.buffer 124 | self.assertIn("SlaveConsulServer ECS::InstanceGroup {", logs[5].getMessage()) 125 | self.assertIn("\"AdjustmentType\": \"NoEffect\",", logs[6].getMessage()) 126 | self.assertIn("RosWaitCondition ROS::WaitCondition ", logs[89].getMessage()) 127 | 128 | async def test_preview_fail(self): 129 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 130 | logger = logging.getLogger() 131 | logger.addHandler(memory_handler) 132 | 133 | sys.argv = [ 134 | '', 'preview', '-c', str(self.DATA_PATH / 'failed_cost_config.iact3.yaml') 135 | ] 136 | await run() 137 | 138 | logs = memory_handler.buffer 139 | self.assertIn("StackValidationFailed", logs[3].getMessage()) 140 | self.assertIn("code:", logs[4].getMessage()) 141 | 142 | async def test_policy(self): 143 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 144 | logger = logging.getLogger() 145 | logger.addHandler(memory_handler) 146 | 147 | sys.argv = [ 148 | '', 'policy', '-c', str(self.DATA_PATH / 'real.iact3.yml') 149 | ] 150 | await run() 151 | 152 | logs = memory_handler.buffer 153 | self.assertIn("Statement", logs[1].getMessage()) 154 | self.assertIn("Action", logs[1].getMessage()) 155 | 156 | async def test_policy_fail(self): 157 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 158 | logger = logging.getLogger() 159 | logger.addHandler(memory_handler) 160 | 161 | sys.argv = [ 162 | '', 'policy', '-t', str(self.DATA_PATH / 'invalid_template.yml') 163 | ] 164 | await run() 165 | 166 | logs = memory_handler.buffer 167 | self.assertIn("\"Statement\": []", logs[1].getMessage()) 168 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from iact3.config import * 4 | from tests.common import BaseTest 5 | 6 | 7 | class TestConfig(BaseTest): 8 | config_data = { 9 | GENERAL: { 10 | AUTH: { 11 | 'name': 'my-default-profile', 12 | 'location': '~/.aliyun/config.json' 13 | }, 14 | REGIONS: ['cn-hangzhou', 'cn-shanghai'], 15 | PARAMETERS: { 16 | 'Key1': 'Value1', 17 | 'Key2': 'Value2', 18 | 'Tags': [{'Key': 'ros', 'Value': 'xxxx'}] 19 | }, 20 | TAGS: { 21 | 'Tag1': 'value1', 22 | 'Tag2': 'value2' 23 | }, 24 | OSS_CONFIG: { 25 | 'bucket_name': 'example-name', 26 | 'bucket_region': 'cn-hangzhou' 27 | } 28 | }, 29 | PROJECT: { 30 | NAME: 'name', 31 | REGIONS: ['cn-hangzhou', 'cn-beijing'], 32 | PARAMETERS: { 33 | 'Key1': 'Value-new', 34 | 'Key3': 'value3' 35 | }, 36 | TAGS: { 37 | 'Tag1': 'value-new', 38 | 'Tag3': 'value3' 39 | }, 40 | TEMPLATE_CONFIG: { 41 | 'template_location': 'ros-template/' 42 | }, 43 | ROLE_NAME: 'my-test-role' 44 | }, 45 | TESTS: { 46 | 'test1': { 47 | NAME: 'test', 48 | REGIONS: ['cn-hangzhou', 'cn-shanghai'], 49 | PARAMETERS: { 50 | 'Key1': 'Value1-base-test', 51 | 'Key2': 'value2-base-test' 52 | }, 53 | TAGS: { 54 | 'Key': 'Value-base-test' 55 | } 56 | }, 57 | 'test2': { 58 | PARAMETERS: { 59 | 'Key4': 'value4' 60 | } 61 | } 62 | } 63 | } 64 | 65 | def test_dataclass_init(self): 66 | project = BaseConfig.from_dict(self.config_data) 67 | print(project) 68 | 69 | def test_create_from_default_file(self): 70 | with self.assertRaises(FileNotFoundError) as cm: 71 | BaseConfig.create() 72 | ex = cm.exception 73 | self.assertEqual(ex.errno, 2) 74 | self.assertEqual(True, ex.filename.endswith(f'.{IAC_NAME}.yml')) 75 | 76 | def test_merge(self): 77 | data1 = { 78 | PROJECT: { 79 | NAME: 'unit', 80 | REGIONS: ['cn-qingdao', 'cn-beijing'], 81 | PARAMETERS: { 82 | 'Key': 'Value-data1-project', 83 | 'Key1': 'Value1-data1-project' 84 | }, 85 | TAGS: { 86 | 'Tag1': 'value1-data1-project', 87 | 'Tag2': 'value2-data1-project' 88 | }, 89 | }, 90 | TESTS: { 91 | 'test1': { 92 | NAME: 'unit1', 93 | REGIONS: ['cn-shanghai'], 94 | PARAMETERS: { 95 | 'Key1': 'Value1-data1-test', 96 | 'Key2': 'value2-data1-test' 97 | }, 98 | TAGS: { 99 | 'Key': 'Value-data1-test' 100 | } 101 | }, 102 | 'test3': { 103 | NAME: 'unit3', 104 | REGIONS: ['cn-shanghai', 'cn-hangzhou', 'cn-beijing'], 105 | } 106 | } 107 | } 108 | 109 | result = BaseConfig.merge(self.config_data, data1) 110 | self.assertEqual(result[GENERAL], self.config_data[GENERAL]) 111 | self.assertEqual(result[PROJECT][REGIONS], data1[PROJECT][REGIONS]) 112 | self.assertEqual(result[PROJECT][PARAMETERS]['Key1'], self.config_data[PROJECT][PARAMETERS]['Key1']) 113 | self.assertEqual(result[PROJECT][TAGS]['Tag1'], data1[PROJECT][TAGS]['Tag1']) 114 | self.assertEqual(result[TESTS]['test1'][NAME], data1[TESTS]['test1'][NAME]) 115 | self._pprint_json(result) 116 | 117 | async def test_get_all_configs(self): 118 | global_config = os.path.join(self.DATA_PATH, 'test_global_config.yml') 119 | global_config_path = Path(global_config).expanduser().resolve() 120 | project_config = os.path.join(self.DATA_PATH, 'test_project_config.yml') 121 | project_config_path = Path(project_config).expanduser().resolve() 122 | template_path = os.path.join(self.DATA_PATH, 'simple_template.yml') 123 | args = { 124 | PROJECT: { 125 | REGIONS: ['cn-hangzhou', 'cn-shanghai', 'cn-beijing', 'cn-qingdao'], 126 | TEMPLATE_CONFIG: { 127 | 'template_location': template_path 128 | } 129 | } 130 | } 131 | config = BaseConfig.create( 132 | global_config_path=global_config_path, 133 | project_config_file=project_config_path, 134 | args=args 135 | ) 136 | self._pprint_json(config.to_dict()) 137 | 138 | with mock.patch('iact3.plugin.oss.OssPlugin.bucket_exist', return_value=True): 139 | configs = await config.get_all_configs() 140 | self.assertEqual(8, len(configs)) 141 | 142 | def test_auth(self): 143 | default_auth = Auth() 144 | default_file = Path(DEFAULT_AUTH_FILE).expanduser().resolve() 145 | if default_file.is_file(): 146 | self.assertIsInstance(default_auth.credential, CredentialClient) 147 | else: 148 | self.assertEqual(default_auth.credential, None) 149 | 150 | auth_name_not_exit = Auth.from_dict({'name': 'not_exist'}) 151 | self.assertEqual(auth_name_not_exit.credential, None) 152 | 153 | auth_name_exit = Auth.from_dict({'name': 'test_2'}) 154 | self.assertIsInstance(auth_name_exit.credential, CredentialClient) 155 | 156 | def test_get_template(self): 157 | template_config = TemplateConfig.from_dict({}) 158 | tpl = self.DATA_PATH / 'not_exist' 159 | result = template_config._get_template_location(tpl) 160 | self.assertIsNone(result) 161 | 162 | tpl = self.DATA_PATH / 'tf' 163 | result = template_config._get_template_location(tpl) 164 | self._pprint_json(result) 165 | -------------------------------------------------------------------------------- /tests/test_cost.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from iact3.cli_modules.cost import Cost 4 | from iact3.config import IAC_NAME 5 | from tests.common import BaseTest 6 | import logging 7 | import logging.handlers 8 | 9 | class TestRun(BaseTest): 10 | 11 | async def test_cost_with_valid_config(self): 12 | config_file = os.path.join(self.DATA_PATH, 'test_config.iact3.yaml') 13 | 14 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 15 | logger = logging.getLogger() 16 | logger.addHandler(memory_handler) 17 | 18 | await Cost.create(config_file=config_file) 19 | 20 | logs = memory_handler.buffer 21 | 22 | self.assertIn("test_name: default", logs[6].getMessage()) 23 | self.assertIn("ALIYUN::ECS::Instance", logs[9].getMessage()) 24 | self.assertIn("ALIYUN::VPC::EIP", logs[10].getMessage()) 25 | self.assertIn("VPC::EIP-AssociationFlow", logs[11].getMessage()) 26 | 27 | async def test_cost_with_invalid_config(self): 28 | config_file = os.path.join(self.DATA_PATH, 'failed_cost_config.iact3.yaml') 29 | 30 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 31 | logger = logging.getLogger() 32 | logger.addHandler(memory_handler) 33 | 34 | await Cost.create(config_file=config_file) 35 | 36 | logs = memory_handler.buffer 37 | 38 | self.assertIn("test_name: default", logs[6].getMessage()) 39 | self.assertIn("StackValidationFailed", logs[7].getMessage()) 40 | self.assertIn("code:", logs[8].getMessage()) 41 | 42 | async def test_cost_with_only_template(self): 43 | template = os.path.join(self.DATA_PATH, 'simple_template.yml') 44 | 45 | with self.assertRaises(FileNotFoundError) as cm: 46 | await Cost.create(template=template) 47 | ex = cm.exception 48 | self.assertEqual(ex.errno, 2) 49 | self.assertEqual(True, ex.filename.endswith(f'.{IAC_NAME}.yml')) 50 | 51 | async def test_cost_with_multi_region(self): 52 | config_file = os.path.join(self.DATA_PATH, 'test_config.iact3.yaml') 53 | 54 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 55 | logger = logging.getLogger() 56 | logger.addHandler(memory_handler) 57 | 58 | await Cost.create(config_file=config_file,regions="cn-hangzhou,cn-beijing") 59 | 60 | logs = memory_handler.buffer 61 | 62 | self.assertIn("test_name: default", logs[11].getMessage()) 63 | self.assertIn("ALIYUN::ECS::Instance", logs[14].getMessage()) 64 | self.assertIn("cn-hangzhou", logs[14].getMessage()) 65 | self.assertIn("ALIYUN::VPC::EIP", logs[15].getMessage()) 66 | self.assertIn("VPC::EIP-AssociationFlow", logs[16].getMessage()) 67 | 68 | self.assertIn("test_name: default", logs[18].getMessage()) 69 | self.assertIn("ALIYUN::ECS::Instance", logs[21].getMessage()) 70 | self.assertIn("cn-beijing", logs[21].getMessage()) 71 | self.assertIn("ALIYUN::VPC::EIP", logs[22].getMessage()) 72 | self.assertIn("VPC::EIP-AssociationFlow", logs[23].getMessage()) 73 | -------------------------------------------------------------------------------- /tests/test_generate_param.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | from iact3.config import TestConfig, TemplateConfig 4 | from iact3.generate_params import ParamGenerator 5 | from tests.common import BaseTest 6 | 7 | 8 | class TestParamGen(BaseTest): 9 | 10 | async def test_get_template_with_url(self): 11 | test_url = [ 12 | 'oss://iactvt-beijing/local_file_test/ecs_instance.template.json?RegionId=cn-beijing', 13 | 'https://iactvt-beijing.oss-cn-beijing.aliyuncs.com/ecs_instance.template.json', 14 | f'file://{self.DATA_PATH}/ecs_instance.template.json' 15 | ] 16 | for u in test_url: 17 | test_config = TestConfig.from_dict({ 18 | 'template_config': { 19 | 'template_url': u 20 | } 21 | }) 22 | pg = ParamGenerator(test_config) 23 | result = await pg._get_template_body() 24 | template_body = yaml.safe_load(result) 25 | self._pprint_json(template_body) 26 | 27 | async def test_get_template_with_id(self): 28 | template_id = 'fe78dcd0-e5e2-4a9c-9b31-ca1e00e0f982' 29 | config = TestConfig.from_dict({ 30 | 'template_config': { 31 | 'template_id': template_id, 32 | 'template_version': 'v1' 33 | } 34 | }) 35 | pg = ParamGenerator(config) 36 | result = await pg._get_template_body() 37 | template_body = yaml.safe_load(result) 38 | self._pprint_json(template_body) 39 | 40 | async def test_get_parameters_order(self): 41 | config = TestConfig.from_dict({ 42 | 'template_config': { 43 | 'template_url': f'file://{self.DATA_PATH}/ecs_instance.template.json' 44 | } 45 | }) 46 | pg = ParamGenerator(config) 47 | template_order = await pg._get_parameters_order() 48 | self._pprint_json(template_order) 49 | 50 | async def test_generate_parameters(self): 51 | auto = '$[iact3-auto]' 52 | tpl_config = TemplateConfig.from_dict({ 53 | 'template_url': f'file://{self.DATA_PATH}/ecs_instance.template.json' 54 | }) 55 | tpl_args = tpl_config.generate_template_args() 56 | config = TestConfig.from_dict({ 57 | 'template_config': tpl_args, 58 | 'parameters': { 59 | 'ZoneId': auto, 60 | 'InstanceType': auto, 61 | 'SystemDiskCategory': auto, 62 | 'DataDiskCategory': auto, 63 | 'VpcId': auto, 64 | 'VswitchId': auto, 65 | 'CommonName': auto, 66 | 'Password': auto, 67 | 'NetworkType': 'vpc', 68 | 'InstanceChargeType': 'Postpaid', 69 | 'AllocatePublicIP': False, 70 | 'SecurityGroupId': auto 71 | } 72 | }) 73 | config.region = self.REGION_ID 74 | config.test_name = 'default' 75 | resolved_parameters = await ParamGenerator.result(config) 76 | self._pprint_json(resolved_parameters.parameters) 77 | 78 | async def test_generate_parameters_time_out(self): 79 | auto = '$[iact3-auto]' 80 | tpl_config = TemplateConfig.from_dict({ 81 | 'template_url': f'file://{self.DATA_PATH}/timeout_template.yml' 82 | }) 83 | tpl_args = tpl_config.generate_template_args() 84 | config = TestConfig.from_dict({ 85 | 'template_config': tpl_args, 86 | 'parameters': { 87 | 'ZoneId': 'cn-hddddd', 88 | 'DBInstanceClass': auto, 89 | 'DBPassword': auto 90 | } 91 | }) 92 | config.region = self.REGION_ID 93 | config.test_name = 'default' 94 | resolved_parameters = await ParamGenerator.result(config) 95 | self._pprint_json(resolved_parameters.parameters) 96 | 97 | async def test_generate_parameters_for_error_log(self): 98 | auto = '$[iact3-auto]' 99 | tpl_config = TemplateConfig.from_dict({ 100 | 'template_url': f'file://{self.DATA_PATH}/ecs_instance.template.json' 101 | }) 102 | tpl_args = tpl_config.generate_template_args() 103 | config = TestConfig.from_dict({ 104 | 'template_config': tpl_args, 105 | 'parameters': { 106 | 'ZoneId': auto, 107 | 'InstanceType': auto, 108 | 'SystemDiskCategory': 'cloud_ssd', 109 | 'DataDiskCategory': 'cloud_ssd', 110 | 'CommonName': auto, 111 | 'Password': auto, 112 | 'NetworkType': 'vpc', 113 | 'InstanceChargeType': 'Postpaid', 114 | 'AllocatePublicIP': False, 115 | 'ImageId': 'm-not-exist' 116 | } 117 | }) 118 | config.region = self.REGION_ID 119 | config.test_name = 'default' 120 | resolved_parameters = await ParamGenerator.result(config) 121 | self._pprint_json(resolved_parameters.parameters) 122 | -------------------------------------------------------------------------------- /tests/test_policy.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from iact3.cli_modules.policy import Policy 4 | from iact3.config import IAC_NAME 5 | from tests.common import BaseTest 6 | import logging 7 | import logging.handlers 8 | 9 | class TestRun(BaseTest): 10 | async def test_policy_with_valid_template(self): 11 | template = os.path.join(self.DATA_PATH, 'simple_template.yml') 12 | 13 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 14 | logger = logging.getLogger() 15 | logger.addHandler(memory_handler) 16 | 17 | await Policy.create(template=template) 18 | logs = memory_handler.buffer 19 | 20 | result = '{\n "Statement": [],\n "Version": "1"\n}' 21 | 22 | self.assertEqual(result, logs[2].getMessage()) 23 | 24 | async def test_policy_with_no_args(self): 25 | with self.assertRaises(FileNotFoundError) as cm: 26 | await Policy.create() 27 | ex = cm.exception 28 | self.assertEqual(ex.errno, 2) 29 | self.assertEqual(True, ex.filename.endswith(f'.{IAC_NAME}.yml')) -------------------------------------------------------------------------------- /tests/test_preview.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from iact3.cli_modules.preview import Preview 4 | from iact3.config import IAC_NAME 5 | from tests.common import BaseTest 6 | import logging 7 | import logging.handlers 8 | 9 | class TestRun(BaseTest): 10 | async def test_preview_with_valid_config(self): 11 | config_file = os.path.join(self.DATA_PATH, 'test_config.iact3.yaml') 12 | 13 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 14 | logger = logging.getLogger() 15 | logger.addHandler(memory_handler) 16 | 17 | await Preview.create(config_file=config_file) 18 | 19 | logs = memory_handler.buffer 20 | 21 | self.assertIn("test_name: default", logs[6].getMessage()) 22 | self.assertIn("VPC::EIPAssociation", logs[10].getMessage()) 23 | self.assertIn("\"AllocationId\": \"EIP\",", logs[11].getMessage()) 24 | 25 | async def test_preview_with_invalid_config(self): 26 | config_file = os.path.join(self.DATA_PATH, 'failed_cost_config.iact3.yaml') 27 | 28 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 29 | logger = logging.getLogger() 30 | logger.addHandler(memory_handler) 31 | 32 | await Preview.create(config_file=config_file) 33 | 34 | logs = memory_handler.buffer 35 | 36 | self.assertIn("test_name: default", logs[6].getMessage()) 37 | self.assertIn("region: cn-hangzhou", logs[7].getMessage()) 38 | self.assertIn("StackValidationFailed", logs[8].getMessage()) 39 | self.assertIn("code:", logs[9].getMessage()) 40 | 41 | async def test_preview_with_only_template(self): 42 | template = os.path.join(self.DATA_PATH, 'simple_template.yml') 43 | 44 | with self.assertRaises(FileNotFoundError) as cm: 45 | await Preview.create(template=template) 46 | ex = cm.exception 47 | self.assertEqual(ex.errno, 2) 48 | self.assertEqual(True, ex.filename.endswith(f'.{IAC_NAME}.yml')) 49 | 50 | async def test_preview_with_multi_region(self): 51 | config_file = os.path.join(self.DATA_PATH, 'test_config.iact3.yaml') 52 | 53 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 54 | logger = logging.getLogger() 55 | logger.addHandler(memory_handler) 56 | 57 | await Preview.create(config_file=config_file,regions="cn-hangzhou,cn-beijing") 58 | 59 | logs = memory_handler.buffer 60 | 61 | self.assertIn("test_name: default", logs[11].getMessage()) 62 | self.assertIn("test_name: default", logs[100].getMessage()) 63 | 64 | -------------------------------------------------------------------------------- /tests/test_report.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from iact3.report.generate_reports import ReportBuilder 4 | from iact3.stack import Stacker 5 | from tests.common import BaseTest 6 | from iact3.stack import Stack 7 | 8 | 9 | class TestReport(BaseTest): 10 | 11 | async def test_index(self): 12 | stacker = Stacker('test', tests=[]) 13 | report = ReportBuilder(stacker, self.DATA_PATH) 14 | index_path = self.DATA_PATH / 'index.html' 15 | try: 16 | os.remove(index_path) 17 | except FileNotFoundError: 18 | pass 19 | self.assertEqual(os.path.exists(index_path), False) 20 | await report.generate_report() 21 | self.assertEqual(os.path.exists(index_path), True) 22 | 23 | async def test_report_default(self): 24 | stack = Stack(stack_name="test-stack", region="cn-hangzhou", stack_id="") 25 | stacker = Stacker('test', tests=[], stacks=[stack]) 26 | report = ReportBuilder(stacker, self.DATA_PATH) 27 | try: 28 | os.remove(self.DATA_PATH / "test-stack-cn-hangzhou.txt") 29 | except FileNotFoundError: 30 | pass 31 | self.assertEqual(os.path.exists(self.DATA_PATH / "test-stack-cn-hangzhou.txt"), False) 32 | 33 | report_name = await report.create_logs(log_format=None) 34 | 35 | self.assertListEqual(report_name,["test-stack-cn-hangzhou.txt","test-result.json"]) 36 | self.assertEqual(os.path.exists(self.DATA_PATH / "test-stack-cn-hangzhou.txt"), True) 37 | with open(self.DATA_PATH / "test-stack-cn-hangzhou.txt", "r") as f: 38 | lines = f.readlines() 39 | 40 | 41 | self.assertIn("Region: cn-hangzhou\n",lines) 42 | self.assertIn("StackName: test-stack\n", lines) 43 | self.assertIn("StackId: \n", lines) 44 | self.assertIn("TestedResult: Failed \n", lines) 45 | self.assertIn("ResultReason: \n", lines) 46 | self.assertIn("Events: \n", lines) 47 | self.assertIn("Resources: \n", lines) 48 | 49 | for file in report_name: 50 | try: 51 | os.remove(self.DATA_PATH / file) 52 | except FileNotFoundError: 53 | pass 54 | 55 | async def test_report_json(self): 56 | stack = Stack(stack_name="test-stack", region="cn-hangzhou", stack_id="") 57 | stacker = Stacker('test', tests=[], stacks=[stack]) 58 | report = ReportBuilder(stacker, self.DATA_PATH) 59 | try: 60 | os.remove(self.DATA_PATH / "test-stack-cn-hangzhou.json") 61 | except FileNotFoundError: 62 | pass 63 | self.assertEqual(os.path.exists(self.DATA_PATH / "test-stack-cn-hangzhou.json"), False) 64 | 65 | report_name = await report.create_logs("json") 66 | 67 | self.assertListEqual(report_name,["test-stack-cn-hangzhou.txt","test-stack-cn-hangzhou.json","test-result.json"]) 68 | self.assertEqual(os.path.exists(self.DATA_PATH / "test-stack-cn-hangzhou.json"), True) 69 | 70 | for file in report_name: 71 | try: 72 | os.remove(self.DATA_PATH / file) 73 | except FileNotFoundError: 74 | pass 75 | 76 | async def test_report_xml(self): 77 | stack = Stack(stack_name="test-stack", region="cn-hangzhou", stack_id="") 78 | stacker = Stacker('test', tests=[], stacks=[stack]) 79 | report = ReportBuilder(stacker, self.DATA_PATH) 80 | try: 81 | os.remove(self.DATA_PATH / "test-stack-cn-hangzhou.xml") 82 | except FileNotFoundError: 83 | pass 84 | self.assertEqual(os.path.exists(self.DATA_PATH / "test-stack-cn-hangzhou.xml"), False) 85 | 86 | report_name = await report.create_logs("xml") 87 | 88 | self.assertListEqual(report_name,["test-stack-cn-hangzhou.txt","test-stack-cn-hangzhou.xml","test-result.json"]) 89 | self.assertEqual(os.path.exists(self.DATA_PATH / "test-stack-cn-hangzhou.xml"), True) 90 | 91 | for file in report_name: 92 | try: 93 | os.remove(self.DATA_PATH / file) 94 | except FileNotFoundError: 95 | pass 96 | 97 | async def test_report_json_xml(self): 98 | stack = Stack(stack_name="test-stack", region="cn-hangzhou", stack_id="") 99 | stacker = Stacker('test', tests=[], stacks=[stack]) 100 | report = ReportBuilder(stacker, self.DATA_PATH) 101 | try: 102 | os.remove(self.DATA_PATH / "test-stack-cn-hangzhou.xml") 103 | os.remove(self.DATA_PATH / "test-stack-cn-hangzhou.json") 104 | except FileNotFoundError: 105 | pass 106 | self.assertEqual(os.path.exists(self.DATA_PATH / "test-stack-cn-hangzhou.xml"), False) 107 | self.assertEqual(os.path.exists(self.DATA_PATH / "test-stack-cn-hangzhou.json"), False) 108 | 109 | report_name = await report.create_logs("xml,json") 110 | 111 | self.assertListEqual(report_name,["test-stack-cn-hangzhou.txt","test-stack-cn-hangzhou.json","test-stack-cn-hangzhou.xml","test-result.json"]) 112 | self.assertEqual(os.path.exists(self.DATA_PATH / "test-stack-cn-hangzhou.xml"), True) 113 | self.assertEqual(os.path.exists(self.DATA_PATH / "test-stack-cn-hangzhou.json"), True) 114 | 115 | for file in report_name: 116 | try: 117 | os.remove(self.DATA_PATH / file) 118 | except FileNotFoundError: 119 | pass -------------------------------------------------------------------------------- /tests/test_run.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from iact3.cli_modules.test import Test 4 | from iact3.config import IAC_NAME 5 | from tests.common import BaseTest 6 | 7 | 8 | class TestRun(BaseTest): 9 | 10 | async def test_run_with_no_args(self): 11 | with self.assertRaises(FileNotFoundError) as cm: 12 | await Test.run() 13 | ex = cm.exception 14 | self.assertEqual(ex.errno, 2) 15 | self.assertEqual(True, ex.filename.endswith(f'.{IAC_NAME}.yml')) 16 | 17 | async def test_run_with_simple(self): 18 | config_file = os.path.join(self.DATA_PATH, f'.{IAC_NAME}.yml') 19 | template = os.path.join(self.DATA_PATH, 'simple_template.yml') 20 | await Test.run(config_file=config_file, template=template) 21 | 22 | async def test_run_with_delete_one_stack(self): 23 | config_file = os.path.join(self.DATA_PATH, f'.{IAC_NAME}.yml') 24 | template = os.path.join(self.DATA_PATH, 'simple_template.yml') 25 | await Test.run(config_file=config_file, template=template, no_delete=True) 26 | 27 | -------------------------------------------------------------------------------- /tests/test_terminalprinter.py: -------------------------------------------------------------------------------- 1 | from iact3.termial_print import TerminalPrinter 2 | from tests.common import BaseTest 3 | from iact3.stack import Stacker 4 | from iact3.stack import Stack 5 | 6 | import logging 7 | import logging.handlers 8 | 9 | class TestConfig(BaseTest): 10 | async def test_display_price(self): 11 | template_price = { 12 | "KubernetesCluster":{ 13 | "Type": "ALIYUN::CS::KubernetesCluster", 14 | "Result": { 15 | "Order":{ 16 | "OriginalAmount":0, 17 | "DiscountAmount":0, 18 | "TradeAmount":0, 19 | "Currency":"" 20 | }, 21 | "OrderSupplement": { 22 | "PriceUnit": "/Hour", 23 | "ChargeType": "PostPaid", 24 | "Quantity": 1 25 | }, 26 | "AssociationProducts":{ 27 | "SlbInstanceApiServer": { 28 | "Type": "ALIYUN::SLB::LoadBalancer", 29 | "Result":{ 30 | "Order": { 31 | "OriginalAmount": 2, 32 | "DiscountAmount": 1, 33 | "TradeAmount": 1, 34 | "Currency": "" 35 | }, 36 | "OrderSupplement": { 37 | "PriceUnit": "/Hour", 38 | "ChargeType": "PostPaid", 39 | "Quantity": 1 40 | } 41 | }, 42 | "AssociationCU": { 43 | "OrderSupplement": { 44 | "PriceUnit": "/CU", 45 | "ChargeType": "PostPaid", 46 | "Quantity": 1, 47 | "PriceType": "Unit" 48 | }, 49 | "Result": { 50 | "Order": { 51 | "Currency": "CNY", 52 | "TradeAmount": 0.161, 53 | "OriginalAmount": 0.23, 54 | "OptionalMixPromotions": [], 55 | "DiscountAmount": 0.069 56 | }, 57 | "OrderSupplement": { 58 | "PriceUnit": "/CU", 59 | "ChargeType": "PostPaid", 60 | "Quantity": 1, 61 | "PriceType": "Unit" 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 72 | logger = logging.getLogger() 73 | logger.addHandler(memory_handler) 74 | 75 | stack = Stack(stack_name="test-stack", region="cn-hangzhou", stack_id="",template_price=template_price) 76 | stacker = Stacker('test', tests=[], stacks=[stack]) 77 | TerminalPrinter._display_price(stacker) 78 | 79 | logs = memory_handler.buffer 80 | 81 | self.assertIn("KubernetesCluster cn-hangzhou ALIYUN::CS::KubernetesCluster PostPaid /Hour 1 0 0 0 ", logs[3].getMessage()) 82 | self.assertIn("ALIYUN::SLB::LoadBalancer PostPaid /Hour 1 2 1 1", logs[4].getMessage()) 83 | self.assertIn("SLB::LoadBalancer-AssociationCU PostPaid /CU 1 CNY 0.23 0.069 0.161", logs[5].getMessage()) 84 | 85 | async def test_display_price_with_no_template_price(self): 86 | 87 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 88 | logger = logging.getLogger() 89 | logger.addHandler(memory_handler) 90 | 91 | stack = Stack(stack_name="test-stack", region="cn-hangzhou", stack_id="", template_price=None, status_reason="**") 92 | stack.status = "*" 93 | stacker = Stacker('test', tests=[], stacks=[stack]) 94 | TerminalPrinter._display_price(stacker) 95 | 96 | logs = memory_handler.buffer 97 | self.assertIn("*", logs[1].getMessage()) 98 | self.assertIn("**", logs[2].getMessage()) 99 | 100 | async def test_display_validation(self): 101 | template_validation = { 102 | "RequestId": "***", 103 | "HostId": "ros.aliyuncs.com", 104 | "Code": "InvalidTemplate", 105 | "Message": "Resource [VSwitch]: The specified reference \"ZoneId\" (in unknown) is incorrect.", 106 | "Recommend": "https://api.aliyun.com/troubleshoot?q=InvalidTemplate&product=ROS" 107 | } 108 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 109 | logger = logging.getLogger() 110 | logger.addHandler(memory_handler) 111 | 112 | TerminalPrinter._display_validation(template_validation) 113 | logs = memory_handler.buffer 114 | self.assertEqual("validate_result result_reason",logs[0].getMessage()) 115 | self.assertIn("InvalidTemplate", logs[2].getMessage()) 116 | 117 | template_validation = { 118 | 119 | } 120 | 121 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 122 | logger = logging.getLogger() 123 | logger.addHandler(memory_handler) 124 | 125 | TerminalPrinter._display_validation(template_validation) 126 | logs = memory_handler.buffer 127 | self.assertEqual("validate_result result_reason", logs[0].getMessage()) 128 | self.assertIn("LegalTemplate Check passed", logs[2].getMessage()) 129 | 130 | async def test_display_preview_resources(self): 131 | preview_result = [ 132 | { 133 | "ResourceType": "ALIYUN::VPC::EIPAssociation", 134 | "LogicalResourceId": "EipBind", 135 | "Properties": { 136 | "InstanceId": "EcsInstance", 137 | "AllocationId": "EIP", 138 | "InstanceType": "EcsInstance" 139 | } 140 | } 141 | ] 142 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 143 | logger = logging.getLogger() 144 | logger.addHandler(memory_handler) 145 | stack = Stack(stack_name="test-stack", region="cn-hangzhou", stack_id="", preview_result=preview_result) 146 | stacker = Stacker('test', tests=[], stacks=[stack]) 147 | TerminalPrinter._display_preview_resources(stacker) 148 | logs = memory_handler.buffer 149 | self.assertIn("EipBind VPC::EIPAssociation {", logs[4].getMessage()) 150 | self.assertIn("\"AllocationId\": \"EIP\",", logs[5].getMessage()) 151 | 152 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 153 | logger = logging.getLogger() 154 | logger.addHandler(memory_handler) 155 | stack = Stack(stack_name="test-stack", region="cn-hangzhou", stack_id="", preview_result=None,status_reason="**") 156 | stack.status = "*" 157 | stacker = Stacker('test', tests=[], stacks=[stack]) 158 | TerminalPrinter._display_preview_resources(stacker) 159 | logs = memory_handler.buffer 160 | self.assertIn("*", logs[2].getMessage()) 161 | self.assertIn("**", logs[3].getMessage()) 162 | 163 | -------------------------------------------------------------------------------- /tests/test_validate.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from iact3.cli_modules.validate import Validate 4 | from iact3.config import IAC_NAME 5 | from tests.common import BaseTest 6 | import logging 7 | import logging.handlers 8 | 9 | class TestRun(BaseTest): 10 | 11 | async def test_validate_with_valid_template(self): 12 | template = os.path.join(self.DATA_PATH, 'simple_template.yml') 13 | 14 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 15 | logger = logging.getLogger() 16 | logger.addHandler(memory_handler) 17 | 18 | await Validate.create(template=template) 19 | logs = memory_handler.buffer 20 | 21 | self.assertEqual("validate_result result_reason", logs[2].getMessage()) 22 | self.assertEqual("----------------- ---------------", logs[3].getMessage()) 23 | self.assertEqual("LegalTemplate Check passed", logs[4].getMessage()) 24 | 25 | async def test_validate_with_invalid_template(self): 26 | template = os.path.join(self.DATA_PATH, 'failed_validate_template.yml') 27 | 28 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 29 | logger = logging.getLogger() 30 | logger.addHandler(memory_handler) 31 | 32 | await Validate.create(template=template) 33 | logs = memory_handler.buffer 34 | 35 | self.assertEqual("validate_result result_reason", logs[2].getMessage()) 36 | self.assertIn("Invalid", logs[4].getMessage()) 37 | 38 | async def test_validate_with_valid_config(self): 39 | config_file = os.path.join(self.DATA_PATH, 'test_config.iact3.yaml') 40 | 41 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 42 | logger = logging.getLogger() 43 | logger.addHandler(memory_handler) 44 | 45 | await Validate.create(config_file=config_file) 46 | logs = memory_handler.buffer 47 | 48 | self.assertEqual("validate_result result_reason", logs[2].getMessage()) 49 | self.assertEqual("----------------- ---------------", logs[3].getMessage()) 50 | self.assertEqual("LegalTemplate Check passed", logs[4].getMessage()) 51 | 52 | async def test_validate_with_invalid_config(self): 53 | config_file = os.path.join(self.DATA_PATH, 'failed_test_validate_config.yml') 54 | 55 | memory_handler = logging.handlers.MemoryHandler(capacity=10240) 56 | logger = logging.getLogger() 57 | logger.addHandler(memory_handler) 58 | 59 | await Validate.create(config_file=config_file) 60 | logs = memory_handler.buffer 61 | 62 | self.assertEqual("validate_result result_reason", logs[2].getMessage()) 63 | self.assertIn("Invalid", logs[4].getMessage()) 64 | 65 | async def test_validate_with_no_args(self): 66 | with self.assertRaises(FileNotFoundError) as cm: 67 | await Validate.create() 68 | ex = cm.exception 69 | self.assertEqual(ex.errno, 2) 70 | self.assertEqual(True, ex.filename.endswith(f'.{IAC_NAME}.yml')) -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36,py37,py38,py39,py310 3 | 4 | [gh-actions] 5 | python = 6 | 3.6: py36 7 | 3.7: py37 8 | 3.8: py38 9 | 3.9: py39 10 | 3.10: py310 11 | 12 | [testenv] 13 | deps=-rrequirements-dev.txt 14 | commands= 15 | pytest --------------------------------------------------------------------------------

219 | Test Name 220 | 222 | Tested Region 223 | 225 | Stack Name 226 | 228 | Tested Results 229 | 231 | Test Logs 232 |