feat: add Go SSH config management functions and type

7 funciones Go del dominio infra para gestion programatica de ~/.ssh/config:
ssh_config_parse (parser de bloques Host/Match), ssh_config_read (lectura del
archivo), ssh_config_find (busqueda por host), ssh_config_add_entry y
ssh_config_remove_entry (CRUD), ssh_config_render (serializacion a texto),
ssh_config_write (escritura atomica). Incluye tipo SshConfigEntry (product type)
y tests unitarios del parser.
This commit is contained in:
2026-04-12 13:54:43 +02:00
parent 773bb3a523
commit f2753e6fff
17 changed files with 834 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
package infra
import "fmt"
// SSHConfigAddEntry añade un nuevo entry a la lista. Retorna error si el alias
// ya existe. No muta el slice original.
func SSHConfigAddEntry(entries []SSHConfigEntry, entry SSHConfigEntry) ([]SSHConfigEntry, error) {
for _, e := range entries {
if e.Alias == entry.Alias {
return nil, fmt.Errorf("alias %q already exists", entry.Alias)
}
}
result := make([]SSHConfigEntry, len(entries), len(entries)+1)
copy(result, entries)
return append(result, entry), nil
}
+38
View File
@@ -0,0 +1,38 @@
---
name: ssh_config_add_entry
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: pure
signature: "func SSHConfigAddEntry(entries []SSHConfigEntry, entry SSHConfigEntry) ([]SSHConfigEntry, error)"
description: "Añade un nuevo SSHConfigEntry a la lista. Error si el alias ya existe."
tags: [ssh, config, add, remote]
uses_functions: []
uses_types: [ssh_config_entry_go_infra]
returns: []
returns_optional: false
error_type: ""
imports: [fmt]
params:
- name: entries
desc: "lista actual de SSHConfigEntry"
- name: entry
desc: "nuevo entry a añadir con alias unico"
output: "nueva lista con el entry añadido al final"
tested: true
tests: ["añade entry a lista vacia", "añade entry a lista existente", "error si alias duplicado"]
test_file_path: "functions/infra/ssh_config_parse_test.go"
file_path: "functions/infra/ssh_config_add_entry.go"
---
## Ejemplo
```go
newEntry := SSHConfigEntry{Alias: "staging", HostName: "10.0.0.2", User: "deploy"}
updated, err := SSHConfigAddEntry(entries, newEntry)
```
## Notas
No muta el slice original — crea una copia nueva. La validacion de unicidad es por alias exacto.
+21
View File
@@ -0,0 +1,21 @@
package infra
// SSHConfigEntry representa un bloque Host en ~/.ssh/config.
type SSHConfigEntry struct {
Alias string // Nombre del host (lo que va despues de "Host")
HostName string // IP o hostname real del servidor
User string // Usuario remoto
Port int // Puerto SSH (0 = no especificado, usa default 22)
IdentityFile string // Ruta a clave privada
Options map[string]string // Opciones SSH adicionales (ForwardAgent, ProxyJump, etc.)
}
// ToSSHConn convierte un SSHConfigEntry a SSHConn para usar con las funciones SSH del registry.
func (e SSHConfigEntry) ToSSHConn() SSHConn {
return SSHConn{
Host: e.HostName,
Port: e.Port,
User: e.User,
KeyPath: e.IdentityFile,
}
}
+12
View File
@@ -0,0 +1,12 @@
package infra
// SSHConfigFind busca un entry por alias en la lista. Retorna el entry y true
// si lo encuentra, o un SSHConfigEntry vacio y false si no existe.
func SSHConfigFind(entries []SSHConfigEntry, alias string) (SSHConfigEntry, bool) {
for _, e := range entries {
if e.Alias == alias {
return e, true
}
}
return SSHConfigEntry{}, false
}
+41
View File
@@ -0,0 +1,41 @@
---
name: ssh_config_find
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: pure
signature: "func SSHConfigFind(entries []SSHConfigEntry, alias string) (SSHConfigEntry, bool)"
description: "Busca un entry por alias en la lista de SSHConfigEntry."
tags: [ssh, config, search, remote]
uses_functions: []
uses_types: [ssh_config_entry_go_infra]
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: entries
desc: "lista de SSHConfigEntry donde buscar"
- name: alias
desc: "nombre del host a buscar (case-sensitive)"
output: "el SSHConfigEntry encontrado y true, o entry vacio y false si no existe"
tested: true
tests: ["encuentra entry existente", "retorna false para alias inexistente"]
test_file_path: "functions/infra/ssh_config_parse_test.go"
file_path: "functions/infra/ssh_config_find.go"
---
## Ejemplo
```go
entry, ok := SSHConfigFind(entries, "myserver")
if ok {
conn := entry.ToSSHConn()
SSHCheck(conn)
}
```
## Notas
Busqueda lineal por coincidencia exacta del alias. Para pocos entries (tipico en un SSH config personal) es suficiente.
+84
View File
@@ -0,0 +1,84 @@
package infra
import (
"bufio"
"strconv"
"strings"
)
// SSHConfigParse parsea el contenido de un archivo ~/.ssh/config y retorna
// una lista de SSHConfigEntry. Ignora comentarios y lineas vacias.
// Los bloques Host con wildcards (* o ?) se ignoran.
func SSHConfigParse(content string) []SSHConfigEntry {
var entries []SSHConfigEntry
var current *SSHConfigEntry
scanner := bufio.NewScanner(strings.NewReader(content))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value := splitDirective(line)
if key == "" {
continue
}
if strings.EqualFold(key, "Host") {
if current != nil && current.Alias != "" {
entries = append(entries, *current)
}
if strings.ContainsAny(value, "*?") {
current = nil
continue
}
current = &SSHConfigEntry{
Alias: value,
Options: make(map[string]string),
}
continue
}
if current == nil {
continue
}
switch strings.ToLower(key) {
case "hostname":
current.HostName = value
case "user":
current.User = value
case "port":
if p, err := strconv.Atoi(value); err == nil {
current.Port = p
}
case "identityfile":
current.IdentityFile = value
default:
current.Options[key] = value
}
}
if current != nil && current.Alias != "" {
entries = append(entries, *current)
}
return entries
}
// splitDirective separa una linea "Key Value" o "Key=Value" en clave y valor.
func splitDirective(line string) (string, string) {
// Intentar separar por =
if idx := strings.IndexByte(line, '='); idx >= 0 {
return strings.TrimSpace(line[:idx]), strings.TrimSpace(line[idx+1:])
}
// Separar por primer espacio/tab
fields := strings.SplitN(line, " ", 2)
if len(fields) < 2 {
fields = strings.SplitN(line, "\t", 2)
}
if len(fields) < 2 {
return "", ""
}
return strings.TrimSpace(fields[0]), strings.TrimSpace(fields[1])
}
+43
View File
@@ -0,0 +1,43 @@
---
name: ssh_config_parse
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: pure
signature: "func SSHConfigParse(content string) []SSHConfigEntry"
description: "Parsea el contenido de un archivo ~/.ssh/config y retorna una lista de SSHConfigEntry."
tags: [ssh, config, parser, remote]
uses_functions: []
uses_types: [ssh_config_entry_go_infra]
returns: []
returns_optional: false
error_type: ""
imports: [bufio, strconv, strings]
params:
- name: content
desc: "texto completo del archivo ~/.ssh/config"
output: "lista de SSHConfigEntry, uno por cada bloque Host (sin wildcards)"
tested: true
tests: ["parsea config con multiples hosts", "ignora comentarios y lineas vacias", "ignora hosts con wildcards", "parsea opciones extra en Options map"]
test_file_path: "functions/infra/ssh_config_parse_test.go"
file_path: "functions/infra/ssh_config_parse.go"
---
## Ejemplo
```go
content := `Host myserver
HostName 192.168.1.100
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519`
entries := SSHConfigParse(content)
// entries[0].Alias == "myserver"
// entries[0].HostName == "192.168.1.100"
```
## Notas
Ignora bloques `Host *` y `Host 192.168.?.*` (wildcards). Soporta directivas con separador espacio o `=`. Las directivas no reconocidas se almacenan en el map `Options`.
+279
View File
@@ -0,0 +1,279 @@
package infra
import (
"strings"
"testing"
)
func TestSSHConfigParse_MultipleHosts(t *testing.T) {
content := `
Host prod
HostName 10.0.0.1
User admin
Port 22
IdentityFile ~/.ssh/id_prod
Host staging
HostName 10.0.0.2
User deploy
Port 2222
`
entries := SSHConfigParse(content)
if len(entries) != 2 {
t.Fatalf("expected 2 entries, got %d", len(entries))
}
if entries[0].Alias != "prod" {
t.Errorf("expected alias prod, got %s", entries[0].Alias)
}
if entries[0].HostName != "10.0.0.1" {
t.Errorf("expected hostname 10.0.0.1, got %s", entries[0].HostName)
}
if entries[0].User != "admin" {
t.Errorf("expected user admin, got %s", entries[0].User)
}
if entries[0].Port != 22 {
t.Errorf("expected port 22, got %d", entries[0].Port)
}
if entries[0].IdentityFile != "~/.ssh/id_prod" {
t.Errorf("expected identity file ~/.ssh/id_prod, got %s", entries[0].IdentityFile)
}
if entries[1].Alias != "staging" {
t.Errorf("expected alias staging, got %s", entries[1].Alias)
}
if entries[1].Port != 2222 {
t.Errorf("expected port 2222, got %d", entries[1].Port)
}
}
func TestSSHConfigParse_IgnoresCommentsAndBlanks(t *testing.T) {
content := `
# This is a comment
Host myserver
HostName 192.168.1.1
# inline comment
User root
`
entries := SSHConfigParse(content)
if len(entries) != 1 {
t.Fatalf("expected 1 entry, got %d", len(entries))
}
if entries[0].User != "root" {
t.Errorf("expected user root, got %s", entries[0].User)
}
}
func TestSSHConfigParse_IgnoresWildcards(t *testing.T) {
content := `
Host *
ServerAliveInterval 60
Host prod
HostName 10.0.0.1
Host 192.168.*
User local
`
entries := SSHConfigParse(content)
if len(entries) != 1 {
t.Fatalf("expected 1 entry (wildcards ignored), got %d", len(entries))
}
if entries[0].Alias != "prod" {
t.Errorf("expected alias prod, got %s", entries[0].Alias)
}
}
func TestSSHConfigParse_ExtraOptions(t *testing.T) {
content := `Host jump
HostName bastion.example.com
User ops
ForwardAgent yes
ProxyJump none
`
entries := SSHConfigParse(content)
if len(entries) != 1 {
t.Fatalf("expected 1 entry, got %d", len(entries))
}
if entries[0].Options["ForwardAgent"] != "yes" {
t.Errorf("expected ForwardAgent=yes, got %s", entries[0].Options["ForwardAgent"])
}
if entries[0].Options["ProxyJump"] != "none" {
t.Errorf("expected ProxyJump=none, got %s", entries[0].Options["ProxyJump"])
}
}
func TestSSHConfigRender_FullEntry(t *testing.T) {
entries := []SSHConfigEntry{{
Alias: "prod",
HostName: "10.0.0.1",
User: "admin",
Port: 2222,
IdentityFile: "~/.ssh/id_prod",
}}
result := SSHConfigRender(entries)
if !strings.Contains(result, "Host prod") {
t.Error("missing Host directive")
}
if !strings.Contains(result, "HostName 10.0.0.1") {
t.Error("missing HostName")
}
if !strings.Contains(result, "User admin") {
t.Error("missing User")
}
if !strings.Contains(result, "Port 2222") {
t.Error("missing Port")
}
if !strings.Contains(result, "IdentityFile ~/.ssh/id_prod") {
t.Error("missing IdentityFile")
}
}
func TestSSHConfigRender_MinimalEntry(t *testing.T) {
entries := []SSHConfigEntry{{Alias: "local"}}
result := SSHConfigRender(entries)
if result != "Host local\n" {
t.Errorf("expected minimal render, got %q", result)
}
}
func TestSSHConfigRender_MultipleEntries(t *testing.T) {
entries := []SSHConfigEntry{
{Alias: "a", HostName: "1.1.1.1"},
{Alias: "b", HostName: "2.2.2.2"},
}
result := SSHConfigRender(entries)
parts := strings.Split(result, "\n\n")
if len(parts) < 2 {
t.Error("expected blocks separated by blank line")
}
}
func TestSSHConfigRender_OptionsSorted(t *testing.T) {
entries := []SSHConfigEntry{{
Alias: "test",
HostName: "1.1.1.1",
Options: map[string]string{"ProxyJump": "bastion", "ForwardAgent": "yes"},
}}
result := SSHConfigRender(entries)
fwIdx := strings.Index(result, "ForwardAgent")
pjIdx := strings.Index(result, "ProxyJump")
if fwIdx < 0 || pjIdx < 0 {
t.Fatal("missing options in render")
}
if fwIdx > pjIdx {
t.Error("options should be sorted alphabetically")
}
}
func TestSSHConfigFind_Exists(t *testing.T) {
entries := []SSHConfigEntry{
{Alias: "prod", HostName: "10.0.0.1"},
{Alias: "staging", HostName: "10.0.0.2"},
}
entry, ok := SSHConfigFind(entries, "staging")
if !ok {
t.Fatal("expected to find staging")
}
if entry.HostName != "10.0.0.2" {
t.Errorf("expected hostname 10.0.0.2, got %s", entry.HostName)
}
}
func TestSSHConfigFind_NotExists(t *testing.T) {
entries := []SSHConfigEntry{{Alias: "prod"}}
_, ok := SSHConfigFind(entries, "nope")
if ok {
t.Error("expected not found")
}
}
func TestSSHConfigAddEntry_ToEmpty(t *testing.T) {
entry := SSHConfigEntry{Alias: "new", HostName: "1.1.1.1"}
result, err := SSHConfigAddEntry(nil, entry)
if err != nil {
t.Fatal(err)
}
if len(result) != 1 || result[0].Alias != "new" {
t.Errorf("unexpected result: %+v", result)
}
}
func TestSSHConfigAddEntry_ToExisting(t *testing.T) {
existing := []SSHConfigEntry{{Alias: "old"}}
entry := SSHConfigEntry{Alias: "new"}
result, err := SSHConfigAddEntry(existing, entry)
if err != nil {
t.Fatal(err)
}
if len(result) != 2 {
t.Fatalf("expected 2 entries, got %d", len(result))
}
// Original no mutado
if len(existing) != 1 {
t.Error("original slice was mutated")
}
}
func TestSSHConfigAddEntry_DuplicateAlias(t *testing.T) {
existing := []SSHConfigEntry{{Alias: "prod"}}
entry := SSHConfigEntry{Alias: "prod"}
_, err := SSHConfigAddEntry(existing, entry)
if err == nil {
t.Error("expected error for duplicate alias")
}
}
func TestSSHConfigRemoveEntry_Exists(t *testing.T) {
entries := []SSHConfigEntry{
{Alias: "a"},
{Alias: "b"},
{Alias: "c"},
}
result, err := SSHConfigRemoveEntry(entries, "b")
if err != nil {
t.Fatal(err)
}
if len(result) != 2 {
t.Fatalf("expected 2 entries, got %d", len(result))
}
if result[0].Alias != "a" || result[1].Alias != "c" {
t.Errorf("unexpected order: %+v", result)
}
// Original no mutado
if len(entries) != 3 {
t.Error("original slice was mutated")
}
}
func TestSSHConfigRemoveEntry_NotExists(t *testing.T) {
entries := []SSHConfigEntry{{Alias: "prod"}}
_, err := SSHConfigRemoveEntry(entries, "nope")
if err == nil {
t.Error("expected error for missing alias")
}
}
func TestSSHConfigParseRender_Roundtrip(t *testing.T) {
original := []SSHConfigEntry{
{Alias: "prod", HostName: "10.0.0.1", User: "admin", Port: 22, IdentityFile: "~/.ssh/id_prod"},
{Alias: "staging", HostName: "10.0.0.2", User: "deploy", Port: 2222},
}
text := SSHConfigRender(original)
parsed := SSHConfigParse(text)
if len(parsed) != len(original) {
t.Fatalf("roundtrip: expected %d entries, got %d", len(original), len(parsed))
}
for i := range original {
if parsed[i].Alias != original[i].Alias {
t.Errorf("roundtrip[%d]: alias mismatch %s != %s", i, parsed[i].Alias, original[i].Alias)
}
if parsed[i].HostName != original[i].HostName {
t.Errorf("roundtrip[%d]: hostname mismatch", i)
}
if parsed[i].User != original[i].User {
t.Errorf("roundtrip[%d]: user mismatch", i)
}
if parsed[i].Port != original[i].Port {
t.Errorf("roundtrip[%d]: port mismatch", i)
}
}
}
+21
View File
@@ -0,0 +1,21 @@
package infra
import (
"fmt"
"os"
"path/filepath"
)
// SSHConfigRead lee y parsea el archivo ~/.ssh/config. Si el archivo no existe,
// retorna una lista vacia sin error (config todavia no creado).
func SSHConfigRead() ([]SSHConfigEntry, error) {
path := filepath.Join(os.Getenv("HOME"), ".ssh", "config")
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("ssh config read: %w", err)
}
return SSHConfigParse(string(data)), nil
}
+39
View File
@@ -0,0 +1,39 @@
---
name: ssh_config_read
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func SSHConfigRead() ([]SSHConfigEntry, error)"
description: "Lee y parsea ~/.ssh/config. Retorna lista vacia si el archivo no existe."
tags: [ssh, config, read, remote]
uses_functions: [ssh_config_parse_go_infra]
uses_types: [ssh_config_entry_go_infra]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [fmt, os, path/filepath]
params: []
output: "lista de SSHConfigEntry parseados del archivo ~/.ssh/config"
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/ssh_config_read.go"
---
## Ejemplo
```go
entries, err := SSHConfigRead()
if err != nil {
log.Fatal(err)
}
for _, e := range entries {
fmt.Printf("%s -> %s@%s\n", e.Alias, e.User, e.HostName)
}
```
## Notas
Si `~/.ssh/config` no existe, retorna lista vacia y nil (no es error — el usuario simplemente no tiene config aun). Usa `SSHConfigParse` internamente.
@@ -0,0 +1,22 @@
package infra
import "fmt"
// SSHConfigRemoveEntry elimina el entry con el alias dado. Retorna error si
// el alias no existe. No muta el slice original.
func SSHConfigRemoveEntry(entries []SSHConfigEntry, alias string) ([]SSHConfigEntry, error) {
idx := -1
for i, e := range entries {
if e.Alias == alias {
idx = i
break
}
}
if idx < 0 {
return nil, fmt.Errorf("alias %q not found", alias)
}
result := make([]SSHConfigEntry, 0, len(entries)-1)
result = append(result, entries[:idx]...)
result = append(result, entries[idx+1:]...)
return result, nil
}
@@ -0,0 +1,37 @@
---
name: ssh_config_remove_entry
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: pure
signature: "func SSHConfigRemoveEntry(entries []SSHConfigEntry, alias string) ([]SSHConfigEntry, error)"
description: "Elimina un entry por alias de la lista. Error si el alias no existe."
tags: [ssh, config, remove, remote]
uses_functions: []
uses_types: [ssh_config_entry_go_infra]
returns: []
returns_optional: false
error_type: ""
imports: [fmt]
params:
- name: entries
desc: "lista actual de SSHConfigEntry"
- name: alias
desc: "alias del host a eliminar"
output: "nueva lista sin el entry eliminado"
tested: true
tests: ["elimina entry existente", "error si alias no existe"]
test_file_path: "functions/infra/ssh_config_parse_test.go"
file_path: "functions/infra/ssh_config_remove_entry.go"
---
## Ejemplo
```go
updated, err := SSHConfigRemoveEntry(entries, "old-server")
```
## Notas
No muta el slice original — crea una copia nueva sin el entry eliminado.
+42
View File
@@ -0,0 +1,42 @@
package infra
import (
"fmt"
"sort"
"strings"
)
// SSHConfigRender convierte una lista de SSHConfigEntry al formato texto
// de ~/.ssh/config. Cada bloque se separa con una linea en blanco.
func SSHConfigRender(entries []SSHConfigEntry) string {
var sb strings.Builder
for i, e := range entries {
if i > 0 {
sb.WriteString("\n")
}
sb.WriteString(fmt.Sprintf("Host %s\n", e.Alias))
if e.HostName != "" {
sb.WriteString(fmt.Sprintf(" HostName %s\n", e.HostName))
}
if e.User != "" {
sb.WriteString(fmt.Sprintf(" User %s\n", e.User))
}
if e.Port != 0 {
sb.WriteString(fmt.Sprintf(" Port %d\n", e.Port))
}
if e.IdentityFile != "" {
sb.WriteString(fmt.Sprintf(" IdentityFile %s\n", e.IdentityFile))
}
if len(e.Options) > 0 {
keys := make([]string, 0, len(e.Options))
for k := range e.Options {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
sb.WriteString(fmt.Sprintf(" %s %s\n", k, e.Options[k]))
}
}
}
return sb.String()
}
+39
View File
@@ -0,0 +1,39 @@
---
name: ssh_config_render
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: pure
signature: "func SSHConfigRender(entries []SSHConfigEntry) string"
description: "Convierte una lista de SSHConfigEntry al formato texto de ~/.ssh/config."
tags: [ssh, config, render, remote]
uses_functions: []
uses_types: [ssh_config_entry_go_infra]
returns: []
returns_optional: false
error_type: ""
imports: [fmt, sort, strings]
params:
- name: entries
desc: "lista de SSHConfigEntry a renderizar"
output: "texto en formato ~/.ssh/config con bloques Host separados por linea en blanco"
tested: true
tests: ["renderiza entry completo", "renderiza entry minimo solo con alias", "separa bloques con linea en blanco", "ordena opciones extra alfabeticamente"]
test_file_path: "functions/infra/ssh_config_parse_test.go"
file_path: "functions/infra/ssh_config_render.go"
---
## Ejemplo
```go
entries := []SSHConfigEntry{{
Alias: "prod", HostName: "10.0.0.1", User: "admin", Port: 22,
}}
text := SSHConfigRender(entries)
// "Host prod\n HostName 10.0.0.1\n User admin\n Port 22\n"
```
## Notas
Campos vacios o Port=0 se omiten. Las opciones extra se renderizan en orden alfabetico para determinismo.
+33
View File
@@ -0,0 +1,33 @@
package infra
import (
"fmt"
"os"
"path/filepath"
)
// SSHConfigWrite escribe la lista de entries al archivo ~/.ssh/config.
// Crea un backup (.config.bak) antes de sobrescribir si el archivo ya existe.
// Crea el directorio ~/.ssh si no existe.
func SSHConfigWrite(entries []SSHConfigEntry) error {
sshDir := filepath.Join(os.Getenv("HOME"), ".ssh")
configPath := filepath.Join(sshDir, "config")
backupPath := filepath.Join(sshDir, "config.bak")
if err := os.MkdirAll(sshDir, 0700); err != nil {
return fmt.Errorf("ssh config write: create dir: %w", err)
}
// Backup si existe
if data, err := os.ReadFile(configPath); err == nil {
if err := os.WriteFile(backupPath, data, 0600); err != nil {
return fmt.Errorf("ssh config write: backup: %w", err)
}
}
content := SSHConfigRender(entries)
if err := os.WriteFile(configPath, []byte(content), 0600); err != nil {
return fmt.Errorf("ssh config write: %w", err)
}
return nil
}
+41
View File
@@ -0,0 +1,41 @@
---
name: ssh_config_write
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func SSHConfigWrite(entries []SSHConfigEntry) error"
description: "Escribe entries a ~/.ssh/config con backup automatico del archivo previo."
tags: [ssh, config, write, remote]
uses_functions: [ssh_config_render_go_infra]
uses_types: [ssh_config_entry_go_infra]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [fmt, os, path/filepath]
params:
- name: entries
desc: "lista de SSHConfigEntry a escribir en ~/.ssh/config"
output: "nil si la escritura fue exitosa"
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/ssh_config_write.go"
---
## Ejemplo
```go
entries, _ := SSHConfigRead()
entries, _ = SSHConfigAddEntry(entries, SSHConfigEntry{
Alias: "prod", HostName: "10.0.0.1", User: "deploy",
})
if err := SSHConfigWrite(entries); err != nil {
log.Fatal(err)
}
```
## Notas
Crea `~/.ssh/` con permisos 0700 si no existe. Hace backup a `~/.ssh/config.bak` antes de sobrescribir. El archivo resultante tiene permisos 0600 (solo lectura/escritura para el owner).
+26
View File
@@ -0,0 +1,26 @@
---
name: ssh_config_entry
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
type SSHConfigEntry struct {
Alias string
HostName string
User string
Port int
IdentityFile string
Options map[string]string
}
description: "Bloque Host de ~/.ssh/config. Contiene alias, hostname, usuario, puerto, clave y opciones extra."
tags: [ssh, config, remote, infra]
uses_types: []
file_path: "functions/infra/ssh_config_entry.go"
---
## Notas
Tipo producto — modela un bloque `Host` del archivo `~/.ssh/config`. Port=0 significa que no se especifica (el SSH client usa 22 por defecto). Options contiene pares clave-valor para directivas SSH adicionales como ForwardAgent, ProxyJump, ServerAliveInterval, etc.
Incluye metodo `ToSSHConn()` para convertir a `SSHConn` y poder usar las funciones SSH operativas del registry (ssh_exec, ssh_check, etc.).