feat: Terraform resource reference from provider schema, provider alias fixes, and timestamps

- Add gen_tf_reference.py to generate OCI Terraform provider resource catalog (~937 resources) from `terraform providers schema -json`
- Auto-inject categorized resource reference into Terraform Agent system prompt to prevent invalid resource type names
- Build-time reference generation in Dockerfile, refreshable via POST /api/terraform/refresh-reference
- Provider alias detection with brace-matching parser for multi-region deployments
- Clean old .tf files before writing new ones on Re-plan
- Message timestamps in terraform chat and history
- Partial DOM rendering to eliminate screen flickering
- Layout overflow fix (no scroll beyond viewport)
This commit is contained in:
nogueiraguh
2026-03-08 01:00:47 -03:00
parent 8bccf9367e
commit 19e481275d
5 changed files with 327 additions and 28 deletions

View File

@@ -1955,6 +1955,14 @@ TF_DEFAULT_SYSTEM_PROMPT = """Você é um agente especializado EXCLUSIVAMENTE em
- Cada bloco `hcl` vira um arquivo separado no sistema.
- Se o usuário NÃO especificar, gere tudo em um único bloco.
### REGRA CRÍTICA: Unicidade de Recursos
- **NUNCA** declare o mesmo recurso (mesmo tipo + mesmo nome) em mais de um arquivo.
- Cada `resource "tipo" "nome"` deve existir em EXATAMENTE UM arquivo.
- Se um recurso precisa de atributos definidos em outro arquivo (ex: route_table_id, security_list_ids), defina TODOS os atributos na declaração ÚNICA do recurso, referenciando recursos de outros arquivos.
- Exemplo ERRADO: declarar `resource "oci_core_subnet" "app"` em `networking.tf` e redeclarar em `routing.tf`.
- Exemplo CORRETO: declarar a subnet UMA VEZ em `networking.tf` com `route_table_id = oci_core_route_table.app_rt.id` referenciando a route table de `routing.tf`.
- Antes de gerar o código, planeje quais recursos vão em cada arquivo para evitar duplicações.
### Contexto OCI
- Use as informações de tenancy, região e compartment fornecidas no contexto.
- Use `var.compartment_id` como default para compartment_id nos recursos.
@@ -2000,8 +2008,68 @@ TF_DEFAULT_SYSTEM_PROMPT = """Você é um agente especializado EXCLUSIVAMENTE em
- Se o pedido for ambíguo, faça perguntas de esclarecimento.
- NÃO sugira rodar comandos terraform — o sistema cuida disso.
- Inclua outputs úteis (IPs, OCIDs dos recursos criados).
- SEMPRE declare `variable "region"` no variables.tf (o sistema injeta via terraform.tfvars).
### Nomes CORRETOS de recursos OCI (ERROS COMUNS)
Abaixo estão os nomes corretos de resource types do provider oracle/oci. O modelo DEVE usar exatamente estes nomes:
- **Network Firewall**:
- `oci_network_firewall_network_firewall` (NÃO `oci_network_firewall`)
- `oci_network_firewall_network_firewall_policy` (NÃO `oci_network_firewall_policy`)
- **DRG Attachment Management**:
- `oci_core_drg_attachment_management` (NÃO `oci_core_vcn_drg_attachment_management`)
- `oci_core_drg_attachment` — para criar attachments normais
- **DRG Route Distribution Statement** (`oci_core_drg_route_distribution_statement`):
- NÃO use `attachment_id` diretamente. Use o bloco `match_criteria`:
```hcl
match_criteria {
match_type = "DRG_ATTACHMENT_ID"
drg_attachment_id = oci_core_drg_attachment.example.id
}
```
- **Remote Peering Connection**: `oci_core_remote_peering_connection`
- **DRG Route Table**: `oci_core_drg_route_table`
- **DRG Route Distribution**: `oci_core_drg_route_distribution`
- **DRG Route Table Route Rule**: `oci_core_drg_route_table_route_rule`
"""
# ── Terraform OCI Resource Reference (generated at build time, updatable at runtime) ──
_TF_RESOURCE_REF_PATH = "/data/oci_tf_resource_reference.txt"
_tf_resource_ref_cache: dict = {"content": None, "mtime": 0}
def _load_tf_resource_reference() -> str:
"""Load the OCI Terraform resource reference file, cached by mtime."""
try:
p = Path(_TF_RESOURCE_REF_PATH)
if not p.exists():
return ""
mtime = p.stat().st_mtime
if _tf_resource_ref_cache["content"] and _tf_resource_ref_cache["mtime"] == mtime:
return _tf_resource_ref_cache["content"]
content = p.read_text()
_tf_resource_ref_cache["content"] = content
_tf_resource_ref_cache["mtime"] = mtime
log.info(f"Loaded TF resource reference: {len(content)} chars, {content.count(chr(10))} lines")
return content
except Exception as e:
log.warning(f"Failed to load TF resource reference: {e}")
return ""
def _regenerate_tf_reference() -> dict:
"""Regenerate the OCI Terraform resource reference file."""
try:
result = subprocess.run(
["python", "/app/gen_tf_reference.py"],
capture_output=True, text=True, timeout=300
)
_tf_resource_ref_cache["content"] = None # invalidate cache
if result.returncode == 0:
ref = _load_tf_resource_reference()
return {"ok": True, "output": result.stdout.strip(), "lines": ref.count('\n')}
return {"ok": False, "error": result.stderr.strip()}
except Exception as e:
return {"ok": False, "error": str(e)}
def _get_adb_connection(cfg: dict):
"""Create an oracledb connection from an adb_vector_configs row."""
import oracledb
@@ -3120,6 +3188,25 @@ async def _chat_background(mid: str, sid: str, msg: ChatMsg, user: dict, genai_c
except Exception as e:
log.warning(f"Failed to inject terraform resource context: {e}")
# ── Inject OCI Terraform resource reference for correct resource names ──
if agent_type == "terraform":
tf_ref = _load_tf_resource_reference()
if tf_ref:
# Extract only the categorized sections (not the full 900+ resource list)
# to keep prompt size manageable
sections = []
for line in tf_ref.split('\n'):
if line.startswith('## All Resource Types'):
break
sections.append(line)
ref_compact = '\n'.join(sections).strip()
if ref_compact:
cfg_dict["system_prompt"] = cfg_dict.get("system_prompt", "") + \
"\n\n### Referência de Recursos OCI Terraform (gerado do provider schema)\n" + \
"Use EXATAMENTE estes nomes de resource types. Se o recurso não estiver nesta lista, ele NÃO EXISTE no provider.\n\n" + \
ref_compact
log.info(f"Terraform: injected resource reference ({len(ref_compact)} chars)")
mcp_tools = []
tool_defs = None
if msg.use_tools:
@@ -3676,6 +3763,26 @@ async def tf_delete_workspace(wid: str, u=Depends(current_user)):
return {"ok": True}
@app.post("/api/terraform/refresh-reference")
async def tf_refresh_reference(u=Depends(current_user)):
"""Regenerate the OCI Terraform resource reference from provider schema."""
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(_chat_executor, _regenerate_tf_reference)
return result
@app.get("/api/terraform/reference-status")
async def tf_reference_status(u=Depends(current_user)):
"""Check if OCI Terraform resource reference is available."""
p = Path(_TF_RESOURCE_REF_PATH)
if p.exists():
content = _load_tf_resource_reference()
lines = content.count('\n') if content else 0
mtime = datetime.fromtimestamp(p.stat().st_mtime).isoformat()
return {"available": True, "lines": lines, "updated_at": mtime}
return {"available": False}
def _write_tf_files(wdir: Path, tf_code: str):
"""Parse tf_code for '// filename: xxx.tf' markers and write separate files."""
import re as _re
@@ -3709,11 +3816,81 @@ async def _terraform_exec(wid: str, action: str, user: dict):
wdir = TERRAFORM_DIR / wid
wdir.mkdir(parents=True, exist_ok=True)
# Clean old .tf files (keep .terraform/, state, lock)
for old_tf in wdir.glob("*.tf"):
old_tf.unlink()
# Write .tf files — parse // filename: comments to split into separate files
_write_tf_files(wdir, ws["tf_code"] or "")
# Auto-generate provider.tf from OCI config
# Detect provider aliases declared by the model in generated files
import re as _re2
oci_cfg = _get_oci_config(ws["oci_config_id"])
alias_blocks = [] # list of (alias_name, region_ref)
# Scan all .tf files for provider "oci" { alias = "..." ... region = ... }
provider_block_re = _re2.compile(r'provider\s+"oci"\s*\{', _re2.MULTILINE)
for tf_file in sorted(wdir.glob("*.tf")):
if tf_file.name == "provider.tf":
continue
content = tf_file.read_text()
# Find each provider "oci" block and extract alias + region
blocks_to_remove = []
for m in provider_block_re.finditer(content):
start = m.start()
# Find matching closing brace
depth = 0
end = start
for ci in range(m.end() - 1, len(content)):
if content[ci] == '{':
depth += 1
elif content[ci] == '}':
depth -= 1
if depth == 0:
end = ci + 1
break
block = content[start:end]
alias_m = _re2.search(r'alias\s*=\s*"([^"]+)"', block)
region_m = _re2.search(r'region\s*=\s*(.+)', block)
if alias_m:
alias_name = alias_m.group(1)
region_ref = region_m.group(1).strip().rstrip("}").strip() if region_m else '"unknown"'
alias_blocks.append((alias_name, region_ref))
blocks_to_remove.append((start, end))
# Remove model-generated provider blocks (we centralize in provider.tf)
if blocks_to_remove:
new_content = content
for s, e in reversed(blocks_to_remove):
new_content = new_content[:s] + new_content[e:]
# Also remove leading comments right before removed blocks
new_content = _re2.sub(r'\n(//[^\n]*\n){1,5}\s*\n', '\n\n', new_content)
new_content = new_content.strip()
if new_content:
tf_file.write_text(new_content + "\n")
else:
tf_file.unlink() # Remove empty file
# Also scan for provider refs in resource blocks (provider = oci.xxx)
for tf_file in sorted(wdir.glob("*.tf")):
if tf_file.name == "provider.tf":
continue
content = tf_file.read_text()
for ref_m in _re2.finditer(r'provider\s*=\s*oci\.(\w+)', content):
ref_alias = ref_m.group(1)
if ref_alias not in [a[0] for a in alias_blocks]:
alias_blocks.append((ref_alias, '"unknown"'))
cred_block = f''' tenancy_ocid = "{oci_cfg.get('tenancy', '')}"
user_ocid = "{oci_cfg.get('user', '')}"
fingerprint = "{oci_cfg.get('fingerprint', '')}"
private_key_path = "{oci_cfg.get('key_file', '')}"'''
# Use region from OCI config row (may differ from oci sdk config)
with db() as c:
oci_row = c.execute("SELECT region FROM oci_configs WHERE id=?", (ws["oci_config_id"],)).fetchone()
primary_region = oci_row["region"] if oci_row else oci_cfg.get("region", "")
provider_tf = f'''terraform {{
required_providers {{
oci = {{
@@ -3723,11 +3900,21 @@ async def _terraform_exec(wid: str, action: str, user: dict):
}}
provider "oci" {{
tenancy_ocid = "{oci_cfg.get('tenancy', '')}"
user_ocid = "{oci_cfg.get('user', '')}"
fingerprint = "{oci_cfg.get('fingerprint', '')}"
private_key_path = "{oci_cfg.get('key_file', '')}"
region = "{oci_cfg.get('region', '')}"
{cred_block}
region = "{primary_region}"
}}
'''
# Add alias providers with same credentials
seen_aliases = set()
for alias_name, region_ref in alias_blocks:
if alias_name in seen_aliases:
continue
seen_aliases.add(alias_name)
provider_tf += f'''
provider "oci" {{
alias = "{alias_name}"
{cred_block}
region = {region_ref}
}}
'''
(wdir / "provider.tf").write_text(provider_tf)