Coverage for cli / commands / gitops_cmd.py: 100.00%

103 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-10-28 16:19 +0000

1"""GCO GitOps command group — the self-managed Argo CD. 

2 

3Provides the ``gco gitops`` sub-commands: 

4 

5* ``status`` — what cdk.json configures (``helm.argocd``): enabled, the pinned 

6 chart, the gco-tenants fence, the GitOps hand-off rendered per Region and 

7 the repo-server size or autoscaler. 

8* ``open`` — ``kubectl port-forward`` to the Argo CD UI over the PRIVATE EKS 

9 API endpoint (``--via-ssm`` tunnels through an SSM-managed instance), like 

10 ``gco monitoring open``. 

11* ``password`` — the generated ``admin`` password from 

12 ``argocd-initial-admin-secret``. 

13* ``screenshot`` — a headless capture of the Applications view (API login, 

14 session cookie, full-page PNG). 

15 

16See docs/GITOPS.md. In-cluster access always goes through the private API 

17endpoint — there is no public Argo CD ingress. 

18""" 

19 

20from __future__ import annotations 

21 

22import sys 

23from pathlib import Path 

24from typing import Any 

25 

26import click 

27 

28from .. import cluster_ui 

29from ..config import GCOConfig 

30from ..output import get_output_formatter 

31 

32pass_config = click.make_pass_decorator(GCOConfig, ensure=True) 

33 

34 

35@click.group() 

36@pass_config 

37def gitops(config: Any) -> None: 

38 """Self-managed Argo CD (cdk.json helm.argocd): GitOps into the tenant namespaces.""" 

39 

40 

41@gitops.command("status") 

42@click.option("--region", "-r", help="Render the GitOps path for this region only.") 

43@pass_config 

44def gitops_status(config: Any, region: str | None) -> None: 

45 """Show the configured Argo CD: toggle, chart pin, fence, GitOps hand-off, repo server. 

46 

47 Reads cdk.json (helm.argocd) and charts.yaml only — no AWS or cluster 

48 access. The GitOps path is rendered for every regional deployment region 

49 (or just --region). 

50 

51 Examples: 

52 gco gitops status 

53 gco gitops status -r us-west-2 --output json 

54 """ 

55 from ..gitops import gitops_status as _gitops_status 

56 

57 formatter = get_output_formatter(config) 

58 try: 

59 regions = ( 

60 [str(region)] if region else cluster_ui.regional_regions(cluster_ui.load_cdk_context()) 

61 ) 

62 status = _gitops_status(regions=regions, project_name=config.project_name) 

63 except (RuntimeError, ValueError) as exc: 

64 formatter.print_error(f"Failed to read helm.argocd from cdk.json: {exc}") 

65 sys.exit(1) 

66 formatter.print(status) 

67 if not status["enabled"]: 

68 formatter.print_info( 

69 "Argo CD is off (cdk.json helm.argocd.enabled); enable it and run " 

70 "'gco stacks deploy' (or deploy once with '--enable argocd')." 

71 ) 

72 

73 

74@gitops.command("open") 

75@cluster_ui.tunnel_options 

76@click.option("--local-port", type=int, help="Local port to bind (default 8080).", default=None) 

77@pass_config 

78def gitops_open( 

79 config: Any, 

80 region: str | None, 

81 via_ssm: str | None, 

82 bastion_ttl_minutes: int, 

83 assume_yes: bool, 

84 local_port: int | None, 

85) -> None: 

86 """Port-forward to the Argo CD UI over the private EKS endpoint. 

87 

88 Runs in the foreground; press Ctrl-C to stop. Sign in as admin with the 

89 password from 'gco gitops password'. 

90 

91 Examples: 

92 gco gitops open --via-ssm auto -y 

93 gco gitops open -r us-west-2 --local-port 18080 --via-ssm i-0123456789abcdef0 

94 """ 

95 from ..cluster_tunnel import open_api_server_tunnel, resolve_region 

96 from ..gitops import DEFAULT_LOCAL_PORT, PORT_FORWARD_TARGET 

97 from ..kubectl_helpers import build_port_forward_command, update_kubeconfig 

98 

99 formatter = get_output_formatter(config) 

100 target_region = resolve_region(config, region) 

101 cluster = f"{config.project_name}-{target_region}" 

102 bind_port = local_port or DEFAULT_LOCAL_PORT 

103 namespace, target, remote_port = PORT_FORWARD_TARGET 

104 try: 

105 update_kubeconfig(cluster, target_region) 

106 with open_api_server_tunnel( 

107 formatter, 

108 cluster=cluster, 

109 region=target_region, 

110 via_ssm=via_ssm, 

111 bastion_ttl_minutes=bastion_ttl_minutes, 

112 assume_yes=assume_yes, 

113 ) as session: 

114 cmd = build_port_forward_command( 

115 namespace, 

116 target, 

117 bind_port, 

118 remote_port, 

119 server=session.server, 

120 tls_server_name=session.tls_server_name, 

121 ) 

122 formatter.print_info( 

123 f"Starting kubectl port-forward for Argo CD on localhost:{bind_port}..." 

124 ) 

125 

126 def _announce() -> None: 

127 formatter.print_success( 

128 f"Forwarding Argo CD → http://localhost:{bind_port} (Ctrl-C to stop)" 

129 ) 

130 formatter.print_info( 

131 "Sign in as admin; 'gco gitops password' prints the generated password." 

132 ) 

133 

134 try: 

135 cluster_ui.exec_port_forward(cmd, bind_port, on_ready=_announce) 

136 except KeyboardInterrupt: # pragma: no cover - interactive Ctrl-C 

137 return 

138 except (RuntimeError, ValueError) as exc: 

139 formatter.print_error(str(exc)) 

140 sys.exit(1) 

141 

142 

143@gitops.command("password") 

144@cluster_ui.tunnel_options 

145@pass_config 

146def gitops_password( 

147 config: Any, 

148 region: str | None, 

149 via_ssm: str | None, 

150 bastion_ttl_minutes: int, 

151 assume_yes: bool, 

152) -> None: 

153 """Print the generated Argo CD admin password (argocd-initial-admin-secret). 

154 

155 Argo CD writes it on first start. Once you change the admin password, 

156 delete that Secret; this command then says so. 

157 

158 Examples: 

159 gco gitops password --via-ssm auto -y 

160 """ 

161 from ..cluster_tunnel import open_api_server_tunnel, resolve_region 

162 from ..gitops import read_admin_password 

163 from ..kubectl_helpers import update_kubeconfig 

164 

165 formatter = get_output_formatter(config) 

166 target_region = resolve_region(config, region) 

167 cluster = f"{config.project_name}-{target_region}" 

168 try: 

169 update_kubeconfig(cluster, target_region) 

170 with open_api_server_tunnel( 

171 formatter, 

172 cluster=cluster, 

173 region=target_region, 

174 via_ssm=via_ssm, 

175 bastion_ttl_minutes=bastion_ttl_minutes, 

176 assume_yes=assume_yes, 

177 ) as session: 

178 password = read_admin_password( 

179 server=session.server, tls_server_name=session.tls_server_name 

180 ) 

181 except (RuntimeError, ValueError) as exc: 

182 formatter.print_error(str(exc)) 

183 sys.exit(1) 

184 formatter.print({"region": target_region, "username": "admin", "password": password}) 

185 

186 

187@gitops.command("screenshot") 

188@cluster_ui.tunnel_options 

189@click.option( 

190 "--output", 

191 "-o", 

192 "output_path", 

193 type=click.Path(dir_okay=False, path_type=Path), 

194 default=None, 

195 help="PNG to write (default: ./argocd-ui.png).", 

196) 

197@click.option("--local-port", type=int, help="Local port to bind (default 8080).", default=None) 

198@click.option( 

199 "--password", 

200 envvar="GCO_ARGOCD_ADMIN_PASSWORD", 

201 help=( 

202 "Admin password (also $GCO_ARGOCD_ADMIN_PASSWORD; default: read from " 

203 "argocd-initial-admin-secret)." 

204 ), 

205) 

206@click.option("--headed", is_flag=True, help="Show the browser window while capturing.") 

207@pass_config 

208def gitops_screenshot( 

209 config: Any, 

210 region: str | None, 

211 via_ssm: str | None, 

212 bastion_ttl_minutes: int, 

213 assume_yes: bool, 

214 output_path: Path | None, 

215 local_port: int | None, 

216 password: str | None, 

217 headed: bool, 

218) -> None: 

219 """Capture the Argo CD Applications view as a full-page PNG. 

220 

221 Forwards the UI in the background, logs in to the Argo CD API as admin and 

222 screenshots /applications with the session cookie — no browser sign-in. 

223 Needs Playwright: pip install 'gco[diagrams]' && playwright install chromium. 

224 

225 Examples: 

226 gco gitops screenshot --via-ssm auto -y 

227 gco gitops screenshot --output images/argocd-ui.png 

228 """ 

229 from ..cluster_tunnel import open_api_server_tunnel, resolve_region 

230 from ..cluster_ui import background_port_forward 

231 from ..gitops import ( 

232 DEFAULT_LOCAL_PORT, 

233 DEFAULT_SCREENSHOT_FILENAME, 

234 PORT_FORWARD_TARGET, 

235 capture_argocd_screenshot, 

236 create_session_token, 

237 read_admin_password, 

238 ) 

239 from ..kubectl_helpers import build_port_forward_command, update_kubeconfig 

240 

241 formatter = get_output_formatter(config) 

242 target_region = resolve_region(config, region) 

243 cluster = f"{config.project_name}-{target_region}" 

244 bind_port = local_port or DEFAULT_LOCAL_PORT 

245 output = output_path or Path.cwd() / DEFAULT_SCREENSHOT_FILENAME 

246 namespace, target, remote_port = PORT_FORWARD_TARGET 

247 base_url = f"http://localhost:{bind_port}" 

248 try: 

249 update_kubeconfig(cluster, target_region) 

250 with open_api_server_tunnel( 

251 formatter, 

252 cluster=cluster, 

253 region=target_region, 

254 via_ssm=via_ssm, 

255 bastion_ttl_minutes=bastion_ttl_minutes, 

256 assume_yes=assume_yes, 

257 ) as session: 

258 admin_password = password or read_admin_password( 

259 server=session.server, tls_server_name=session.tls_server_name 

260 ) 

261 cmd = build_port_forward_command( 

262 namespace, 

263 target, 

264 bind_port, 

265 remote_port, 

266 server=session.server, 

267 tls_server_name=session.tls_server_name, 

268 ) 

269 with background_port_forward(cmd, bind_port): 

270 token = create_session_token(base_url, admin_password) 

271 written = capture_argocd_screenshot( 

272 base_url, output, token=token, headless=not headed 

273 ) 

274 except (RuntimeError, ValueError) as exc: 

275 formatter.print_error(str(exc)) 

276 sys.exit(1) 

277 formatter.print_success(f"Saved the Argo CD Applications view to {written}") 

278 formatter.print({"region": target_region, "output": str(written)})