ic-code 1.0.0__tar.gz → 1.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. {ic_code-1.0.0/src/ic_code.egg-info → ic_code-1.0.2}/PKG-INFO +9 -5
  2. {ic_code-1.0.0 → ic_code-1.0.2}/README.md +8 -4
  3. ic_code-1.0.2/aws/cloudfront/__init__.py +16 -0
  4. ic_code-1.0.2/aws/cloudfront/info.py +205 -0
  5. ic_code-1.0.2/aws/profile/__init__.py +14 -0
  6. ic_code-1.0.2/aws/profile/info.py +311 -0
  7. {ic_code-1.0.0 → ic_code-1.0.2}/cf/dns/list_info.py +1 -1
  8. ic_code-1.0.2/docs/config_migration.md +169 -0
  9. {ic_code-1.0.0 → ic_code-1.0.2}/docs/configuration.md +3 -1
  10. {ic_code-1.0.0 → ic_code-1.0.2}/docs/migration_guide.md +4 -2
  11. {ic_code-1.0.0 → ic_code-1.0.2}/docs/security.md +5 -4
  12. {ic_code-1.0.0 → ic_code-1.0.2}/docs/user_guide.md +9 -3
  13. ic_code-1.0.2/oci_module/compartment/__init__.py +16 -0
  14. ic_code-1.0.2/oci_module/compartment/info.py +195 -0
  15. {ic_code-1.0.0 → ic_code-1.0.2}/pyproject.toml +1 -1
  16. {ic_code-1.0.0 → ic_code-1.0.2}/scripts/validate_config.py +13 -3
  17. {ic_code-1.0.0 → ic_code-1.0.2}/setup.py +82 -0
  18. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/cli.py +365 -0
  19. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/commands/config.py +243 -2
  20. ic_code-1.0.2/src/ic/config/installer.py +360 -0
  21. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/config/manager.py +228 -6
  22. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/config/schema.py +1 -1
  23. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/config/secrets.py +16 -9
  24. {ic_code-1.0.0 → ic_code-1.0.2/src/ic_code.egg-info}/PKG-INFO +9 -5
  25. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic_code.egg-info/SOURCES.txt +8 -0
  26. {ic_code-1.0.0 → ic_code-1.0.2}/CHANGELOG.md +0 -0
  27. {ic_code-1.0.0 → ic_code-1.0.2}/LICENSE +0 -0
  28. {ic_code-1.0.0 → ic_code-1.0.2}/MANIFEST.in +0 -0
  29. {ic_code-1.0.0 → ic_code-1.0.2}/SECURITY.md +0 -0
  30. {ic_code-1.0.0 → ic_code-1.0.2}/aws/__init__.py +0 -0
  31. {ic_code-1.0.0 → ic_code-1.0.2}/aws/codepipeline/__init__.py +0 -0
  32. {ic_code-1.0.0 → ic_code-1.0.2}/aws/codepipeline/build.py +0 -0
  33. {ic_code-1.0.0 → ic_code-1.0.2}/aws/codepipeline/deploy.py +0 -0
  34. {ic_code-1.0.0 → ic_code-1.0.2}/aws/ec2/__init__.py +0 -0
  35. {ic_code-1.0.0 → ic_code-1.0.2}/aws/ec2/info.py +0 -0
  36. {ic_code-1.0.0 → ic_code-1.0.2}/aws/ec2/list_tags.py +0 -0
  37. {ic_code-1.0.0 → ic_code-1.0.2}/aws/ec2/tag_check.py +0 -0
  38. {ic_code-1.0.0 → ic_code-1.0.2}/aws/ecs/__init__.py +0 -0
  39. {ic_code-1.0.0 → ic_code-1.0.2}/aws/ecs/info.py +0 -0
  40. {ic_code-1.0.0 → ic_code-1.0.2}/aws/ecs/service.py +0 -0
  41. {ic_code-1.0.0 → ic_code-1.0.2}/aws/ecs/task.py +0 -0
  42. {ic_code-1.0.0 → ic_code-1.0.2}/aws/eks/__init__.py +0 -0
  43. {ic_code-1.0.0 → ic_code-1.0.2}/aws/eks/addons.py +0 -0
  44. {ic_code-1.0.0 → ic_code-1.0.2}/aws/eks/fargate.py +0 -0
  45. {ic_code-1.0.0 → ic_code-1.0.2}/aws/eks/info.py +0 -0
  46. {ic_code-1.0.0 → ic_code-1.0.2}/aws/eks/nodes.py +0 -0
  47. {ic_code-1.0.0 → ic_code-1.0.2}/aws/eks/pods.py +0 -0
  48. {ic_code-1.0.0 → ic_code-1.0.2}/aws/eks/update_config.py +0 -0
  49. {ic_code-1.0.0 → ic_code-1.0.2}/aws/fargate/__init__.py +0 -0
  50. {ic_code-1.0.0 → ic_code-1.0.2}/aws/fargate/info.py +0 -0
  51. {ic_code-1.0.0 → ic_code-1.0.2}/aws/lb/__init__.py +0 -0
  52. {ic_code-1.0.0 → ic_code-1.0.2}/aws/lb/info.py +0 -0
  53. {ic_code-1.0.0 → ic_code-1.0.2}/aws/lb/list_tags.py +0 -0
  54. {ic_code-1.0.0 → ic_code-1.0.2}/aws/lb/tag_check.py +0 -0
  55. {ic_code-1.0.0 → ic_code-1.0.2}/aws/msk/__init__.py +0 -0
  56. {ic_code-1.0.0 → ic_code-1.0.2}/aws/msk/broker.py +0 -0
  57. {ic_code-1.0.0 → ic_code-1.0.2}/aws/msk/info.py +0 -0
  58. {ic_code-1.0.0 → ic_code-1.0.2}/aws/nat/__init__.py +0 -0
  59. {ic_code-1.0.0 → ic_code-1.0.2}/aws/nat/list_tags.py +0 -0
  60. {ic_code-1.0.0 → ic_code-1.0.2}/aws/nat/tag_check.py +0 -0
  61. {ic_code-1.0.0 → ic_code-1.0.2}/aws/rds/__init__.py +0 -0
  62. {ic_code-1.0.0 → ic_code-1.0.2}/aws/rds/info.py +0 -0
  63. {ic_code-1.0.0 → ic_code-1.0.2}/aws/rds/list_tags.py +0 -0
  64. {ic_code-1.0.0 → ic_code-1.0.2}/aws/rds/tag_check.py +0 -0
  65. {ic_code-1.0.0 → ic_code-1.0.2}/aws/s3/__init__.py +0 -0
  66. {ic_code-1.0.0 → ic_code-1.0.2}/aws/s3/info.py +0 -0
  67. {ic_code-1.0.0 → ic_code-1.0.2}/aws/s3/list_tags.py +0 -0
  68. {ic_code-1.0.0 → ic_code-1.0.2}/aws/s3/tag_check.py +0 -0
  69. {ic_code-1.0.0 → ic_code-1.0.2}/aws/sg/__init__.py +0 -0
  70. {ic_code-1.0.0 → ic_code-1.0.2}/aws/sg/info.py +0 -0
  71. {ic_code-1.0.0 → ic_code-1.0.2}/aws/vpc/__init__.py +0 -0
  72. {ic_code-1.0.0 → ic_code-1.0.2}/aws/vpc/info.py +0 -0
  73. {ic_code-1.0.0 → ic_code-1.0.2}/aws/vpc/list_tags.py +0 -0
  74. {ic_code-1.0.0 → ic_code-1.0.2}/aws/vpc/tag_check.py +0 -0
  75. {ic_code-1.0.0 → ic_code-1.0.2}/aws/vpn/__init__.py +0 -0
  76. {ic_code-1.0.0 → ic_code-1.0.2}/aws/vpn/info.py +0 -0
  77. {ic_code-1.0.0 → ic_code-1.0.2}/azure_module/__init__.py +0 -0
  78. {ic_code-1.0.0 → ic_code-1.0.2}/azure_module/aci/info.py +0 -0
  79. {ic_code-1.0.0 → ic_code-1.0.2}/azure_module/aks/info.py +0 -0
  80. {ic_code-1.0.0 → ic_code-1.0.2}/azure_module/lb/info.py +0 -0
  81. {ic_code-1.0.0 → ic_code-1.0.2}/azure_module/nsg/info.py +0 -0
  82. {ic_code-1.0.0 → ic_code-1.0.2}/azure_module/storage/info.py +0 -0
  83. {ic_code-1.0.0 → ic_code-1.0.2}/azure_module/vm/info.py +0 -0
  84. {ic_code-1.0.0 → ic_code-1.0.2}/azure_module/vnet/info.py +0 -0
  85. {ic_code-1.0.0 → ic_code-1.0.2}/cf/__init__.py +0 -0
  86. {ic_code-1.0.0 → ic_code-1.0.2}/cf/dns/__init__.py +0 -0
  87. {ic_code-1.0.0 → ic_code-1.0.2}/common/__init__.py +0 -0
  88. {ic_code-1.0.0 → ic_code-1.0.2}/common/azure_utils.py +0 -0
  89. {ic_code-1.0.0 → ic_code-1.0.2}/common/gather_env.py +0 -0
  90. {ic_code-1.0.0 → ic_code-1.0.2}/common/gcp_config_validator.py +0 -0
  91. {ic_code-1.0.0 → ic_code-1.0.2}/common/gcp_monitoring.py +0 -0
  92. {ic_code-1.0.0 → ic_code-1.0.2}/common/gcp_utils.py +0 -0
  93. {ic_code-1.0.0 → ic_code-1.0.2}/common/log.py +0 -0
  94. {ic_code-1.0.0 → ic_code-1.0.2}/common/slack.py +0 -0
  95. {ic_code-1.0.0 → ic_code-1.0.2}/common/utils.py +0 -0
  96. {ic_code-1.0.0 → ic_code-1.0.2}/config/default.yaml +0 -0
  97. {ic_code-1.0.0 → ic_code-1.0.2}/config/secrets.yaml.example +0 -0
  98. {ic_code-1.0.0 → ic_code-1.0.2}/docs/PYPI_DEPLOYMENT.md +0 -0
  99. {ic_code-1.0.0 → ic_code-1.0.2}/docs/README.md +0 -0
  100. {ic_code-1.0.0 → ic_code-1.0.2}/docs/aws/AWS_IMPLEMENTATION_SUMMARY.md +0 -0
  101. {ic_code-1.0.0 → ic_code-1.0.2}/docs/aws/README.md +0 -0
  102. {ic_code-1.0.0 → ic_code-1.0.2}/docs/aws/aws_cli_prd.md +0 -0
  103. {ic_code-1.0.0 → ic_code-1.0.2}/docs/deployment.md +0 -0
  104. {ic_code-1.0.0 → ic_code-1.0.2}/docs/final_validation_report.md +0 -0
  105. {ic_code-1.0.0 → ic_code-1.0.2}/docs/gcp/GCP_CONFIGURATION_GUIDE.md +0 -0
  106. {ic_code-1.0.0 → ic_code-1.0.2}/docs/gcp/GCP_SECURITY_BEST_PRACTICES.md +0 -0
  107. {ic_code-1.0.0 → ic_code-1.0.2}/docs/installation.md +0 -0
  108. {ic_code-1.0.0 → ic_code-1.0.2}/docs/mcp_integration.md +0 -0
  109. {ic_code-1.0.0 → ic_code-1.0.2}/docs/migration.md +0 -0
  110. {ic_code-1.0.0 → ic_code-1.0.2}/docs/migration_history.md +0 -0
  111. {ic_code-1.0.0 → ic_code-1.0.2}/docs/troubleshooting.md +0 -0
  112. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/__init__.py +0 -0
  113. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/billing/__init__.py +0 -0
  114. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/billing/info.py +0 -0
  115. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/compute/__init__.py +0 -0
  116. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/compute/info.py +0 -0
  117. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/firewall/__init__.py +0 -0
  118. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/firewall/info.py +0 -0
  119. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/functions/__init__.py +0 -0
  120. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/functions/info.py +0 -0
  121. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/gke/__init__.py +0 -0
  122. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/gke/info.py +0 -0
  123. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/lb/__init__.py +0 -0
  124. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/lb/info.py +0 -0
  125. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/run/__init__.py +0 -0
  126. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/run/info.py +0 -0
  127. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/sql/__init__.py +0 -0
  128. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/sql/info.py +0 -0
  129. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/storage/__init__.py +0 -0
  130. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/storage/info.py +0 -0
  131. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/vpc/__init__.py +0 -0
  132. {ic_code-1.0.0 → ic_code-1.0.2}/gcp/vpc/info.py +0 -0
  133. {ic_code-1.0.0 → ic_code-1.0.2}/mcp/__init__.py +0 -0
  134. {ic_code-1.0.0 → ic_code-1.0.2}/mcp/gcp_connector.py +0 -0
  135. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/__init__.py +0 -0
  136. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/common/__init__.py +0 -0
  137. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/common/utils.py +0 -0
  138. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/cost/__init__.py +0 -0
  139. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/cost/credit.py +0 -0
  140. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/cost/usage.py +0 -0
  141. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/info/__init__.py +0 -0
  142. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/info/oci_info.py +0 -0
  143. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/lb/__init__.py +0 -0
  144. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/lb/info.py +0 -0
  145. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/nsg/__init__.py +0 -0
  146. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/nsg/info.py +0 -0
  147. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/obj/__init__.py +0 -0
  148. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/obj/info.py +0 -0
  149. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/policy/__init__.py +0 -0
  150. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/policy/info.py +0 -0
  151. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/policy/search.py +0 -0
  152. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/vcn/__init__.py +0 -0
  153. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/vcn/info.py +0 -0
  154. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/vm/__init__.py +0 -0
  155. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/vm/info.py +0 -0
  156. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/volume/__init__.py +0 -0
  157. {ic_code-1.0.0 → ic_code-1.0.2}/oci_module/volume/info.py +0 -0
  158. {ic_code-1.0.0 → ic_code-1.0.2}/requirements.txt +0 -0
  159. {ic_code-1.0.0 → ic_code-1.0.2}/scripts/bump-version.sh +0 -0
  160. {ic_code-1.0.0 → ic_code-1.0.2}/scripts/cf_edge_nsg_make/cf_edge_nsg_make.py +0 -0
  161. {ic_code-1.0.0 → ic_code-1.0.2}/scripts/deploy.sh +0 -0
  162. {ic_code-1.0.0 → ic_code-1.0.2}/scripts/final_validation_and_documentation.py +0 -0
  163. {ic_code-1.0.0 → ic_code-1.0.2}/scripts/migration_execution_and_validation.py +0 -0
  164. {ic_code-1.0.0 → ic_code-1.0.2}/scripts/oracle-policy/user_policy.py +0 -0
  165. {ic_code-1.0.0 → ic_code-1.0.2}/scripts/performance_optimization.py +0 -0
  166. {ic_code-1.0.0 → ic_code-1.0.2}/scripts/setup_venv.py +0 -0
  167. {ic_code-1.0.0 → ic_code-1.0.2}/scripts/unset_env.sh +0 -0
  168. {ic_code-1.0.0 → ic_code-1.0.2}/setup.cfg +0 -0
  169. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/__init__.py +0 -0
  170. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/commands/__init__.py +0 -0
  171. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/compat/__init__.py +0 -0
  172. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/compat/cli.py +0 -0
  173. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/compat/common.py +0 -0
  174. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/config/__init__.py +0 -0
  175. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/config/cleanup.py +0 -0
  176. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/config/docs_organizer.py +0 -0
  177. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/config/external.py +0 -0
  178. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/config/migration.py +0 -0
  179. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/config/security.py +0 -0
  180. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/core/__init__.py +0 -0
  181. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/core/logging.py +0 -0
  182. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/core/mcp_manager.py +0 -0
  183. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/core/session.py +0 -0
  184. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic/core/silence_logging.py +0 -0
  185. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic_code.egg-info/dependency_links.txt +0 -0
  186. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic_code.egg-info/entry_points.txt +0 -0
  187. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic_code.egg-info/requires.txt +0 -0
  188. {ic_code-1.0.0 → ic_code-1.0.2}/src/ic_code.egg-info/top_level.txt +0 -0
  189. {ic_code-1.0.0 → ic_code-1.0.2}/ssh/__init__.py +0 -0
  190. {ic_code-1.0.0 → ic_code-1.0.2}/ssh/auto_ssh.py +0 -0
  191. {ic_code-1.0.0 → ic_code-1.0.2}/ssh/server_info.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ic-code
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Infrastructure CLI tool for managing AWS, CloudFlare, SSH, and more
5
5
  Home-page: https://github.com/dgr009/ic
6
6
  Author: SangYun Kim
@@ -99,14 +99,18 @@ IC CLI uses a modern YAML-based configuration system with separate files for def
99
99
  #### 1. Create Configuration Files
100
100
 
101
101
  ```bash
102
- # Copy example files
102
+ # Copy example files (new preferred location)
103
+ mkdir -p .ic/config
104
+ cp .ic/config/secrets.yaml.example .ic/config/secrets.yaml
105
+
106
+ # Or use legacy location for backward compatibility
103
107
  cp config/secrets.yaml.example config/secrets.yaml
104
108
 
105
109
  # Edit with your actual values
106
- vim config/secrets.yaml
110
+ vim .ic/config/secrets.yaml
107
111
  ```
108
112
 
109
- #### 2. Configure Secrets (config/secrets.yaml)
113
+ #### 2. Configure Secrets (~/.ic/config/secrets.yaml)
110
114
 
111
115
  ```yaml
112
116
  # AWS Configuration
@@ -139,7 +143,7 @@ oci:
139
143
  profile: "DEFAULT"
140
144
  ```
141
145
 
142
- #### 3. Default Settings (config/default.yaml)
146
+ #### 3. Default Settings (.ic/config/default.yaml)
143
147
 
144
148
  The default configuration is already provided and includes:
145
149
  - AWS regions
@@ -50,14 +50,18 @@ IC CLI uses a modern YAML-based configuration system with separate files for def
50
50
  #### 1. Create Configuration Files
51
51
 
52
52
  ```bash
53
- # Copy example files
53
+ # Copy example files (new preferred location)
54
+ mkdir -p .ic/config
55
+ cp .ic/config/secrets.yaml.example .ic/config/secrets.yaml
56
+
57
+ # Or use legacy location for backward compatibility
54
58
  cp config/secrets.yaml.example config/secrets.yaml
55
59
 
56
60
  # Edit with your actual values
57
- vim config/secrets.yaml
61
+ vim .ic/config/secrets.yaml
58
62
  ```
59
63
 
60
- #### 2. Configure Secrets (config/secrets.yaml)
64
+ #### 2. Configure Secrets (~/.ic/config/secrets.yaml)
61
65
 
62
66
  ```yaml
63
67
  # AWS Configuration
@@ -90,7 +94,7 @@ oci:
90
94
  profile: "DEFAULT"
91
95
  ```
92
96
 
93
- #### 3. Default Settings (config/default.yaml)
97
+ #### 3. Default Settings (.ic/config/default.yaml)
94
98
 
95
99
  The default configuration is already provided and includes:
96
100
  - AWS regions
@@ -0,0 +1,16 @@
1
+ """
2
+ AWS CloudFront module for distribution information and management.
3
+
4
+ This module provides functionality to collect and display CloudFront distribution
5
+ information including origins, domains, and price classes.
6
+ """
7
+
8
+ __version__ = "1.0.0"
9
+ __author__ = "IC Development Team"
10
+
11
+ from .info import CloudFrontCollector, CloudFrontRenderer
12
+
13
+ __all__ = [
14
+ "CloudFrontCollector",
15
+ "CloudFrontRenderer"
16
+ ]
@@ -0,0 +1,205 @@
1
+ """
2
+ AWS CloudFront distribution information collector and renderer.
3
+
4
+ This module provides classes to collect CloudFront distribution details
5
+ and render them in a user-friendly table format.
6
+ """
7
+
8
+ import boto3
9
+ from typing import Dict, List, Any, Optional
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+ from botocore.exceptions import ClientError, NoCredentialsError
13
+
14
+ import boto3
15
+
16
+
17
+ class CloudFrontCollector:
18
+ """Collects CloudFront distribution information from AWS."""
19
+
20
+ def __init__(self):
21
+ self.console = Console()
22
+
23
+ def collect_distributions(self, account_profiles: Dict[str, str]) -> List[Dict[str, str]]:
24
+ """
25
+ Collect CloudFront distribution information from multiple accounts.
26
+
27
+ Args:
28
+ account_profiles: Dictionary mapping account names to AWS profile names
29
+
30
+ Returns:
31
+ List of distribution information dictionaries
32
+ """
33
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
34
+ import time
35
+
36
+ distributions = []
37
+
38
+ with Progress(
39
+ SpinnerColumn(),
40
+ TextColumn("[progress.description]{task.description}"),
41
+ BarColumn(),
42
+ TaskProgressColumn(),
43
+ console=self.console
44
+ ) as progress:
45
+
46
+ # Create main task
47
+ main_task = progress.add_task(
48
+ f"Collecting CloudFront distributions from {len(account_profiles)} account(s)...",
49
+ total=len(account_profiles)
50
+ )
51
+
52
+ for account_name, profile_name in account_profiles.items():
53
+ # Update task description
54
+ progress.update(main_task, description=f"Processing account: {account_name}")
55
+
56
+ start_time = time.time()
57
+ try:
58
+ session = boto3.Session(profile_name=profile_name)
59
+ account_distributions = self._get_account_distributions(session, account_name)
60
+ distributions.extend(account_distributions)
61
+
62
+ # Calculate processing time
63
+ processing_time = time.time() - start_time
64
+ progress.console.print(
65
+ f"✅ {account_name}: Found {len(account_distributions)} distributions "
66
+ f"({processing_time:.2f}s)"
67
+ )
68
+
69
+ except Exception as e:
70
+ progress.console.print(f"❌ Failed to collect CloudFront data for {account_name}: {e}")
71
+
72
+ # Advance progress
73
+ progress.advance(main_task)
74
+
75
+ return distributions
76
+
77
+ def _get_account_distributions(self, session: boto3.Session, account_name: str) -> List[Dict[str, str]]:
78
+ """
79
+ Get CloudFront distributions for a specific account.
80
+
81
+ Args:
82
+ session: AWS session for the account
83
+ account_name: Name of the AWS account
84
+
85
+ Returns:
86
+ List of distribution information dictionaries
87
+ """
88
+ try:
89
+ cloudfront_client = session.client('cloudfront')
90
+
91
+ # List all distributions
92
+ response = cloudfront_client.list_distributions()
93
+ distributions = []
94
+
95
+ if 'DistributionList' in response and 'Items' in response['DistributionList']:
96
+ for distribution in response['DistributionList']['Items']:
97
+ dist_info = self.get_distribution_details(distribution, account_name)
98
+ distributions.append(dist_info)
99
+
100
+ return distributions
101
+
102
+ except ClientError as e:
103
+ error_code = e.response['Error']['Code']
104
+ if error_code == 'AccessDenied':
105
+ self.console.print(f"⚠️ Access denied for CloudFront in {account_name}")
106
+ else:
107
+ self.console.print(f"❌ CloudFront API error in {account_name}: {e}")
108
+ return []
109
+ except NoCredentialsError:
110
+ self.console.print(f"❌ No credentials available for {account_name}")
111
+ return []
112
+
113
+ def get_distribution_details(self, distribution: Dict[str, Any], account_name: str) -> Dict[str, str]:
114
+ """
115
+ Extract and format distribution details.
116
+
117
+ Args:
118
+ distribution: CloudFront distribution data from AWS API
119
+ account_name: Name of the AWS account
120
+
121
+ Returns:
122
+ Dictionary with formatted distribution information
123
+ """
124
+ return {
125
+ 'account': account_name,
126
+ 'ID': distribution.get('Id', 'N/A'),
127
+ 'Name': distribution.get('Comment', 'N/A') or 'N/A',
128
+ '원본(Origin)': self.get_primary_origin(distribution.get('Origins', {}).get('Items', [])),
129
+ '도메인(Domain)': distribution.get('DomainName', 'N/A'),
130
+ 'Class': self.format_price_class(distribution.get('PriceClass', 'PriceClass_All'))
131
+ }
132
+
133
+ def get_primary_origin(self, origins: List[Dict[str, Any]]) -> str:
134
+ """
135
+ Get the primary origin or indicate multiple origins.
136
+
137
+ Args:
138
+ origins: List of origin configurations
139
+
140
+ Returns:
141
+ Primary origin domain or "Multiple" indicator
142
+ """
143
+ if not origins:
144
+ return 'N/A'
145
+ elif len(origins) == 1:
146
+ return origins[0].get('DomainName', 'N/A')
147
+ else:
148
+ return f'Multiple ({len(origins)})'
149
+
150
+ def format_price_class(self, price_class: str) -> str:
151
+ """
152
+ Convert technical price class names to human-readable format.
153
+
154
+ Args:
155
+ price_class: Technical price class name from AWS
156
+
157
+ Returns:
158
+ Human-readable price class description
159
+ """
160
+ price_class_mapping = {
161
+ 'PriceClass_All': 'All Edge Locations',
162
+ 'PriceClass_200': 'North America and Europe Only',
163
+ 'PriceClass_100': 'North America Only'
164
+ }
165
+
166
+ return price_class_mapping.get(price_class, price_class)
167
+
168
+
169
+ class CloudFrontRenderer:
170
+ """Renders CloudFront distribution information in table format."""
171
+
172
+ def __init__(self):
173
+ self.console = Console()
174
+
175
+ def render_distributions(self, distributions: List[Dict[str, str]]) -> None:
176
+ """
177
+ Render CloudFront distributions in a Rich table.
178
+
179
+ Args:
180
+ distributions: List of distribution information dictionaries
181
+ """
182
+ if not distributions:
183
+ self.console.print("📋 No CloudFront distributions found.")
184
+ return
185
+
186
+ table = Table(title="CloudFront Distributions")
187
+ table.add_column("Account", style="cyan", no_wrap=True)
188
+ table.add_column("ID", style="green", no_wrap=True)
189
+ table.add_column("Name", style="yellow")
190
+ table.add_column("원본(Origin)", style="blue")
191
+ table.add_column("도메인(Domain)", style="magenta")
192
+ table.add_column("Class", style="red")
193
+
194
+ for dist in distributions:
195
+ table.add_row(
196
+ dist['account'],
197
+ dist['ID'],
198
+ dist['Name'],
199
+ dist['원본(Origin)'],
200
+ dist['도메인(Domain)'],
201
+ dist['Class']
202
+ )
203
+
204
+ self.console.print(table)
205
+ self.console.print(f"\n📊 Total distributions: {len(distributions)}")
@@ -0,0 +1,14 @@
1
+ """
2
+ AWS Profile management module.
3
+
4
+ This module provides functionality for parsing and displaying AWS profile information
5
+ from ~/.aws/config and ~/.aws/credentials files.
6
+ """
7
+
8
+ from .info import AWSProfileParser, ProfileInfoCollector, ProfileTableRenderer
9
+
10
+ __all__ = [
11
+ 'AWSProfileParser',
12
+ 'ProfileInfoCollector',
13
+ 'ProfileTableRenderer'
14
+ ]
@@ -0,0 +1,311 @@
1
+ """
2
+ AWS profile information parser and renderer.
3
+
4
+ This module provides classes to parse AWS configuration and credentials files
5
+ and render profile information in a user-friendly table format.
6
+ """
7
+
8
+ import os
9
+ import re
10
+ import configparser
11
+ from pathlib import Path
12
+ from typing import Dict, List, Any, Optional
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+
16
+
17
+ class AWSProfileParser:
18
+ """Parses AWS configuration and credentials files."""
19
+
20
+ def __init__(self):
21
+ self.console = Console()
22
+ self.aws_config_path = Path.home() / ".aws" / "config"
23
+ self.aws_credentials_path = Path.home() / ".aws" / "credentials"
24
+
25
+ def parse_config_file(self, config_path: Optional[str] = None) -> Dict[str, Dict[str, str]]:
26
+ """
27
+ Parse AWS config file (~/.aws/config).
28
+
29
+ Args:
30
+ config_path: Optional path to config file
31
+
32
+ Returns:
33
+ Dictionary of profile configurations
34
+ """
35
+ if config_path:
36
+ config_file = Path(config_path)
37
+ else:
38
+ config_file = self.aws_config_path
39
+
40
+ if not config_file.exists():
41
+ self.console.print(f"⚠️ AWS config file not found: {config_file}")
42
+ return {}
43
+
44
+ try:
45
+ config = configparser.ConfigParser()
46
+ config.read(config_file)
47
+
48
+ profiles = {}
49
+ for section_name in config.sections():
50
+ # Handle both 'profile name' and 'name' formats
51
+ if section_name.startswith('profile '):
52
+ profile_name = section_name[8:] # Remove 'profile ' prefix
53
+ else:
54
+ profile_name = section_name
55
+
56
+ profile_data = dict(config[section_name])
57
+ profiles[profile_name] = profile_data
58
+
59
+ return profiles
60
+
61
+ except Exception as e:
62
+ self.console.print(f"❌ Failed to parse AWS config file: {e}")
63
+ return {}
64
+
65
+ def parse_credentials_file(self, creds_path: Optional[str] = None) -> Dict[str, Dict[str, str]]:
66
+ """
67
+ Parse AWS credentials file (~/.aws/credentials).
68
+
69
+ Args:
70
+ creds_path: Optional path to credentials file
71
+
72
+ Returns:
73
+ Dictionary of profile credentials
74
+ """
75
+ if creds_path:
76
+ creds_file = Path(creds_path)
77
+ else:
78
+ creds_file = self.aws_credentials_path
79
+
80
+ if not creds_file.exists():
81
+ self.console.print(f"⚠️ AWS credentials file not found: {creds_file}")
82
+ return {}
83
+
84
+ try:
85
+ config = configparser.ConfigParser()
86
+ config.read(creds_file)
87
+
88
+ credentials = {}
89
+ for section_name in config.sections():
90
+ profile_data = dict(config[section_name])
91
+ credentials[section_name] = profile_data
92
+
93
+ return credentials
94
+
95
+ except Exception as e:
96
+ self.console.print(f"❌ Failed to parse AWS credentials file: {e}")
97
+ return {}
98
+
99
+ def extract_account_from_role_arn(self, role_arn: str) -> Optional[str]:
100
+ """
101
+ Extract account ID from role ARN.
102
+
103
+ Args:
104
+ role_arn: AWS role ARN (e.g., arn:aws:iam::123456789012:role/RoleName)
105
+
106
+ Returns:
107
+ Account ID or None if not found
108
+ """
109
+ if not role_arn:
110
+ return None
111
+
112
+ # Pattern: arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME
113
+ arn_pattern = r'arn:aws:iam::(\d{12}):role/'
114
+ match = re.search(arn_pattern, role_arn)
115
+
116
+ if match:
117
+ return match.group(1)
118
+
119
+ return None
120
+
121
+ def extract_role_name_from_arn(self, role_arn: str) -> Optional[str]:
122
+ """
123
+ Extract role name from role ARN (last part after final slash).
124
+
125
+ Args:
126
+ role_arn: AWS role ARN
127
+
128
+ Returns:
129
+ Role name or None if not found
130
+ """
131
+ if not role_arn:
132
+ return None
133
+
134
+ # Extract everything after the last slash
135
+ parts = role_arn.split('/')
136
+ if len(parts) > 1:
137
+ return parts[-1]
138
+
139
+ return None
140
+
141
+
142
+ class ProfileInfoCollector:
143
+ """Collects and formats AWS profile information."""
144
+
145
+ def __init__(self):
146
+ self.parser = AWSProfileParser()
147
+ self.console = Console()
148
+
149
+ def collect_profile_info(self) -> List[Dict[str, str]]:
150
+ """
151
+ Collect comprehensive AWS profile information.
152
+
153
+ Returns:
154
+ List of profile information dictionaries
155
+ """
156
+ from rich.progress import Progress, SpinnerColumn, TextColumn
157
+ import time
158
+
159
+ with Progress(
160
+ SpinnerColumn(),
161
+ TextColumn("[progress.description]{task.description}"),
162
+ console=self.console
163
+ ) as progress:
164
+
165
+ # Parse config file
166
+ task = progress.add_task("Parsing AWS configuration files...", total=None)
167
+ start_time = time.time()
168
+
169
+ config_data = self.parser.parse_config_file()
170
+ progress.update(task, description="Parsing AWS credentials file...")
171
+
172
+ creds_data = self.parser.parse_credentials_file()
173
+ progress.update(task, description="Merging profile information...")
174
+
175
+ # Merge and format profile information
176
+ profiles = self.merge_config_and_credentials(config_data, creds_data)
177
+
178
+ # Calculate processing time
179
+ processing_time = time.time() - start_time
180
+ progress.update(task, description=f"Completed in {processing_time:.2f}s")
181
+ progress.stop()
182
+
183
+ return profiles
184
+
185
+ def merge_config_and_credentials(self, config_data: Dict[str, Dict[str, str]],
186
+ creds_data: Dict[str, Dict[str, str]]) -> List[Dict[str, str]]:
187
+ """
188
+ Merge configuration and credentials data into unified profile information.
189
+
190
+ Args:
191
+ config_data: Data from ~/.aws/config
192
+ creds_data: Data from ~/.aws/credentials
193
+
194
+ Returns:
195
+ List of merged profile information
196
+ """
197
+ profiles = []
198
+
199
+ # Get all unique profile names
200
+ all_profiles = set(config_data.keys()) | set(creds_data.keys())
201
+
202
+ for profile_name in sorted(all_profiles):
203
+ config_info = config_data.get(profile_name, {})
204
+ creds_info = creds_data.get(profile_name, {})
205
+
206
+ # Extract profile information
207
+ profile_info = {
208
+ 'profile_name': profile_name,
209
+ 'account_id': self._extract_account_id(config_info, creds_info),
210
+ 'source': config_info.get('source_profile', ''),
211
+ 'role_name': self._extract_role_name(config_info),
212
+ 'credential': 'active' if creds_info else 'inactive',
213
+ 'region': config_info.get('region', ''),
214
+ 'role_arn': config_info.get('role_arn', '')
215
+ }
216
+
217
+ profiles.append(profile_info)
218
+
219
+ return profiles
220
+
221
+ def _extract_account_id(self, config_info: Dict[str, str], creds_info: Dict[str, str]) -> str:
222
+ """Extract account ID from various sources."""
223
+ # Try direct account_id first
224
+ if 'account_id' in config_info:
225
+ return config_info['account_id']
226
+
227
+ # Try extracting from role_arn
228
+ if 'role_arn' in config_info:
229
+ account_id = self.parser.extract_account_from_role_arn(config_info['role_arn'])
230
+ if account_id:
231
+ return account_id
232
+
233
+ # Try from credentials (less common)
234
+ if 'account_id' in creds_info:
235
+ return creds_info['account_id']
236
+
237
+ return ''
238
+
239
+ def _extract_role_name(self, config_info: Dict[str, str]) -> str:
240
+ """Extract role name from configuration."""
241
+ if 'role_arn' in config_info:
242
+ role_name = self.parser.extract_role_name_from_arn(config_info['role_arn'])
243
+ if role_name:
244
+ return role_name
245
+
246
+ # Fallback to role_name if directly specified
247
+ return config_info.get('role_name', '')
248
+
249
+
250
+ class ProfileTableRenderer:
251
+ """Renders AWS profile information in table format using Rich."""
252
+
253
+ def __init__(self):
254
+ self.console = Console()
255
+
256
+ def render_profiles(self, profiles: List[Dict[str, str]]) -> None:
257
+ """
258
+ Render AWS profiles in a Rich table.
259
+
260
+ Args:
261
+ profiles: List of profile information dictionaries
262
+ """
263
+ if not profiles:
264
+ self.console.print("📋 No AWS profiles found.")
265
+ self.console.print("\n💡 Suggestions:")
266
+ self.console.print(" • Check if ~/.aws/config and ~/.aws/credentials files exist")
267
+ self.console.print(" • Run 'aws configure' to set up your first profile")
268
+ self.console.print(" • Verify AWS CLI installation")
269
+ return
270
+
271
+ table = Table(title="AWS Profiles")
272
+ table.add_column("Profile Name", style="cyan", no_wrap=True)
273
+ table.add_column("Account ID", style="green", no_wrap=True)
274
+ table.add_column("Source", style="yellow")
275
+ table.add_column("Role Name", style="blue")
276
+ table.add_column("Credential", style="magenta")
277
+ table.add_column("Region", style="red")
278
+
279
+ for profile in profiles:
280
+ # Format credential status with colors
281
+ credential_status = profile['credential']
282
+ if credential_status == 'active':
283
+ credential_display = f"[green]{credential_status}[/green]"
284
+ else:
285
+ credential_display = f"[red]{credential_status}[/red]"
286
+
287
+ table.add_row(
288
+ profile['profile_name'],
289
+ profile['account_id'] or '-',
290
+ profile['source'] or '-',
291
+ profile['role_name'] or '-',
292
+ credential_display,
293
+ profile['region'] or '-'
294
+ )
295
+
296
+ self.console.print(table)
297
+
298
+ # Display summary
299
+ active_profiles = sum(1 for p in profiles if p['credential'] == 'active')
300
+ self.console.print(f"\n📊 Total profiles: {len(profiles)} | Active: {active_profiles} | Inactive: {len(profiles) - active_profiles}")
301
+
302
+ # Display helpful information
303
+ if active_profiles == 0:
304
+ self.console.print("\n⚠️ No active profiles found (no credentials available)")
305
+ self.console.print("💡 Run 'aws configure' to set up credentials for your profiles")
306
+
307
+ # Show profiles with missing account IDs
308
+ missing_accounts = [p['profile_name'] for p in profiles if not p['account_id']]
309
+ if missing_accounts:
310
+ self.console.print(f"\n⚠️ Profiles without account ID: {', '.join(missing_accounts)}")
311
+ self.console.print("💡 Consider adding role_arn or account_id to these profiles")
@@ -162,7 +162,7 @@ def info(args):
162
162
  """
163
163
  # 인증 정보 검사
164
164
  if not CF_EMAIL or not CF_TOKEN:
165
- log_error("CloudFlare 인증 정보가 설정되지 않았습니다. config/secrets.yaml을 확인하세요.")
165
+ log_error("CloudFlare 인증 정보가 설정되지 않았습니다. .ic/config/secrets.yaml 또는 config/secrets.yaml을 확인하세요.")
166
166
  return
167
167
 
168
168
  env_accounts = []