diff --git a/internal/terraform/enums.go b/internal/terraform/enums.go index 6bfd3e6..10a0cb9 100644 --- a/internal/terraform/enums.go +++ b/internal/terraform/enums.go @@ -172,7 +172,35 @@ func pageAccessTypeToString(t status_pagev1.PageAccessType) string { return "password" case status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_AUTHENTICATED: return "email-domain" + case status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_IP_RESTRICTED: + return "ip" default: return "public" } } + +func pageThemeToString(t status_pagev1.PageTheme) string { + switch t { + case status_pagev1.PageTheme_PAGE_THEME_SYSTEM: + return "system" + case status_pagev1.PageTheme_PAGE_THEME_LIGHT: + return "light" + case status_pagev1.PageTheme_PAGE_THEME_DARK: + return "dark" + default: + return "" + } +} + +func localeToString(l status_pagev1.Locale) string { + switch l { + case status_pagev1.Locale_LOCALE_EN: + return "en" + case status_pagev1.Locale_LOCALE_FR: + return "fr" + case status_pagev1.Locale_LOCALE_DE: + return "de" + default: + return "" + } +} diff --git a/internal/terraform/fetch.go b/internal/terraform/fetch.go index e7f5c11..a953cef 100644 --- a/internal/terraform/fetch.go +++ b/internal/terraform/fetch.go @@ -4,12 +4,12 @@ import ( "context" "fmt" - monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/monitor/v1/monitorv1connect" - notificationv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/notification/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/notification/v1/notificationv1connect" - status_pagev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_page/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/status_page/v1/status_pagev1connect" + monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" + notificationv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/notification/v1" + status_pagev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_page/v1" "connectrpc.com/connect" "github.com/openstatusHQ/cli/internal/api" ) diff --git a/internal/terraform/generate_test.go b/internal/terraform/generate_test.go index 4419ba0..68c8898 100644 --- a/internal/terraform/generate_test.go +++ b/internal/terraform/generate_test.go @@ -213,6 +213,113 @@ func TestCrossReferenceFallbackToString(t *testing.T) { mustContain(t, content, `monitor_id = "nonexistent-id"`) } +func TestGenerateStatusPagesFile_IPAccess(t *testing.T) { + page := &status_pagev1.StatusPage{} + page.SetId("p1") + page.SetTitle("Internal") + page.SetSlug("internal") + page.SetAccessType(status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_IP_RESTRICTED) + page.SetAllowedIpRanges("10.0.0.0/8,192.168.0.0/16") + + data := &WorkspaceData{StatusPages: []StatusPageData{{Page: page}}} + gen := NewGenerator(data) + content := string(gen.GenerateStatusPagesFile().Bytes()) + + mustContain(t, content, `access_type = "ip"`) + mustContain(t, content, `allowed_ip_ranges = "10.0.0.0/8,192.168.0.0/16"`) + mustNotContain(t, content, "REPLACE_ME") +} + +func TestGenerateStatusPagesFile_IPAccessEmptyFallback(t *testing.T) { + page := &status_pagev1.StatusPage{} + page.SetId("p1") + page.SetTitle("Internal") + page.SetSlug("internal") + page.SetAccessType(status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_IP_RESTRICTED) + + data := &WorkspaceData{StatusPages: []StatusPageData{{Page: page}}} + gen := NewGenerator(data) + content := string(gen.GenerateStatusPagesFile().Bytes()) + + mustContain(t, content, `access_type = "ip"`) + mustContain(t, content, `allowed_ip_ranges = "REPLACE_ME"`) + mustContain(t, content, "# TODO:") +} + +func TestGenerateStatusPagesFile_EmailDomainAccess(t *testing.T) { + page := &status_pagev1.StatusPage{} + page.SetId("p1") + page.SetTitle("Internal") + page.SetSlug("internal") + page.SetAccessType(status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_AUTHENTICATED) + page.SetAuthEmailDomains([]string{"example.com", "acme.com"}) + + data := &WorkspaceData{StatusPages: []StatusPageData{{Page: page}}} + gen := NewGenerator(data) + content := string(gen.GenerateStatusPagesFile().Bytes()) + + mustContain(t, content, `access_type = "email-domain"`) + mustContain(t, content, `auth_email_domains = ["acme.com", "example.com"]`) + mustNotContain(t, content, "REPLACE_ME") +} + +func TestGenerateStatusPagesFile_EmailDomainEmptyFallback(t *testing.T) { + page := &status_pagev1.StatusPage{} + page.SetId("p1") + page.SetTitle("Internal") + page.SetSlug("internal") + page.SetAccessType(status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_AUTHENTICATED) + + data := &WorkspaceData{StatusPages: []StatusPageData{{Page: page}}} + gen := NewGenerator(data) + content := string(gen.GenerateStatusPagesFile().Bytes()) + + mustContain(t, content, `access_type = "email-domain"`) + mustContain(t, content, `auth_email_domains = ["REPLACE_ME"]`) + mustContain(t, content, "# TODO:") +} + +func TestGenerateStatusPagesFile_ThemeLocaleAllowIndex(t *testing.T) { + page := &status_pagev1.StatusPage{} + page.SetId("p1") + page.SetTitle("Themed") + page.SetSlug("themed") + page.SetTheme(status_pagev1.PageTheme_PAGE_THEME_DARK) + page.SetDefaultLocale(status_pagev1.Locale_LOCALE_FR) + page.SetLocales([]status_pagev1.Locale{ + status_pagev1.Locale_LOCALE_FR, + status_pagev1.Locale_LOCALE_EN, + }) + page.SetAllowIndex(true) + + data := &WorkspaceData{StatusPages: []StatusPageData{{Page: page}}} + gen := NewGenerator(data) + content := string(gen.GenerateStatusPagesFile().Bytes()) + + mustContain(t, content, `theme = "dark"`) + mustContain(t, content, `default_locale = "fr"`) + mustContain(t, content, `locales = ["en", "fr"]`) + mustContain(t, content, `allow_index = true`) +} + +func TestGenerateStatusPagesFile_DefaultsOmitted(t *testing.T) { + page := &status_pagev1.StatusPage{} + page.SetId("p1") + page.SetTitle("Plain") + page.SetSlug("plain") + page.SetTheme(status_pagev1.PageTheme_PAGE_THEME_SYSTEM) + page.SetDefaultLocale(status_pagev1.Locale_LOCALE_EN) + + data := &WorkspaceData{StatusPages: []StatusPageData{{Page: page}}} + gen := NewGenerator(data) + content := string(gen.GenerateStatusPagesFile().Bytes()) + + mustNotContain(t, content, "theme") + mustNotContain(t, content, "default_locale") + mustNotContain(t, content, "locales") + mustNotContain(t, content, "allow_index") +} + func mustContain(t *testing.T, content, substr string) { t.Helper() if !strings.Contains(content, substr) { diff --git a/internal/terraform/hcl.go b/internal/terraform/hcl.go index dae96db..e2d6e51 100644 --- a/internal/terraform/hcl.go +++ b/internal/terraform/hcl.go @@ -2,6 +2,7 @@ package terraform import ( "fmt" + "sort" monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" notificationv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/notification/v1" @@ -23,13 +24,13 @@ type Generator struct { pageRefs map[string]resourceRef groupRefs map[string]resourceRef - httpMonitorNames map[string]string - tcpMonitorNames map[string]string - dnsMonitorNames map[string]string - notifNames map[string]string - pageNames map[string]string - componentNames map[string]string - groupNames map[string]string + httpMonitorNames map[string]string + tcpMonitorNames map[string]string + dnsMonitorNames map[string]string + notifNames map[string]string + pageNames map[string]string + componentNames map[string]string + groupNames map[string]string } func NewGenerator(data *WorkspaceData) *Generator { @@ -333,13 +334,59 @@ func (g *Generator) GenerateStatusPagesFile() *hclwrite.File { b.SetAttributeValue("custom_domain", cty.StringVal(page.GetCustomDomain())) } - accessType := pageAccessTypeToString(page.GetAccessType()) - if accessType != "public" { - b.SetAttributeValue("access_type", cty.StringVal(accessType)) - if accessType == "password" { + switch pageAccessTypeToString(page.GetAccessType()) { + case "password": + b.SetAttributeValue("access_type", cty.StringVal("password")) + appendTODOComment(b) + b.SetAttributeValue("password", cty.StringVal("REPLACE_ME")) + case "email-domain": + b.SetAttributeValue("access_type", cty.StringVal("email-domain")) + domains := append([]string(nil), page.GetAuthEmailDomains()...) + sort.Strings(domains) + if len(domains) > 0 { + vals := make([]cty.Value, len(domains)) + for i, d := range domains { + vals[i] = cty.StringVal(d) + } + b.SetAttributeValue("auth_email_domains", cty.ListVal(vals)) + } else { appendTODOComment(b) - b.SetAttributeValue("password", cty.StringVal("REPLACE_ME")) + b.SetAttributeValue("auth_email_domains", cty.ListVal([]cty.Value{cty.StringVal("REPLACE_ME")})) } + case "ip": + b.SetAttributeValue("access_type", cty.StringVal("ip")) + if r := page.GetAllowedIpRanges(); r != "" { + b.SetAttributeValue("allowed_ip_ranges", cty.StringVal(r)) + } else { + appendTODOComment(b) + b.SetAttributeValue("allowed_ip_ranges", cty.StringVal("REPLACE_ME")) + } + } + + if theme := pageThemeToString(page.GetTheme()); theme != "" && theme != "system" { + b.SetAttributeValue("theme", cty.StringVal(theme)) + } + if dl := localeToString(page.GetDefaultLocale()); dl != "" && dl != "en" { + b.SetAttributeValue("default_locale", cty.StringVal(dl)) + } + if locs := page.GetLocales(); len(locs) > 0 { + strs := make([]string, 0, len(locs)) + for _, l := range locs { + if s := localeToString(l); s != "" { + strs = append(strs, s) + } + } + if len(strs) > 0 { + sort.Strings(strs) + vals := make([]cty.Value, len(strs)) + for i, s := range strs { + vals[i] = cty.StringVal(s) + } + b.SetAttributeValue("locales", cty.ListVal(vals)) + } + } + if page.GetAllowIndex() { + b.SetAttributeValue("allow_index", cty.BoolVal(true)) } body.AppendNewline() diff --git a/plan.md b/plan.md index 3d04df9..6337981 100644 --- a/plan.md +++ b/plan.md @@ -450,21 +450,21 @@ Each phase is one commit. Phases are ordered so that earlier work doesn't confli - [x] `internal/terraform/generate_test.go` — update `TestGenerateProviderFile` assertion to `~> 0.2`. - [x] `go test ./internal/terraform/...` green. -### Phase 2 — Status page correctness pack (commit 2: `fix(terraform): emit access_type=ip + auth_email_domains/allowed_ip_ranges; add theme/locale/allow_index`) +### Phase 2 — Status page correctness pack (commit 2: `fix(terraform): emit access_type=ip + auth_email_domains/allowed_ip_ranges; add theme/locale/allow_index`) ✅ Order matters within this phase: add helpers first, then call them. -- [ ] `internal/terraform/enums.go` — extend `pageAccessTypeToString` with `case status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_IP_RESTRICTED: return "ip"`. -- [ ] `internal/terraform/enums.go` — add `pageThemeToString(t status_pagev1.PageTheme) string` (returns `""` for UNSPECIFIED, `"system"|"light"|"dark"` otherwise). -- [ ] `internal/terraform/enums.go` — add `localeToString(l status_pagev1.Locale) string` (returns `""` for UNSPECIFIED, `"en"|"fr"|"de"` otherwise). -- [ ] `internal/terraform/hcl.go` — `GenerateStatusPagesFile`: replace the current `accessType != "public"` block with the four-case switch (`public` omits / `password` keeps current TODO+REPLACE_ME / `email-domain` emits `auth_email_domains` sorted via `sort.Strings` or TODO+REPLACE_ME / `ip` emits `allowed_ip_ranges` or TODO+REPLACE_ME). -- [ ] `internal/terraform/hcl.go` — `GenerateStatusPagesFile`: after the access-type block, emit `theme` (when not `system`), `default_locale` (when not `en`), `locales` (sorted, when non-empty), `allow_index` (when `true`), using skip-default rules from Q1. -- [ ] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_IPAccess` with non-empty `AllowedIpRanges`. -- [ ] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_IPAccessEmptyFallback`: IP_RESTRICTED + empty `allowed_ip_ranges` → asserts `# TODO:` comment and `REPLACE_ME` value. -- [ ] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_EmailDomainAccess` with non-empty domains. -- [ ] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_EmailDomainEmptyFallback`. -- [ ] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_ThemeLocaleAllowIndex`: dark + fr default_locale + locales=[en,fr] + allow_index=true → all four emitted; default page → none emitted. -- [ ] `go test ./internal/terraform/...` green. +- [x] `internal/terraform/enums.go` — extend `pageAccessTypeToString` with `case status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_IP_RESTRICTED: return "ip"`. +- [x] `internal/terraform/enums.go` — add `pageThemeToString(t status_pagev1.PageTheme) string` (returns `""` for UNSPECIFIED, `"system"|"light"|"dark"` otherwise). +- [x] `internal/terraform/enums.go` — add `localeToString(l status_pagev1.Locale) string` (returns `""` for UNSPECIFIED, `"en"|"fr"|"de"` otherwise). +- [x] `internal/terraform/hcl.go` — `GenerateStatusPagesFile`: replace the current `accessType != "public"` block with the four-case switch (`public` omits / `password` keeps current TODO+REPLACE_ME / `email-domain` emits `auth_email_domains` sorted via `sort.Strings` or TODO+REPLACE_ME / `ip` emits `allowed_ip_ranges` or TODO+REPLACE_ME). +- [x] `internal/terraform/hcl.go` — `GenerateStatusPagesFile`: after the access-type block, emit `theme` (when not `system`), `default_locale` (when not `en`), `locales` (sorted, when non-empty), `allow_index` (when `true`), using skip-default rules from Q1. +- [x] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_IPAccess` with non-empty `AllowedIpRanges`. +- [x] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_IPAccessEmptyFallback`: IP_RESTRICTED + empty `allowed_ip_ranges` → asserts `# TODO:` comment and `REPLACE_ME` value. +- [x] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_EmailDomainAccess` with non-empty domains. +- [x] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_EmailDomainEmptyFallback`. +- [x] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_ThemeLocaleAllowIndex`: dark + fr default_locale + locales=[en,fr] + allow_index=true → all four emitted; default page → none emitted. +- [x] `go test ./internal/terraform/...` green. ### Phase 3 — Component group `default_open` (commit 3: `feat(terraform): emit default_open on status page component groups`)