diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index d5ccfb06dfc47..d79e3f65316e5 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11190,6 +11190,9 @@ const docTemplate = `{ "$ref": "#/definitions/derp.ServerInfoMessage" }, "round_trip_ping": { + "type": "string" + }, + "round_trip_ping_ms": { "type": "integer" }, "stun": { @@ -11258,7 +11261,9 @@ const docTemplate = `{ "enabled": { "type": "boolean" }, - "error": {} + "error": { + "type": "string" + } } }, "healthcheck.DatabaseReport": { @@ -11271,6 +11276,9 @@ const docTemplate = `{ "type": "boolean" }, "latency": { + "type": "string" + }, + "latency_ms": { "type": "integer" }, "reachable": { @@ -11315,20 +11323,6 @@ const docTemplate = `{ } }, "healthcheck.WebsocketReport": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "healthy": { - "type": "boolean" - }, - "response": { - "$ref": "#/definitions/healthcheck.WebsocketResponse" - } - } - }, - "healthcheck.WebsocketResponse": { "type": "object", "properties": { "body": { @@ -11336,6 +11330,12 @@ const docTemplate = `{ }, "code": { "type": "integer" + }, + "error": { + "type": "string" + }, + "healthy": { + "type": "boolean" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 69b3e1f6a5453..2dfc35578dc5a 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10172,6 +10172,9 @@ "$ref": "#/definitions/derp.ServerInfoMessage" }, "round_trip_ping": { + "type": "string" + }, + "round_trip_ping_ms": { "type": "integer" }, "stun": { @@ -10240,7 +10243,9 @@ "enabled": { "type": "boolean" }, - "error": {} + "error": { + "type": "string" + } } }, "healthcheck.DatabaseReport": { @@ -10253,6 +10258,9 @@ "type": "boolean" }, "latency": { + "type": "string" + }, + "latency_ms": { "type": "integer" }, "reachable": { @@ -10297,20 +10305,6 @@ } }, "healthcheck.WebsocketReport": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "healthy": { - "type": "boolean" - }, - "response": { - "$ref": "#/definitions/healthcheck.WebsocketResponse" - } - } - }, - "healthcheck.WebsocketResponse": { "type": "object", "properties": { "body": { @@ -10318,6 +10312,12 @@ }, "code": { "type": "integer" + }, + "error": { + "type": "string" + }, + "healthy": { + "type": "boolean" } } }, diff --git a/coderd/healthcheck/accessurl.go b/coderd/healthcheck/accessurl.go index b25e956b7d918..b91889b2842a2 100644 --- a/coderd/healthcheck/accessurl.go +++ b/coderd/healthcheck/accessurl.go @@ -12,6 +12,7 @@ import ( "github.com/coder/coder/coderd/util/ptr" ) +// @typescript-generate AccessURLReport type AccessURLReport struct { AccessURL string `json:"access_url"` Healthy bool `json:"healthy"` diff --git a/coderd/healthcheck/database.go b/coderd/healthcheck/database.go index 291b9361b8314..c92ef3c447d56 100644 --- a/coderd/healthcheck/database.go +++ b/coderd/healthcheck/database.go @@ -10,11 +10,13 @@ import ( "github.com/coder/coder/coderd/database" ) +// @typescript-generate DatabaseReport type DatabaseReport struct { - Healthy bool `json:"healthy"` - Reachable bool `json:"reachable"` - Latency time.Duration `json:"latency"` - Error *string `json:"error"` + Healthy bool `json:"healthy"` + Reachable bool `json:"reachable"` + Latency string `json:"latency"` + LatencyMs int `json:"latency_ms"` + Error *string `json:"error"` } type DatabaseReportOptions struct { @@ -39,10 +41,12 @@ func (r *DatabaseReport) Run(ctx context.Context, opts *DatabaseReportOptions) { slices.Sort(pings) // Take the median ping. - r.Latency = pings[pingCount/2] + latency := pings[pingCount/2] + r.Latency = latency.String() + r.LatencyMs = int(latency.Milliseconds()) // Somewhat arbitrary, but if the latency is over 15ms, we consider it // unhealthy. - if r.Latency < 15*time.Millisecond { + if latency < 15*time.Millisecond { r.Healthy = true } r.Reachable = true diff --git a/coderd/healthcheck/database_test.go b/coderd/healthcheck/database_test.go index bf9f64471333f..615728a8b573b 100644 --- a/coderd/healthcheck/database_test.go +++ b/coderd/healthcheck/database_test.go @@ -35,7 +35,8 @@ func TestDatabase(t *testing.T) { assert.True(t, report.Healthy) assert.True(t, report.Reachable) - assert.Equal(t, ping, report.Latency) + assert.Equal(t, ping.String(), report.Latency) + assert.Equal(t, int(ping.Milliseconds()), report.LatencyMs) assert.Nil(t, report.Error) }) @@ -81,7 +82,8 @@ func TestDatabase(t *testing.T) { assert.True(t, report.Healthy) assert.True(t, report.Reachable) - assert.Equal(t, time.Millisecond, report.Latency) + assert.Equal(t, time.Millisecond.String(), report.Latency) + assert.Equal(t, 1, report.LatencyMs) assert.Nil(t, report.Error) }) } diff --git a/coderd/healthcheck/derp.go b/coderd/healthcheck/derp.go index 0b77c42076254..8237d422e1410 100644 --- a/coderd/healthcheck/derp.go +++ b/coderd/healthcheck/derp.go @@ -24,6 +24,7 @@ import ( "github.com/coder/coder/coderd/util/ptr" ) +// @typescript-generate DERPReport type DERPReport struct { Healthy bool `json:"healthy"` @@ -36,6 +37,7 @@ type DERPReport struct { Error *string `json:"error"` } +// @typescript-generate DERPRegionReport type DERPRegionReport struct { mu sync.Mutex Healthy bool `json:"healthy"` @@ -44,6 +46,8 @@ type DERPRegionReport struct { NodeReports []*DERPNodeReport `json:"node_reports"` Error *string `json:"error"` } + +// @typescript-generate DERPNodeReport type DERPNodeReport struct { mu sync.Mutex clientCounter int @@ -53,7 +57,8 @@ type DERPNodeReport struct { ServerInfo derp.ServerInfoMessage `json:"node_info"` CanExchangeMessages bool `json:"can_exchange_messages"` - RoundTripPing time.Duration `json:"round_trip_ping"` + RoundTripPing string `json:"round_trip_ping"` + RoundTripPingMs int `json:"round_trip_ping_ms"` UsesWebsocket bool `json:"uses_websocket"` ClientLogs [][]string `json:"client_logs"` ClientErrs [][]string `json:"client_errs"` @@ -62,10 +67,11 @@ type DERPNodeReport struct { STUN DERPStunReport `json:"stun"` } +// @typescript-generate DERPStunReport type DERPStunReport struct { Enabled bool CanSTUN bool - Error error + Error *string } type DERPReportOptions struct { @@ -251,7 +257,9 @@ func (r *DERPNodeReport) doExchangeMessage(ctx context.Context) { r.mu.Lock() r.CanExchangeMessages = true - r.RoundTripPing = time.Since(*t) + rtt := time.Since(*t) + r.RoundTripPing = rtt.String() + r.RoundTripPingMs = int(rtt.Milliseconds()) r.mu.Unlock() cancel() @@ -301,20 +309,20 @@ func (r *DERPNodeReport) doSTUNTest(ctx context.Context) { addr, port, err := r.stunAddr(ctx) if err != nil { - r.STUN.Error = xerrors.Errorf("get stun addr: %w", err) + r.STUN.Error = convertError(xerrors.Errorf("get stun addr: %w", err)) return } // We only create a prober to call ProbeUDP manually. p, err := prober.DERP(prober.New(), "", time.Second, time.Second, time.Second) if err != nil { - r.STUN.Error = xerrors.Errorf("create prober: %w", err) + r.STUN.Error = convertError(xerrors.Errorf("create prober: %w", err)) return } err = p.ProbeUDP(addr, port)(ctx) if err != nil { - r.STUN.Error = xerrors.Errorf("probe stun: %w", err) + r.STUN.Error = convertError(xerrors.Errorf("probe stun: %w", err)) return } diff --git a/coderd/healthcheck/derp_test.go b/coderd/healthcheck/derp_test.go index 8fa1cb5f952da..d27cdc182ec31 100644 --- a/coderd/healthcheck/derp_test.go +++ b/coderd/healthcheck/derp_test.go @@ -67,7 +67,7 @@ func TestDERP(t *testing.T) { for _, node := range region.NodeReports { assert.True(t, node.Healthy) assert.True(t, node.CanExchangeMessages) - assert.Positive(t, node.RoundTripPing) + assert.NotEmpty(t, node.RoundTripPing) assert.Len(t, node.ClientLogs, 2) assert.Len(t, node.ClientLogs[0], 1) assert.Len(t, node.ClientErrs[0], 0) @@ -76,7 +76,7 @@ func TestDERP(t *testing.T) { assert.False(t, node.STUN.Enabled) assert.False(t, node.STUN.CanSTUN) - assert.NoError(t, node.STUN.Error) + assert.Nil(t, node.STUN.Error) } } }) @@ -111,7 +111,7 @@ func TestDERP(t *testing.T) { for _, node := range region.NodeReports { assert.True(t, node.Healthy) assert.True(t, node.CanExchangeMessages) - assert.Positive(t, node.RoundTripPing) + assert.NotEmpty(t, node.RoundTripPing) assert.Len(t, node.ClientLogs, 2) assert.Len(t, node.ClientLogs[0], 1) assert.Len(t, node.ClientErrs[0], 0) @@ -120,7 +120,7 @@ func TestDERP(t *testing.T) { assert.True(t, node.STUN.Enabled) assert.True(t, node.STUN.CanSTUN) - assert.NoError(t, node.STUN.Error) + assert.Nil(t, node.STUN.Error) } } }) @@ -174,7 +174,7 @@ func TestDERP(t *testing.T) { for _, node := range region.NodeReports { assert.False(t, node.Healthy) assert.True(t, node.CanExchangeMessages) - assert.Positive(t, node.RoundTripPing) + assert.NotEmpty(t, node.RoundTripPing) assert.Len(t, node.ClientLogs, 2) assert.Len(t, node.ClientLogs[0], 3) assert.Len(t, node.ClientLogs[1], 3) @@ -185,7 +185,7 @@ func TestDERP(t *testing.T) { assert.False(t, node.STUN.Enabled) assert.False(t, node.STUN.CanSTUN) - assert.NoError(t, node.STUN.Error) + assert.Nil(t, node.STUN.Error) } } }) @@ -227,7 +227,7 @@ func TestDERP(t *testing.T) { assert.True(t, node.STUN.Enabled) assert.True(t, node.STUN.CanSTUN) - assert.NoError(t, node.STUN.Error) + assert.Nil(t, node.STUN.Error) } } }) diff --git a/coderd/healthcheck/healthcheck.go b/coderd/healthcheck/healthcheck.go index dea09600ca9f6..29a4398b8391c 100644 --- a/coderd/healthcheck/healthcheck.go +++ b/coderd/healthcheck/healthcheck.go @@ -29,6 +29,7 @@ type Checker interface { Database(ctx context.Context, opts *DatabaseReportOptions) DatabaseReport } +// @typescript-generate Report type Report struct { // Time is the time the report was generated at. Time time.Time `json:"time"` diff --git a/coderd/healthcheck/websocket.go b/coderd/healthcheck/websocket.go index f2ab754f452d3..0b4a56e2d5ca9 100644 --- a/coderd/healthcheck/websocket.go +++ b/coderd/healthcheck/websocket.go @@ -2,6 +2,7 @@ package healthcheck import ( "context" + "fmt" "io" "net/http" "net/url" @@ -10,8 +11,6 @@ import ( "golang.org/x/xerrors" "nhooyr.io/websocket" - - "github.com/coder/coder/coderd/httpapi" ) type WebsocketReportOptions struct { @@ -20,15 +19,12 @@ type WebsocketReportOptions struct { HTTPClient *http.Client } +// @typescript-generate WebsocketReport type WebsocketReport struct { - Healthy bool `json:"healthy"` - Response WebsocketResponse `json:"response"` - Error *string `json:"error"` -} - -type WebsocketResponse struct { - Body string `json:"body"` - Code int `json:"code"` + Healthy bool `json:"healthy"` + Body string `json:"body"` + Code int `json:"code"` + Error *string `json:"error"` } func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions) { @@ -60,10 +56,8 @@ func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions) } } - r.Response = WebsocketResponse{ - Body: body, - Code: res.StatusCode, - } + r.Body = body + r.Code = res.StatusCode } if err != nil { r.Error = convertError(xerrors.Errorf("websocket dial: %w", err)) @@ -115,7 +109,8 @@ func (s *WebsocketEchoServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) ctx := r.Context() c, err := websocket.Accept(rw, r, &websocket.AcceptOptions{}) if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, "unable to accept: "+err.Error()) + rw.WriteHeader(http.StatusBadRequest) + _, _ = rw.Write([]byte(fmt.Sprint("unable to accept:", err))) return } defer c.Close(websocket.StatusGoingAway, "goodbye") diff --git a/coderd/healthcheck/websocket_test.go b/coderd/healthcheck/websocket_test.go index 45f8a4ae5c31c..cb56081197577 100644 --- a/coderd/healthcheck/websocket_test.go +++ b/coderd/healthcheck/websocket_test.go @@ -63,7 +63,7 @@ func TestWebsocket(t *testing.T) { }) require.NotNil(t, wsReport.Error) - assert.Equal(t, wsReport.Response.Body, "test error") - assert.Equal(t, wsReport.Response.Code, http.StatusBadRequest) + assert.Equal(t, wsReport.Body, "test error") + assert.Equal(t, wsReport.Code, http.StatusBadRequest) }) } diff --git a/docs/api/debug.md b/docs/api/debug.md index cd68322a674fc..e3382c6586504 100644 --- a/docs/api/debug.md +++ b/docs/api/debug.md @@ -51,7 +51,8 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "database": { "error": "string", "healthy": true, - "latency": 0, + "latency": "string", + "latency_ms": 0, "reachable": true }, "derp": { @@ -118,11 +119,12 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "tokenBucketBytesBurst": 0, "tokenBucketBytesPerSecond": 0 }, - "round_trip_ping": 0, + "round_trip_ping": "string", + "round_trip_ping_ms": 0, "stun": { "canSTUN": true, "enabled": true, - "error": null + "error": "string" }, "uses_websocket": true } @@ -179,11 +181,12 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "tokenBucketBytesBurst": 0, "tokenBucketBytesPerSecond": 0 }, - "round_trip_ping": 0, + "round_trip_ping": "string", + "round_trip_ping_ms": 0, "stun": { "canSTUN": true, "enabled": true, - "error": null + "error": "string" }, "uses_websocket": true } @@ -218,12 +221,10 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \ "healthy": true, "time": "string", "websocket": { + "body": "string", + "code": 0, "error": "string", - "healthy": true, - "response": { - "body": "string", - "code": 0 - } + "healthy": true } } ``` diff --git a/docs/api/schemas.md b/docs/api/schemas.md index b9d5e3d1b78a1..ee2f53655957a 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -6501,11 +6501,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "tokenBucketBytesBurst": 0, "tokenBucketBytesPerSecond": 0 }, - "round_trip_ping": 0, + "round_trip_ping": "string", + "round_trip_ping_ms": 0, "stun": { "canSTUN": true, "enabled": true, - "error": null + "error": "string" }, "uses_websocket": true } @@ -6522,7 +6523,8 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `healthy` | boolean | false | | | | `node` | [tailcfg.DERPNode](#tailcfgderpnode) | false | | | | `node_info` | [derp.ServerInfoMessage](#derpserverinfomessage) | false | | | -| `round_trip_ping` | integer | false | | | +| `round_trip_ping` | string | false | | | +| `round_trip_ping_ms` | integer | false | | | | `stun` | [healthcheck.DERPStunReport](#healthcheckderpstunreport) | false | | | | `uses_websocket` | boolean | false | | | @@ -6557,11 +6559,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "tokenBucketBytesBurst": 0, "tokenBucketBytesPerSecond": 0 }, - "round_trip_ping": 0, + "round_trip_ping": "string", + "round_trip_ping_ms": 0, "stun": { "canSTUN": true, "enabled": true, - "error": null + "error": "string" }, "uses_websocket": true } @@ -6668,11 +6671,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "tokenBucketBytesBurst": 0, "tokenBucketBytesPerSecond": 0 }, - "round_trip_ping": 0, + "round_trip_ping": "string", + "round_trip_ping_ms": 0, "stun": { "canSTUN": true, "enabled": true, - "error": null + "error": "string" }, "uses_websocket": true } @@ -6729,11 +6733,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "tokenBucketBytesBurst": 0, "tokenBucketBytesPerSecond": 0 }, - "round_trip_ping": 0, + "round_trip_ping": "string", + "round_trip_ping_ms": 0, "stun": { "canSTUN": true, "enabled": true, - "error": null + "error": "string" }, "uses_websocket": true } @@ -6784,7 +6789,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| { "canSTUN": true, "enabled": true, - "error": null + "error": "string" } ``` @@ -6794,7 +6799,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | --------- | ------- | -------- | ------------ | ----------- | | `canSTUN` | boolean | false | | | | `enabled` | boolean | false | | | -| `error` | any | false | | | +| `error` | string | false | | | ## healthcheck.DatabaseReport @@ -6802,19 +6807,21 @@ If the schedule is empty, the user will be updated to use the default schedule.| { "error": "string", "healthy": true, - "latency": 0, + "latency": "string", + "latency_ms": 0, "reachable": true } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ----------- | ------- | -------- | ------------ | ----------- | -| `error` | string | false | | | -| `healthy` | boolean | false | | | -| `latency` | integer | false | | | -| `reachable` | boolean | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------ | ------- | -------- | ------------ | ----------- | +| `error` | string | false | | | +| `healthy` | boolean | false | | | +| `latency` | string | false | | | +| `latency_ms` | integer | false | | | +| `reachable` | boolean | false | | | ## healthcheck.Report @@ -6832,7 +6839,8 @@ If the schedule is empty, the user will be updated to use the default schedule.| "database": { "error": "string", "healthy": true, - "latency": 0, + "latency": "string", + "latency_ms": 0, "reachable": true }, "derp": { @@ -6899,11 +6907,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "tokenBucketBytesBurst": 0, "tokenBucketBytesPerSecond": 0 }, - "round_trip_ping": 0, + "round_trip_ping": "string", + "round_trip_ping_ms": 0, "stun": { "canSTUN": true, "enabled": true, - "error": null + "error": "string" }, "uses_websocket": true } @@ -6960,11 +6969,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| "tokenBucketBytesBurst": 0, "tokenBucketBytesPerSecond": 0 }, - "round_trip_ping": 0, + "round_trip_ping": "string", + "round_trip_ping_ms": 0, "stun": { "canSTUN": true, "enabled": true, - "error": null + "error": "string" }, "uses_websocket": true } @@ -6999,12 +7009,10 @@ If the schedule is empty, the user will be updated to use the default schedule.| "healthy": true, "time": "string", "websocket": { + "body": "string", + "code": 0, "error": "string", - "healthy": true, - "response": { - "body": "string", - "code": 0 - } + "healthy": true } } ``` @@ -7024,40 +7032,23 @@ If the schedule is empty, the user will be updated to use the default schedule.| ## healthcheck.WebsocketReport -```json -{ - "error": "string", - "healthy": true, - "response": { - "body": "string", - "code": 0 - } -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ---------- | -------------------------------------------------------------- | -------- | ------------ | ----------- | -| `error` | string | false | | | -| `healthy` | boolean | false | | | -| `response` | [healthcheck.WebsocketResponse](#healthcheckwebsocketresponse) | false | | | - -## healthcheck.WebsocketResponse - ```json { "body": "string", - "code": 0 + "code": 0, + "error": "string", + "healthy": true } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ------ | ------- | -------- | ------------ | ----------- | -| `body` | string | false | | | -| `code` | integer | false | | | +| Name | Type | Required | Restrictions | Description | +| --------- | ------- | -------- | ------------ | ----------- | +| `body` | string | false | | | +| `code` | integer | false | | | +| `error` | string | false | | | +| `healthy` | boolean | false | | | ## netcheck.Report diff --git a/go.mod b/go.mod index 9f8d043812df2..61b8f203d50bb 100644 --- a/go.mod +++ b/go.mod @@ -356,7 +356,7 @@ require ( go.opentelemetry.io/otel/metric v1.16.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect - golang.org/x/text v0.11.0 // indirect + golang.org/x/text v0.11.0 golang.org/x/time v0.3.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde // indirect diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 53b64b9fd32c9..3cf4b1fe79c11 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -15,6 +15,8 @@ import ( "text/template" "github.com/fatih/structtag" + "golang.org/x/text/cases" + "golang.org/x/text/language" "golang.org/x/tools/go/packages" "golang.org/x/xerrors" @@ -23,21 +25,25 @@ import ( "github.com/coder/coder/coderd/util/slice" ) -const ( - baseDir = "./codersdk" - indent = " " +var ( + baseDirs = [...]string{"./codersdk", "./coderd/healthcheck"} + indent = " " ) func main() { ctx := context.Background() log := slog.Make(sloghuman.Sink(os.Stderr)) - output, err := Generate(baseDir) - if err != nil { - log.Fatal(ctx, err.Error()) - } + _, _ = fmt.Print("// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT.\n\n") + for _, baseDir := range baseDirs { + _, _ = fmt.Printf("// The code below is generated from %s.\n\n", strings.TrimPrefix(baseDir, "./")) + output, err := Generate(baseDir) + if err != nil { + log.Fatal(ctx, err.Error()) + } - // Just cat the output to a file to capture it - _, _ = fmt.Println(output) + // Just cat the output to a file to capture it + _, _ = fmt.Print(output, "\n\n") + } } func Generate(directory string) (string, error) { @@ -63,11 +69,6 @@ type TypescriptTypes struct { // String just combines all the codeblocks. func (t TypescriptTypes) String() string { var s strings.Builder - const prelude = ` -// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT. - -` - _, _ = s.WriteString(prelude) sortedTypes := make([]string, 0, len(t.Types)) sortedEnums := make([]string, 0, len(t.Enums)) @@ -176,6 +177,7 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) { Enums: make(map[string]types.Object), EnumConsts: make(map[string][]*types.Const), IgnoredTypes: make(map[string]struct{}), + AllowedTypes: make(map[string]struct{}), } // Look for comments that indicate to ignore a type for typescript generation. @@ -196,6 +198,24 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) { } } + // This allows opt-in generation, instead of opt-out. + allowRegex := regexp.MustCompile("@typescript-generate[:]?(?P.*)") + for _, file := range g.pkg.Syntax { + for _, comment := range file.Comments { + for _, line := range comment.List { + text := line.Text + matches := allowRegex.FindStringSubmatch(text) + allowed := allowRegex.SubexpIndex("allowed_types") + if len(matches) >= allowed && matches[allowed] != "" { + arr := strings.Split(matches[allowed], ",") + for _, s := range arr { + m.AllowedTypes[strings.TrimSpace(s)] = struct{}{} + } + } + } + } + } + for _, n := range g.pkg.Types.Scope().Names() { obj := g.pkg.Types.Scope().Lookup(n) err := g.generateOne(m, obj) @@ -260,6 +280,15 @@ type Maps struct { Enums map[string]types.Object EnumConsts map[string][]*types.Const IgnoredTypes map[string]struct{} + AllowedTypes map[string]struct{} +} + +// objName prepends the package name of a type if it is outside of codersdk. +func objName(obj types.Object) string { + if pkgName := obj.Pkg().Name(); pkgName != "codersdk" { + return cases.Title(language.English).String(pkgName) + obj.Name() + } + return obj.Name() } func (g *Generator) generateOne(m *Maps, obj types.Object) error { @@ -273,6 +302,13 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error { return nil } + // If we have allowed types, only allow those to be generated. + if _, ok := m.AllowedTypes[obj.Name()]; len(m.AllowedTypes) > 0 && !ok { + return nil + } + + objName := objName(obj) + switch obj := obj.(type) { // All named types are type declarations case *types.TypeName: @@ -286,13 +322,13 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error { // Structs are obvious. codeBlock, err := g.buildStruct(obj, underNamed) if err != nil { - return xerrors.Errorf("generate %q: %w", obj.Name(), err) + return xerrors.Errorf("generate %q: %w", objName, err) } - m.Structs[obj.Name()] = codeBlock + m.Structs[objName] = codeBlock case *types.Basic: // type string // These are enums. Store to expand later. - m.Enums[obj.Name()] = obj + m.Enums[objName] = obj case *types.Map, *types.Array, *types.Slice: // Declared maps that are not structs are still valid codersdk objects. // Handle them custom by calling 'typescriptType' directly instead of @@ -301,7 +337,7 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error { // These are **NOT** enums, as a map in Go would never be used for an enum. ts, err := g.typescriptType(obj.Type().Underlying()) if err != nil { - return xerrors.Errorf("(map) generate %q: %w", obj.Name(), err) + return xerrors.Errorf("(map) generate %q: %w", objName, err) } var str strings.Builder @@ -311,8 +347,8 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error { _, _ = str.WriteRune('\n') } // Use similar output syntax to enums. - _, _ = str.WriteString(fmt.Sprintf("export type %s = %s\n", obj.Name(), ts.ValueType)) - m.Structs[obj.Name()] = str.String() + _, _ = str.WriteString(fmt.Sprintf("export type %s = %s\n", objName, ts.ValueType)) + m.Structs[objName] = str.String() case *types.Interface: // Interfaces are used as generics. Non-generic interfaces are // not supported. @@ -330,9 +366,9 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error { block, err := g.buildUnion(obj, union) if err != nil { - return xerrors.Errorf("generate union %q: %w", obj.Name(), err) + return xerrors.Errorf("generate union %q: %w", objName, err) } - m.Generics[obj.Name()] = block + m.Generics[objName] = block } case *types.Signature: // Ignore named functions. @@ -353,7 +389,7 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error { case *types.Func: // Noop default: - _, _ = fmt.Println(obj.Name()) + _, _ = fmt.Println(objName) } return nil } @@ -361,7 +397,7 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error { func (g *Generator) posLine(obj types.Object) string { file := g.pkg.Fset.File(obj.Pos()) // Do not use filepath, as that changes behavior based on OS - return fmt.Sprintf("// From %s\n", path.Join("codersdk", filepath.Base(file.Name()))) + return fmt.Sprintf("// From %s\n", path.Join(obj.Pkg().Name(), filepath.Base(file.Name()))) } // buildStruct just prints the typescript def for a type. @@ -387,7 +423,7 @@ func (g *Generator) buildUnion(obj types.Object, st *types.Union) (string, error allTypes = slice.Unique(allTypes) - _, _ = s.WriteString(fmt.Sprintf("export type %s = %s\n", obj.Name(), strings.Join(allTypes, " | "))) + _, _ = s.WriteString(fmt.Sprintf("export type %s = %s\n", objName(obj), strings.Join(allTypes, " | "))) return s.String(), nil } @@ -421,7 +457,7 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err } state.PosLine = g.posLine(obj) - state.Name = obj.Name() + state.Name = objName(obj) // Handle named embedded structs in the codersdk package via extension. var extends []string @@ -453,6 +489,10 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err panic("invalid struct tags on type " + obj.String()) } + if !field.Exported() { + continue + } + // Use the json name if present jsonTag, err := tags.Get("json") var ( @@ -729,6 +769,8 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { case "time.Time": // We really should come up with a standard for time. return TypescriptType{ValueType: "string"}, nil + case "time.Duration": + return TypescriptType{ValueType: "number"}, nil case "database/sql.NullTime": return TypescriptType{ValueType: "string", Optional: true}, nil case "github.com/coder/coder/codersdk.NullTime": @@ -744,12 +786,13 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { } // Then see if the type is defined elsewhere. If it is, we can just - // put the name as it will be defined in the typescript codeblock + // put the objName as it will be defined in the typescript codeblock // we generate. - name := n.Obj().Name() + objName := objName(n.Obj()) genericName := "" genericTypes := make(map[string]string) - if obj := g.pkg.Types.Scope().Lookup(name); obj != nil { + pkgName := n.Obj().Pkg().Name() + if obj := g.pkg.Types.Scope().Lookup(n.Obj().Name()); g.pkg.Name == pkgName && obj != nil { // Sweet! Using other typescript types as fields. This could be an // enum or another struct if args := n.TypeArgs(); args != nil && args.Len() > 0 { @@ -758,7 +801,7 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { for i := 0; i < args.Len(); i++ { genType, err := g.typescriptType(args.At(i)) if err != nil { - return TypescriptType{}, xerrors.Errorf("generic field %q<%q>: %w", name, args.At(i).String(), err) + return TypescriptType{}, xerrors.Errorf("generic field %q<%q>: %w", objName, args.At(i).String(), err) } if param, ok := args.At(i).(*types.TypeParam); ok { @@ -773,13 +816,13 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { genericConstraints = append(genericConstraints, genType.ValueType) } - genericName = name + fmt.Sprintf("<%s>", strings.Join(genericNames, ", ")) - name += fmt.Sprintf("<%s>", strings.Join(genericConstraints, ", ")) + genericName = objName + fmt.Sprintf("<%s>", strings.Join(genericNames, ", ")) + objName += fmt.Sprintf("<%s>", strings.Join(genericConstraints, ", ")) } return TypescriptType{ GenericTypes: genericTypes, GenericValue: genericName, - ValueType: name, + ValueType: objName, }, nil } diff --git a/scripts/apitypings/testdata/enums/enums.go b/scripts/apitypings/testdata/enums/enums.go index dc153df934ce8..777a91441ab2f 100644 --- a/scripts/apitypings/testdata/enums/enums.go +++ b/scripts/apitypings/testdata/enums/enums.go @@ -1,4 +1,4 @@ -package enums +package codersdk type ( Enum string diff --git a/scripts/apitypings/testdata/enums/enums.ts b/scripts/apitypings/testdata/enums/enums.ts index bf9863da23dfe..2fc20f0e33d29 100644 --- a/scripts/apitypings/testdata/enums/enums.ts +++ b/scripts/apitypings/testdata/enums/enums.ts @@ -1,5 +1,3 @@ -// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT. - // From codersdk/enums.go export type Enums = Enum[] diff --git a/scripts/apitypings/testdata/genericmap/genericmap.go b/scripts/apitypings/testdata/genericmap/genericmap.go index ab9163b587a0a..721ba95313d58 100644 --- a/scripts/apitypings/testdata/genericmap/genericmap.go +++ b/scripts/apitypings/testdata/genericmap/genericmap.go @@ -1,4 +1,4 @@ -package genericmap +package codersdk type Foo struct { Bar string `json:"bar"` diff --git a/scripts/apitypings/testdata/genericmap/genericmap.ts b/scripts/apitypings/testdata/genericmap/genericmap.ts index 5489d03970ac7..9ceca8b44d706 100644 --- a/scripts/apitypings/testdata/genericmap/genericmap.ts +++ b/scripts/apitypings/testdata/genericmap/genericmap.ts @@ -1,5 +1,3 @@ -// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT. - // From codersdk/genericmap.go export interface Buzz { readonly foo: Foo diff --git a/scripts/apitypings/testdata/generics/generics.go b/scripts/apitypings/testdata/generics/generics.go index a842cb5693c9b..0d1d53e2869e2 100644 --- a/scripts/apitypings/testdata/generics/generics.go +++ b/scripts/apitypings/testdata/generics/generics.go @@ -1,4 +1,4 @@ -package generics +package codersdk import "time" @@ -10,26 +10,26 @@ type Custom interface { string | bool | int | time.Duration | []string | *int } -// StaticGeneric has all generic fields defined in the field -type StaticGeneric struct { - Static GenericFields[string, int, time.Duration, string] `json:"static"` +// Static has all generic fields defined in the field +type Static struct { + Static Fields[string, int, time.Duration, string] `json:"static"` } -// DynamicGeneric can has some dynamic fields -type DynamicGeneric[A any, S Single] struct { - Dynamic GenericFields[bool, A, string, S] `json:"dynamic"` - Comparable bool `json:"comparable"` +// Dynamic has some dynamic fields. +type Dynamic[A any, S Single] struct { + Dynamic Fields[bool, A, string, S] `json:"dynamic"` + Comparable bool `json:"comparable"` } -type ComplexGeneric[C comparable, S Single, T Custom] struct { - Dynamic GenericFields[C, bool, string, S] `json:"dynamic"` - Order GenericFieldsDiffOrder[C, string, S, T] `json:"order"` - Comparable C `json:"comparable"` - Single S `json:"single"` - Static StaticGeneric `json:"static"` +type Complex[C comparable, S Single, T Custom] struct { + Dynamic Fields[C, bool, string, S] `json:"dynamic"` + Order FieldsDiffOrder[C, string, S, T] `json:"order"` + Comparable C `json:"comparable"` + Single S `json:"single"` + Static Static `json:"static"` } -type GenericFields[C comparable, A any, T Custom, S Single] struct { +type Fields[C comparable, A any, T Custom, S Single] struct { Comparable C `json:"comparable"` Any A `json:"any"` @@ -38,6 +38,6 @@ type GenericFields[C comparable, A any, T Custom, S Single] struct { SingleConstraint S `json:"single_constraint"` } -type GenericFieldsDiffOrder[A any, C comparable, S Single, T Custom] struct { - GenericFields[C, A, T, S] +type FieldsDiffOrder[A any, C comparable, S Single, T Custom] struct { + Fields Fields[C, A, T, S] } diff --git a/scripts/apitypings/testdata/generics/generics.ts b/scripts/apitypings/testdata/generics/generics.ts index ce851f0cc6ff5..57cfdb7bc5fde 100644 --- a/scripts/apitypings/testdata/generics/generics.ts +++ b/scripts/apitypings/testdata/generics/generics.ts @@ -1,22 +1,20 @@ -// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT. - // From codersdk/generics.go -export interface ComplexGeneric { - readonly dynamic: GenericFields - readonly order: GenericFieldsDiffOrder +export interface Complex { + readonly dynamic: Fields + readonly order: FieldsDiffOrder readonly comparable: C readonly single: S - readonly static: StaticGeneric + readonly static: Static } // From codersdk/generics.go -export interface DynamicGeneric { - readonly dynamic: GenericFields +export interface Dynamic { + readonly dynamic: Fields readonly comparable: boolean } // From codersdk/generics.go -export interface GenericFields { +export interface Fields { readonly comparable: C readonly any: A readonly custom: T @@ -25,13 +23,13 @@ export interface GenericFields { - readonly GenericFields: GenericFields +export interface FieldsDiffOrder { + readonly Fields: Fields } // From codersdk/generics.go -export interface StaticGeneric { - readonly static: GenericFields +export interface Static { + readonly static: Fields } // From codersdk/generics.go diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index e374e46e192f1..886f95e06bbf1 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1,5 +1,7 @@ // Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT. +// The code below is generated from codersdk. + // From codersdk/templates.go export interface ACLAvailable { readonly users: User[] @@ -234,7 +236,6 @@ export interface CreateTestAuditLogRequest { // From codersdk/apikey.go export interface CreateTokenRequest { - // This is likely an enum in an external package ("time.Duration") readonly lifetime: number readonly scope: APIKeyScope readonly token_name: string @@ -1056,7 +1057,6 @@ export interface TemplateVersionsByTemplateRequest extends Pagination { // From codersdk/apikey.go export interface TokenConfig { - // This is likely an enum in an external package ("time.Duration") readonly max_token_lifetime: number } @@ -1908,3 +1908,92 @@ export const WorkspaceTransitions: WorkspaceTransition[] = [ // From codersdk/workspaceproxy.go export type RegionTypes = Region | WorkspaceProxy + +// The code below is generated from coderd/healthcheck. + +// From healthcheck/accessurl.go +export interface HealthcheckAccessURLReport { + readonly access_url: string + readonly healthy: boolean + readonly reachable: boolean + readonly status_code: number + readonly healthz_response: string + readonly error?: string +} + +// From healthcheck/derp.go +export interface HealthcheckDERPNodeReport { + readonly healthy: boolean + // Named type "tailscale.com/tailcfg.DERPNode" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly node?: any + // Named type "tailscale.com/derp.ServerInfoMessage" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly node_info: any + readonly can_exchange_messages: boolean + readonly round_trip_ping: string + readonly round_trip_ping_ms: number + readonly uses_websocket: boolean + readonly client_logs: string[][] + readonly client_errs: string[][] + readonly error?: string + readonly stun: HealthcheckDERPStunReport +} + +// From healthcheck/derp.go +export interface HealthcheckDERPRegionReport { + readonly healthy: boolean + // Named type "tailscale.com/tailcfg.DERPRegion" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly region?: any + readonly node_reports: HealthcheckDERPNodeReport[] + readonly error?: string +} + +// From healthcheck/derp.go +export interface HealthcheckDERPReport { + readonly healthy: boolean + readonly regions: Record + // Named type "tailscale.com/net/netcheck.Report" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly netcheck?: any + readonly netcheck_err?: string + readonly netcheck_logs: string[] + readonly error?: string +} + +// From healthcheck/derp.go +export interface HealthcheckDERPStunReport { + readonly Enabled: boolean + readonly CanSTUN: boolean + readonly Error?: string +} + +// From healthcheck/database.go +export interface HealthcheckDatabaseReport { + readonly healthy: boolean + readonly reachable: boolean + readonly latency: string + readonly latency_ms: number + readonly error?: string +} + +// From healthcheck/healthcheck.go +export interface HealthcheckReport { + readonly time: string + readonly healthy: boolean + readonly failing_sections: string[] + readonly derp: HealthcheckDERPReport + readonly access_url: HealthcheckAccessURLReport + readonly websocket: HealthcheckWebsocketReport + readonly database: HealthcheckDatabaseReport + readonly coder_version: string +} + +// From healthcheck/websocket.go +export interface HealthcheckWebsocketReport { + readonly healthy: boolean + readonly body: string + readonly code: number + readonly error?: string +}