Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions internal/export/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (s *Service) Export(ctx context.Context, opts Options) (*output.Manifest, e
}

for _, svc := range services {
if err := s.exportService(ctx, writer, opts, svc); err != nil {
if err := s.exportService(ctx, writer, opts, svc, manifest); err != nil {
manifest.Incomplete = true
return manifest, fmt.Errorf("export service %q: %w", svc.SystemName, err)
}
Expand Down Expand Up @@ -179,7 +179,7 @@ func (s *Service) exportPolicyCatalog(ctx context.Context, writer *output.Writer
return writer.WriteJSON("policies/catalog.json", catalog)
}

func (s *Service) exportService(ctx context.Context, writer *output.Writer, opts Options, svc serviceRef) error {
func (s *Service) exportService(ctx context.Context, writer *output.Writer, opts Options, svc serviceRef, manifest *output.Manifest) error {
yamlData, err := s.toolbox.ExportProduct(ctx, opts.AdminURL, opts.Token, svc.SystemName)
if err != nil {
return err
Expand All @@ -203,6 +203,7 @@ func (s *Service) exportService(ctx context.Context, writer *output.Writer, opts
for _, f := range fetches {
var payload json.RawMessage
if err := s.client.Get(ctx, f.path, &payload); err != nil {
manifest.RecordSkip(svc.SystemName, f.file, f.path, err)
continue
}
rel := filepath.Join("products", svc.SystemName, f.file)
Expand Down
46 changes: 46 additions & 0 deletions internal/export/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,49 @@ func TestAppendYAMLNewline(t *testing.T) {
t.Fatalf("got %q", got)
}
}

func TestExportRecordsWarningsOnSkippedSidecars(t *testing.T) {
dir := t.TempDir()
client := &mockClient{
responses: map[string]any{
"/services": serviceListResponse{
Services: []serviceEntry{{Service: serviceRef{ID: 10, SystemName: "payments"}}},
},
"/backend_apis": backendListResponse{},
"/policies": map[string]any{},
"/services/10/proxy": map[string]any{
"proxy": map[string]any{"auth_type": "oidc"},
},
"/services/10/proxy/policies": map[string]any{"policies": []any{}},
"/services/10/application_plans": map[string]any{"plans": []any{}},
"/services/10/backend_usages": map[string]any{"backend_usages": []any{}},
"/services/10/metrics": map[string]any{"metrics": []any{}},
},
}
toolbox := &mockToolbox{outputs: map[string][]byte{"payments": []byte("apiVersion: v1\n")}}

svc := NewService(client, toolbox)
manifest, err := svc.Export(context.Background(), Options{
AdminURL: "https://tenant.example.com",
Token: "tok",
OutDir: dir,
})
if err != nil {
t.Fatal(err)
}
if !manifest.Incomplete {
t.Fatal("expected incomplete manifest")
}
if len(manifest.Warnings) != 1 {
t.Fatalf("Warnings = %#v, want 1 entry", manifest.Warnings)
}
if !strings.Contains(manifest.Warnings[0], "oidc_configuration.json") {
t.Fatalf("warning = %q", manifest.Warnings[0])
}
if _, err := os.Stat(filepath.Join(dir, "products", "payments", "proxy.json")); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(dir, "products", "payments", "oidc_configuration.json")); !os.IsNotExist(err) {
t.Fatalf("oidc_configuration.json should be absent: %v", err)
}
}
26 changes: 18 additions & 8 deletions internal/output/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,24 @@ import (
const SchemaVersion = "1.0"

type Manifest struct {
SchemaVersion string `json:"schema_version"`
ExportedAt string `json:"exported_at"`
AdminURL string `json:"admin_url"`
ProductCount int `json:"product_count"`
BackendCount int `json:"backend_count"`
ApplicationCount int `json:"application_count,omitempty"`
IncludeApplications bool `json:"include_applications"`
Incomplete bool `json:"incomplete"`
SchemaVersion string `json:"schema_version"`
ExportedAt string `json:"exported_at"`
AdminURL string `json:"admin_url"`
ProductCount int `json:"product_count"`
BackendCount int `json:"backend_count"`
ApplicationCount int `json:"application_count,omitempty"`
IncludeApplications bool `json:"include_applications"`
Incomplete bool `json:"incomplete"`
Warnings []string `json:"warnings,omitempty"`
}

// RecordSkip appends a warning when an optional export resource could not be fetched.
func (m *Manifest) RecordSkip(product, file, apiPath string, err error) {
if m == nil || err == nil {
return
}
m.Warnings = append(m.Warnings, fmt.Sprintf("product %s: skipped %s (%s: %v)", product, file, apiPath, err))
m.Incomplete = true
}

type Writer struct {
Expand Down
11 changes: 11 additions & 0 deletions internal/output/writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,14 @@ func TestWriterRoot(t *testing.T) {
t.Fatalf("Root = %q", w.Root())
}
}

func TestManifestRecordSkip(t *testing.T) {
m := &Manifest{}
m.RecordSkip("payments", "oidc_configuration.json", "/services/10/proxy/oidc_configuration", os.ErrNotExist)
if !m.Incomplete {
t.Fatal("expected incomplete")
}
if len(m.Warnings) != 1 || !strings.Contains(m.Warnings[0], "payments") {
t.Fatalf("Warnings = %#v", m.Warnings)
}
}
7 changes: 7 additions & 0 deletions internal/visualize/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ func renderIndex(t *Tenant, outDir string) string {
if t.Manifest.Incomplete {
b.WriteString("> **Warning:** Export marked incomplete — some data may be missing.\n\n")
}
if len(t.Manifest.Warnings) > 0 {
b.WriteString("## Export warnings\n\n")
for _, warning := range t.Manifest.Warnings {
b.WriteString(fmt.Sprintf("- %s\n", mdCell(warning)))
}
b.WriteString("\n")
}

b.WriteString("## Overview\n\n")
b.WriteString("| Field | Value |\n")
Expand Down
27 changes: 27 additions & 0 deletions internal/visualize/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,33 @@ func TestWriteReportIncompleteBanner(t *testing.T) {
}
}

func TestWriteReportManifestWarnings(t *testing.T) {
tenant, err := LoadExport(filepath.Join("testdata", "export-minimal"))
if err != nil {
t.Fatal(err)
}
tenant.Manifest.Incomplete = true
tenant.Manifest.Warnings = []string{
"product payments: skipped oidc_configuration.json (/services/10/proxy/oidc_configuration: unrecoverable)",
}

out := t.TempDir()
if err := WriteReport(tenant, out); err != nil {
t.Fatal(err)
}
index, err := os.ReadFile(filepath.Join(out, "index.md"))
if err != nil {
t.Fatal(err)
}
text := string(index)
if !strings.Contains(text, "## Export warnings") {
t.Fatalf("missing warnings section: %s", text)
}
if !strings.Contains(text, "oidc_configuration.json") {
t.Fatalf("missing warning detail: %s", text)
}
}

func TestWriteReportValidation(t *testing.T) {
if err := WriteReport(nil, t.TempDir()); err == nil {
t.Fatal("expected error for nil tenant")
Expand Down
Loading