feat(motor): agregar version fijable en build y clusterlog bootstrap-instance
Primer paso hacia separar el motor (herramienta genérica) de cada instancia (contenido de un deployment): Version deja de ser const para poder inyectarse con -ldflags en `make build` (git describe, sin tags aún), y el comando nuevo bootstrap-instance genera el esqueleto mínimo de una instancia (zola.toml, content/<sección>/_index.md, admins.json vacío, .clusterlog-version) reusando saveAdmins() en vez de duplicar su formato. Documentado en CLI.md; el empaquetado de templates/tema y el CI de instancia quedan para cuando el motor publique releases. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -366,3 +366,42 @@ clusterlog --root . --json verify commit HEAD
|
|||||||
```
|
```
|
||||||
|
|
||||||
El agente debe detenerse ante cualquier código distinto de cero y presentar el error al usuario sin ocultarlo.
|
El agente debe detenerse ante cualquier código distinto de cero y presentar el error al usuario sin ocultarlo.
|
||||||
|
|
||||||
|
## 16. Versión
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clusterlog version
|
||||||
|
```
|
||||||
|
|
||||||
|
No requiere `--root` ni identidad. Devuelve `{"name":"clusterlog","version":"..."}`.
|
||||||
|
El valor de `version` se fija en tiempo de compilación:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -ldflags "-X example.org/bitacora-cluster/internal/clusterlog.Version=v0.2.0" ...
|
||||||
|
```
|
||||||
|
|
||||||
|
`make build` ya lo hace automáticamente a partir de `git describe --tags --always --dirty` (variable `CLUSTERLOG_VERSION` del `Makefile`); un build local sin tags produce algo como `abc1234-dirty`, no `dev` ni una cadena vacía.
|
||||||
|
|
||||||
|
## 17. Instancias nuevas
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clusterlog bootstrap-instance \
|
||||||
|
--output ./clusterlog-nuevo-deploy \
|
||||||
|
--product-name "Bitácora de Ejemplo" \
|
||||||
|
--base-url "https://clusterlog.ejemplo.org"
|
||||||
|
```
|
||||||
|
|
||||||
|
No requiere `--root`: `--output` es un directorio nuevo, normalmente fuera del repositorio actual. Tampoco usa `--commit` (no hay nada que firmar todavía). Genera:
|
||||||
|
|
||||||
|
- `zola.toml` con `base_url`/`title`/`[extra] product_name` sustituidos y el resto de los valores genéricos del motor;
|
||||||
|
- `content/<sección>/_index.md` para `documentacion`, `manuales`, `memorias`, `proyectos`, `tareas` y `revision`;
|
||||||
|
- `data/admins.json` y `config/allowed_signers` vacíos (mismo formato que produce `admin add`, sin ningún administrador todavía);
|
||||||
|
- `data/review-events/.gitkeep`, `data/task-events/.gitkeep`, `data/generated/`;
|
||||||
|
- `.clusterlog-version` con la versión del binario que generó el scaffold;
|
||||||
|
- `README.md` y `.gitignore` de arranque.
|
||||||
|
|
||||||
|
Falla con `output_not_empty` (código `5`) si `--output` ya existe y no está vacío — nunca sobreescribe una instancia existente. Con `--dry-run` no crea el directorio; sólo muestra qué haría.
|
||||||
|
|
||||||
|
Después de `bootstrap-instance`, el flujo es el mismo que "Registrar al primer administrador" en `README.md`, apuntando `--root` al directorio nuevo: `admin add` → `sync` → `validate --skip-zola`. `validate` antes de la primera `sync` falla con `generated_state_stale` a propósito: la proyección todavía no existe.
|
||||||
|
|
||||||
|
**No incluido todavía** (fases posteriores, ver el ADR de motor/instancia cuando exista): plantillas Tera ni tema empaquetados para descarga, ni un `.gitea/workflows/ci.yml` de instancia — el motor aún no publica releases binarios.
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ SHELL := /usr/bin/env bash
|
|||||||
GO ?= go
|
GO ?= go
|
||||||
ZOLA ?= zola
|
ZOLA ?= zola
|
||||||
BINARY ?= bin/clusterlog
|
BINARY ?= bin/clusterlog
|
||||||
|
CLUSTERLOG_VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||||||
|
LDFLAGS := -X example.org/bitacora-cluster/internal/clusterlog.Version=$(CLUSTERLOG_VERSION)
|
||||||
|
|
||||||
.PHONY: all build test vet e2e theme sync sync-check validate validate-local site serve smoke ci clean runme-install runme-list runme-check runme-ci
|
.PHONY: all build test vet e2e theme sync sync-check validate validate-local site serve smoke ci clean runme-install runme-list runme-check runme-ci
|
||||||
|
|
||||||
@@ -9,7 +11,7 @@ all: build
|
|||||||
|
|
||||||
build:
|
build:
|
||||||
mkdir -p $(dir $(BINARY))
|
mkdir -p $(dir $(BINARY))
|
||||||
$(GO) build -trimpath -o $(BINARY) ./cmd/clusterlog
|
$(GO) build -trimpath -ldflags "$(LDFLAGS)" -o $(BINARY) ./cmd/clusterlog
|
||||||
|
|
||||||
test:
|
test:
|
||||||
$(GO) test ./...
|
$(GO) test ./...
|
||||||
|
|||||||
@@ -55,6 +55,16 @@ zola serve
|
|||||||
|
|
||||||
El sitio queda disponible en la dirección que muestre Zola. Antes de publicarlo, cambie `base_url` en `zola.toml` y manténgalo detrás de VPN, red privada o autenticación corporativa.
|
El sitio queda disponible en la dirección que muestre Zola. Antes de publicarlo, cambie `base_url` en `zola.toml` y manténgalo detrás de VPN, red privada o autenticación corporativa.
|
||||||
|
|
||||||
|
## Crear una instancia nueva
|
||||||
|
|
||||||
|
Este repositorio hoy mezcla la herramienta con el contenido de un deployment. Para empezar un deployment **distinto** sin partir de este contenido, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clusterlog bootstrap-instance --output ./mi-deploy --product-name "Mi Bitácora"
|
||||||
|
```
|
||||||
|
|
||||||
|
Genera `zola.toml`, las seis secciones de `content/` vacías, `data/admins.json`/`config/allowed_signers` vacíos y `.clusterlog-version`. Ver la sección 17 de [`CLI.md`](CLI.md) para el detalle completo; sigue siendo un primer paso — todavía no incluye plantillas/tema empaquetados ni CI de instancia (el motor no publica releases binarios todavía).
|
||||||
|
|
||||||
## Registrar al primer administrador
|
## Registrar al primer administrador
|
||||||
|
|
||||||
La llave pública identifica el registro del administrador. Para una auditoría fuerte, las operaciones posteriores deben usar `--commit` y la rama protegida debe rechazar commits sin firma válida.
|
La llave pública identifica el registro del administrador. Para una auditoría fuerte, las operaciones posteriores deben usar `--commit` y la rama protegida debe rechazar commits sin firma válida.
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ func (a *App) Run(args []string) error {
|
|||||||
if command == "schema" {
|
if command == "schema" {
|
||||||
return a.runSchema(args[1:])
|
return a.runSchema(args[1:])
|
||||||
}
|
}
|
||||||
|
if command == "bootstrap-instance" {
|
||||||
|
return a.runBootstrapInstance(args[1:])
|
||||||
|
}
|
||||||
|
|
||||||
root, err := a.ResolveRoot()
|
root, err := a.ResolveRoot()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -66,6 +69,8 @@ Opciones globales (antes del comando):
|
|||||||
--dry-run valida y muestra el resultado sin escribir
|
--dry-run valida y muestra el resultado sin escribir
|
||||||
|
|
||||||
Comandos:
|
Comandos:
|
||||||
|
version
|
||||||
|
bootstrap-instance --output DIR [--product-name NOMBRE] [--base-url URL]
|
||||||
identity
|
identity
|
||||||
admin add | admin list
|
admin add | admin list
|
||||||
memory create
|
memory create
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package clusterlog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed all:scaffold
|
||||||
|
var scaffoldFS embed.FS
|
||||||
|
|
||||||
|
const scaffoldRoot = "scaffold"
|
||||||
|
|
||||||
|
var scaffoldSections = []string{"documentacion", "manuales", "memorias", "proyectos", "tareas", "revision"}
|
||||||
|
|
||||||
|
// runBootstrapInstance genera el esqueleto de una instancia nueva de
|
||||||
|
// clusterlog (content/, data/, config/, zola.toml) en --output. No toca el
|
||||||
|
// proyecto actual: --output es un directorio nuevo, casi siempre fuera de
|
||||||
|
// este repositorio, así que este comando no pasa por ResolveRoot ni por el
|
||||||
|
// mecanismo de --commit.
|
||||||
|
func (a *App) runBootstrapInstance(args []string) error {
|
||||||
|
fs := newFlagSet("bootstrap-instance")
|
||||||
|
output := fs.String("output", "", "directorio donde crear la instancia nueva")
|
||||||
|
productName := fs.String("product-name", "Bitácora Operativa", "nombre del producto para zola.toml")
|
||||||
|
baseURL := fs.String("base-url", "https://clusterlog.example.org", "base_url de zola.toml; cámbielo antes de publicar")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return NewError(ExitUsage, "invalid_flags", err.Error(), nil)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(*output) == "" {
|
||||||
|
return NewError(ExitUsage, "missing_output", "indique --output con el directorio de la instancia nueva", nil)
|
||||||
|
}
|
||||||
|
root, err := filepath.Abs(*output)
|
||||||
|
if err != nil {
|
||||||
|
return NewError(ExitInternal, "internal", err.Error(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entries, statErr := os.ReadDir(root); statErr == nil {
|
||||||
|
if len(entries) > 0 {
|
||||||
|
return NewError(ExitConflict, "output_not_empty", "el directorio de salida ya existe y no está vacío", root)
|
||||||
|
}
|
||||||
|
} else if !os.IsNotExist(statErr) {
|
||||||
|
return NewError(ExitInternal, "internal", statErr.Error(), root)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Options.DryRun {
|
||||||
|
return a.printResult("bootstrap-instance", map[string]any{
|
||||||
|
"output": root,
|
||||||
|
"product_name": *productName,
|
||||||
|
"base_url": *baseURL,
|
||||||
|
"dry_run": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
created, err := writeScaffold(root, *productName, *baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
registry := AdminRegistry{Version: 1, ReviewPolicy: ReviewPolicy{Default: "all-active", IncludeAuthor: true}, Admins: []Admin{}}
|
||||||
|
if err := saveAdmins(root, registry); err != nil {
|
||||||
|
return NewError(ExitInternal, "internal", err.Error(), nil)
|
||||||
|
}
|
||||||
|
created = append(created,
|
||||||
|
filepath.Join(root, "data", "admins.json"),
|
||||||
|
filepath.Join(root, "config", "allowed_signers"),
|
||||||
|
)
|
||||||
|
sort.Strings(created)
|
||||||
|
|
||||||
|
return a.printResult("bootstrap-instance", map[string]any{
|
||||||
|
"output": root,
|
||||||
|
"engine_version": Version,
|
||||||
|
"product_name": *productName,
|
||||||
|
"base_url": *baseURL,
|
||||||
|
"files_created": created,
|
||||||
|
"next_steps": []string{
|
||||||
|
"clusterlog --root " + root + " --ssh-key <su-llave.pub> admin add --id <id> --name <nombre> --email <correo> --key <su-llave.pub> --roles owner,admin",
|
||||||
|
"clusterlog --root " + root + " sync",
|
||||||
|
"clusterlog --root " + root + " validate --skip-zola",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeScaffold(root, productName, baseURL string) ([]string, error) {
|
||||||
|
created := []string{}
|
||||||
|
|
||||||
|
for _, section := range scaffoldSections {
|
||||||
|
src := scaffoldRoot + "/content/" + section + "/_index.md"
|
||||||
|
data, err := scaffoldFS.ReadFile(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewError(ExitInternal, "internal", "plantilla de scaffold faltante: "+src, err.Error())
|
||||||
|
}
|
||||||
|
dst := filepath.Join(root, "content", section, "_index.md")
|
||||||
|
if err := writeFileAtomic(dst, data, 0o644); err != nil {
|
||||||
|
return nil, NewError(ExitInternal, "internal", err.Error(), dst)
|
||||||
|
}
|
||||||
|
created = append(created, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
zolaTmpl, err := scaffoldFS.ReadFile(scaffoldRoot + "/zola.toml.tmpl")
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewError(ExitInternal, "internal", "plantilla zola.toml faltante", err.Error())
|
||||||
|
}
|
||||||
|
title := "Bitácora Operativa"
|
||||||
|
if strings.TrimSpace(productName) != "" {
|
||||||
|
title = productName
|
||||||
|
}
|
||||||
|
zolaContent := renderScaffoldTemplate(string(zolaTmpl), map[string]string{
|
||||||
|
"BASE_URL": baseURL,
|
||||||
|
"TITLE": title,
|
||||||
|
"PRODUCT_NAME": productName,
|
||||||
|
})
|
||||||
|
zolaPath := filepath.Join(root, "zola.toml")
|
||||||
|
if err := writeFileAtomic(zolaPath, []byte(zolaContent), 0o644); err != nil {
|
||||||
|
return nil, NewError(ExitInternal, "internal", err.Error(), zolaPath)
|
||||||
|
}
|
||||||
|
created = append(created, zolaPath)
|
||||||
|
|
||||||
|
gitignoreTmpl, err := scaffoldFS.ReadFile(scaffoldRoot + "/gitignore.tmpl")
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewError(ExitInternal, "internal", "plantilla .gitignore faltante", err.Error())
|
||||||
|
}
|
||||||
|
gitignorePath := filepath.Join(root, ".gitignore")
|
||||||
|
if err := writeFileAtomic(gitignorePath, gitignoreTmpl, 0o644); err != nil {
|
||||||
|
return nil, NewError(ExitInternal, "internal", err.Error(), gitignorePath)
|
||||||
|
}
|
||||||
|
created = append(created, gitignorePath)
|
||||||
|
|
||||||
|
readmeTmpl, err := scaffoldFS.ReadFile(scaffoldRoot + "/README.md.tmpl")
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewError(ExitInternal, "internal", "plantilla README faltante", err.Error())
|
||||||
|
}
|
||||||
|
readmeContent := renderScaffoldTemplate(string(readmeTmpl), map[string]string{
|
||||||
|
"TITLE": title,
|
||||||
|
"PRODUCT_NAME": productName,
|
||||||
|
})
|
||||||
|
readmePath := filepath.Join(root, "README.md")
|
||||||
|
if err := writeFileAtomic(readmePath, []byte(readmeContent), 0o644); err != nil {
|
||||||
|
return nil, NewError(ExitInternal, "internal", err.Error(), readmePath)
|
||||||
|
}
|
||||||
|
created = append(created, readmePath)
|
||||||
|
|
||||||
|
versionPath := filepath.Join(root, ".clusterlog-version")
|
||||||
|
if err := writeFileAtomic(versionPath, []byte(Version+"\n"), 0o644); err != nil {
|
||||||
|
return nil, NewError(ExitInternal, "internal", err.Error(), versionPath)
|
||||||
|
}
|
||||||
|
created = append(created, versionPath)
|
||||||
|
|
||||||
|
for _, dir := range []string{filepath.Join(root, "data", "review-events"), filepath.Join(root, "data", "task-events")} {
|
||||||
|
keep := filepath.Join(dir, ".gitkeep")
|
||||||
|
if err := writeFileAtomic(keep, []byte{}, 0o644); err != nil {
|
||||||
|
return nil, NewError(ExitInternal, "internal", err.Error(), keep)
|
||||||
|
}
|
||||||
|
created = append(created, keep)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Join(root, "data", "generated"), 0o755); err != nil {
|
||||||
|
return nil, NewError(ExitInternal, "internal", err.Error(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return created, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderScaffoldTemplate(text string, values map[string]string) string {
|
||||||
|
for key, value := range values {
|
||||||
|
text = strings.ReplaceAll(text, "{{"+key+"}}", value)
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package clusterlog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newBootstrapApp(t *testing.T, dryRun bool) *App {
|
||||||
|
t.Helper()
|
||||||
|
return New(Options{DryRun: dryRun, JSON: true, Stdout: os.Stdout, Stderr: os.Stderr, Stdin: os.Stdin, Executable: "clusterlog"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBootstrapInstanceCreatesExpectedFiles(t *testing.T) {
|
||||||
|
base := t.TempDir()
|
||||||
|
out := filepath.Join(base, "instancia-nueva")
|
||||||
|
app := newBootstrapApp(t, false)
|
||||||
|
|
||||||
|
err := app.runBootstrapInstance([]string{"--output", out, "--product-name", "Bitácora de Prueba", "--base-url", "https://clusterlog.prueba.org"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("runBootstrapInstance falló: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, section := range scaffoldSections {
|
||||||
|
path := filepath.Join(out, "content", section, "_index.md")
|
||||||
|
if _, statErr := os.Stat(path); statErr != nil {
|
||||||
|
t.Fatalf("falta %s: %v", path, statErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zolaData, err := os.ReadFile(filepath.Join(out, "zola.toml"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
zola := string(zolaData)
|
||||||
|
if !strings.Contains(zola, `base_url = "https://clusterlog.prueba.org"`) {
|
||||||
|
t.Fatalf("zola.toml no tiene el base_url esperado: %s", zola)
|
||||||
|
}
|
||||||
|
if !strings.Contains(zola, `title = "Bitácora de Prueba"`) {
|
||||||
|
t.Fatalf("zola.toml no tiene el title esperado: %s", zola)
|
||||||
|
}
|
||||||
|
if !strings.Contains(zola, `product_name = "Bitácora de Prueba"`) {
|
||||||
|
t.Fatalf("zola.toml no tiene product_name esperado: %s", zola)
|
||||||
|
}
|
||||||
|
|
||||||
|
var registry AdminRegistry
|
||||||
|
adminsData, err := os.ReadFile(filepath.Join(out, "data", "admins.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(adminsData, ®istry); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if registry.Version != 1 || len(registry.Admins) != 0 || registry.ReviewPolicy.Default != "all-active" {
|
||||||
|
t.Fatalf("registro de admins inesperado: %+v", registry)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(filepath.Join(out, "config", "allowed_signers")); err != nil {
|
||||||
|
t.Fatalf("falta config/allowed_signers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionData, err := os.ReadFile(filepath.Join(out, ".clusterlog-version"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if trimmed := strings.TrimSpace(string(versionData)); trimmed != Version {
|
||||||
|
t.Fatalf(".clusterlog-version = %q, se esperaba %q", trimmed, Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range []string{"review-events", "task-events"} {
|
||||||
|
if _, err := os.Stat(filepath.Join(out, "data", dir, ".gitkeep")); err != nil {
|
||||||
|
t.Fatalf("falta data/%s/.gitkeep: %v", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(filepath.Join(out, "README.md")); err != nil {
|
||||||
|
t.Fatalf("falta README.md: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(out, ".gitignore")); err != nil {
|
||||||
|
t.Fatalf("falta .gitignore: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBootstrapInstanceScaffoldValidatesClean(t *testing.T) {
|
||||||
|
base := t.TempDir()
|
||||||
|
out := filepath.Join(base, "instancia-nueva")
|
||||||
|
app := newBootstrapApp(t, false)
|
||||||
|
if err := app.runBootstrapInstance([]string{"--output", out}); err != nil {
|
||||||
|
t.Fatalf("runBootstrapInstance falló: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
report := ValidationReport{Checks: map[string]int{}, Errors: []ValidationIssue{}, Warnings: []ValidationIssue{}}
|
||||||
|
validateApp := &App{}
|
||||||
|
registry, err := loadAdmins(out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
validateApp.validateAdmins(out, registry, &report)
|
||||||
|
pages := validateApp.validatePages(out, registry, &report)
|
||||||
|
validateApp.validateEvents(out, registry, pages, &report)
|
||||||
|
if len(report.Errors) != 0 {
|
||||||
|
t.Fatalf("el scaffold recién creado no debería tener errores de validate: %+v", report.Errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBootstrapInstanceRefusesNonEmptyOutput(t *testing.T) {
|
||||||
|
base := t.TempDir()
|
||||||
|
out := filepath.Join(base, "instancia-existente")
|
||||||
|
if err := os.MkdirAll(out, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mustWriteTestFile(t, filepath.Join(out, "algo.txt"), "ya había algo aquí")
|
||||||
|
|
||||||
|
app := newBootstrapApp(t, false)
|
||||||
|
err := app.runBootstrapInstance([]string{"--output", out})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("se esperaba un error por directorio no vacío")
|
||||||
|
}
|
||||||
|
cliErr, ok := err.(*CLIError)
|
||||||
|
if !ok || cliErr.Kind != "output_not_empty" {
|
||||||
|
t.Fatalf("se esperaba output_not_empty, se obtuvo: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBootstrapInstanceDryRunWritesNothing(t *testing.T) {
|
||||||
|
base := t.TempDir()
|
||||||
|
out := filepath.Join(base, "instancia-dry-run")
|
||||||
|
app := newBootstrapApp(t, true)
|
||||||
|
if err := app.runBootstrapInstance([]string{"--output", out}); err != nil {
|
||||||
|
t.Fatalf("runBootstrapInstance --dry-run falló: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(out); err == nil {
|
||||||
|
t.Fatal("--dry-run no debería haber creado el directorio de salida")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBootstrapInstanceRequiresOutput(t *testing.T) {
|
||||||
|
app := newBootstrapApp(t, false)
|
||||||
|
err := app.runBootstrapInstance([]string{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("se esperaba un error por --output faltante")
|
||||||
|
}
|
||||||
|
cliErr, ok := err.(*CLIError)
|
||||||
|
if !ok || cliErr.Kind != "missing_output" {
|
||||||
|
t.Fatalf("se esperaba missing_output, se obtuvo: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# {{TITLE}}
|
||||||
|
|
||||||
|
Este repositorio es una **instancia de clusterlog**: contiene el contenido,
|
||||||
|
los datos y la configuración de un deployment concreto, no el código del
|
||||||
|
motor. Se generó con:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clusterlog bootstrap-instance --output . --product-name "{{PRODUCT_NAME}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Qué hay aquí
|
||||||
|
|
||||||
|
- `content/` — documentación, manuales, memorias, proyectos y tareas de esta
|
||||||
|
instancia. Empieza con las seis secciones vacías (`_index.md` únicamente).
|
||||||
|
- `data/admins.json`, `config/allowed_signers` — registro de administradores,
|
||||||
|
vacío hasta el primer `admin add`.
|
||||||
|
- `zola.toml` — configuración de este deployment (`base_url`, nombre del
|
||||||
|
producto). Cámbiela antes de publicar.
|
||||||
|
- `.clusterlog-version` — versión del motor `clusterlog` usada para generar
|
||||||
|
este esqueleto. Manténgala sincronizada con el binario que use su CI.
|
||||||
|
|
||||||
|
## Primeros pasos
|
||||||
|
|
||||||
|
1. Cambie `base_url` en `zola.toml` por el dominio real de este deployment.
|
||||||
|
2. Registre al primer administrador (recibe `owner` automáticamente):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clusterlog --root . --ssh-key ~/.ssh/id_ed25519.pub admin add \
|
||||||
|
--id ana --name "Ana Torres" --email ana@example.org \
|
||||||
|
--key ~/.ssh/id_ed25519.pub --roles owner,admin
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Inicialice Git y firme el primer commit (vea la sección "Registrar al
|
||||||
|
primer administrador" en la documentación del motor para el detalle
|
||||||
|
completo de `git config`/`commit -S`).
|
||||||
|
4. Genere la proyección y valide:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clusterlog --root . sync
|
||||||
|
clusterlog --root . validate --skip-zola
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pendiente (no incluido por este comando)
|
||||||
|
|
||||||
|
- Plantillas Tera y el tema `zola.386`: todavía se obtienen desde un checkout
|
||||||
|
del repositorio del motor (`make theme` / `scripts/bootstrap-theme.sh`).
|
||||||
|
Empaquetarlos en un release descargable del motor es trabajo de una fase
|
||||||
|
posterior.
|
||||||
|
- Un workflow de CI para esta instancia (que descargue el binario del motor
|
||||||
|
por versión fijada) no se generó todavía: el motor aún no publica releases
|
||||||
|
binarios. Se agrega en cuanto exista ese release.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
+++
|
||||||
|
title = "Documentación"
|
||||||
|
description = "Arquitectura, decisiones, inventarios, políticas y explicaciones técnicas duraderas."
|
||||||
|
sort_by = "weight"
|
||||||
|
template = "section.html"
|
||||||
|
page_template = "page.html"
|
||||||
|
+++
|
||||||
|
|
||||||
|
Aquí vive el conocimiento descriptivo del clúster: el **qué**, el **por qué** y las relaciones entre componentes.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
+++
|
||||||
|
title = "Manuales"
|
||||||
|
description = "Runbooks y procedimientos operativos verificables, con prerrequisitos, pasos y reversión."
|
||||||
|
sort_by = "weight"
|
||||||
|
template = "section.html"
|
||||||
|
page_template = "page.html"
|
||||||
|
+++
|
||||||
|
|
||||||
|
Los manuales deben ser ejecutables por una persona distinta de quien los escribió y declarar claramente sus condiciones de seguridad.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
+++
|
||||||
|
title = "Memorias"
|
||||||
|
description = "Historia cronológica y auditable de intervenciones, cambios, incidentes y correcciones."
|
||||||
|
sort_by = "date"
|
||||||
|
template = "memories.html"
|
||||||
|
page_template = "memory.html"
|
||||||
|
generate_feeds = true
|
||||||
|
+++
|
||||||
|
|
||||||
|
Cada entrada describe una sesión de trabajo o una unidad de cambio. Las entradas sujetas a revisión aparecen también en **Revisión semanal**.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
+++
|
||||||
|
title = "Proyectos"
|
||||||
|
description = "Objetivos compartidos, alcance, responsables, hitos y criterios de éxito."
|
||||||
|
sort_by = "date"
|
||||||
|
template = "projects.html"
|
||||||
|
page_template = "project.html"
|
||||||
|
+++
|
||||||
|
|
||||||
|
Los proyectos dan contexto a las tareas y permiten explicar por qué existe cada línea de trabajo.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
+++
|
||||||
|
title = "Revisión semanal"
|
||||||
|
description = "Memorias que aún requieren confirmación de uno o más administradores."
|
||||||
|
template = "review.html"
|
||||||
|
in_search_index = false
|
||||||
|
+++
|
||||||
|
|
||||||
|
Esta página es la agenda operativa de la reunión semanal. Una memoria desaparece cuando todos los revisores requeridos han registrado su revisión para el hash actual del contenido.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
+++
|
||||||
|
title = "Tareas"
|
||||||
|
description = "Trabajo visible para el equipo, con prioridad, estimación, fecha objetivo y tiempo acumulado."
|
||||||
|
sort_by = "date"
|
||||||
|
template = "tasks.html"
|
||||||
|
page_template = "task.html"
|
||||||
|
+++
|
||||||
|
|
||||||
|
El estado actual y el tiempo real se calculan a partir de eventos anexables; la definición original de la tarea permanece legible en Markdown.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/public/
|
||||||
|
/bin/
|
||||||
|
/themes/zola.386/
|
||||||
|
.DS_Store
|
||||||
|
*.tmp
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
base_url = "{{BASE_URL}}"
|
||||||
|
title = "{{TITLE}}"
|
||||||
|
description = "Documentación, manuales, memorias de cambio, proyectos y tareas del equipo de soporte."
|
||||||
|
default_language = "es"
|
||||||
|
theme = "zola.386"
|
||||||
|
output_dir = "public"
|
||||||
|
compile_sass = true
|
||||||
|
minify_html = true
|
||||||
|
generate_feeds = true
|
||||||
|
feed_filenames = ["atom.xml", "rss.xml"]
|
||||||
|
build_search_index = true
|
||||||
|
generate_sitemap = true
|
||||||
|
generate_robots_txt = false
|
||||||
|
|
||||||
|
taxonomies = [
|
||||||
|
{ name = "categories" },
|
||||||
|
{ name = "tags", feed = true },
|
||||||
|
{ name = "systems" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[markdown]
|
||||||
|
external_links_target_blank = true
|
||||||
|
external_links_no_referrer = true
|
||||||
|
smart_punctuation = true
|
||||||
|
github_alerts = true
|
||||||
|
insert_anchor_links = "right"
|
||||||
|
|
||||||
|
[markdown.highlighting]
|
||||||
|
error_on_missing_language = false
|
||||||
|
style = "inline"
|
||||||
|
theme = "vitesse-black"
|
||||||
|
|
||||||
|
[search]
|
||||||
|
include_title = true
|
||||||
|
include_description = true
|
||||||
|
include_date = true
|
||||||
|
include_path = true
|
||||||
|
include_content = true
|
||||||
|
truncate_content_length = 12000
|
||||||
|
index_format = "elasticlunr_json"
|
||||||
|
|
||||||
|
[slugify]
|
||||||
|
paths = "on"
|
||||||
|
taxonomies = "on"
|
||||||
|
anchors = "on"
|
||||||
|
paths_keep_dates = false
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
product_name = "{{PRODUCT_NAME}}"
|
||||||
|
product_short_name = "clusterlog"
|
||||||
|
repository_label = "Repositorio interno"
|
||||||
|
review_meeting_label = "Revisión semanal"
|
||||||
|
footer_text = "Fuente auditable: Markdown + eventos JSON + Git"
|
||||||
@@ -10,7 +10,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Version = "0.1.0"
|
// Version se puede sobreescribir en tiempo de build con:
|
||||||
|
//
|
||||||
|
// -ldflags "-X example.org/bitacora-cluster/internal/clusterlog.Version=v0.2.0"
|
||||||
|
//
|
||||||
|
// Ver la variable CLUSTERLOG_VERSION en el Makefile.
|
||||||
|
var Version = "0.1.0"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ExitOK = 0
|
ExitOK = 0
|
||||||
|
|||||||
Reference in New Issue
Block a user