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
10 changes: 4 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,15 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e
github.com/gorilla/mux v1.8.1
github.com/jackc/pgx/v4 v4.18.3
github.com/jackc/pgx/v5 v5.6.0
github.com/joho/godotenv v1.5.1
github.com/mattn/go-isatty v0.0.20
github.com/nitrictech/nitric/cloud/common v0.0.0-20231206014944-68e146f4f69a
github.com/olahol/melody v1.1.3
github.com/robfig/cron/v3 v3.0.1
github.com/samber/lo v1.38.1
github.com/spf13/afero v1.11.0
github.com/wk8/go-ordered-map/v2 v2.1.8
go.etcd.io/bbolt v1.3.6
golang.org/x/sync v0.6.0
google.golang.org/protobuf v1.32.0
Expand Down Expand Up @@ -86,13 +87,15 @@ require (
github.com/ashanbrown/makezero v1.1.1 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bkielbasa/cyclop v1.2.1 // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v4 v4.2.0 // indirect
github.com/breml/bidichk v0.2.7 // indirect
github.com/breml/errchkjson v0.3.6 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/butuzov/ireturn v0.3.0 // indirect
github.com/butuzov/mirror v1.1.0 // indirect
github.com/catenacyber/perfsprint v0.6.0 // indirect
Expand Down Expand Up @@ -159,13 +162,8 @@ require (
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/invopop/yaml v0.1.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jgautheron/goconst v1.7.0 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect
Expand Down
119 changes: 10 additions & 109 deletions go.sum

Large diffs are not rendered by default.

176 changes: 175 additions & 1 deletion pkg/cloud/sql/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,23 @@ package sql

import (
"context"
"encoding/hex"
"fmt"
"log"
"net"
"net/netip"
"strings"
"time"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/volume"
"github.com/docker/go-connections/nat"
"github.com/jackc/pgx/v4"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
orderedmap "github.com/wk8/go-ordered-map/v2"

"github.com/nitrictech/cli/pkg/docker"
"github.com/nitrictech/cli/pkg/netx"
Expand Down Expand Up @@ -169,6 +175,64 @@ func (l *LocalSqlServer) ConnectionString(ctx context.Context, req *sqlpb.SqlCon
}, nil
}

// create a function that will execute a query on the local database
func (l *LocalSqlServer) Query(ctx context.Context, connectionString string, query string) ([]*orderedmap.OrderedMap[string, any], error) {
// Connect to the PostgreSQL instance using the provided connection string
conn, err := pgx.Connect(ctx, connectionString)
if err != nil {
return nil, err
}

defer conn.Close(ctx)

// Begin transaction
tx, err := conn.Begin(ctx)
if err != nil {
return nil, err
}

// Split commands from string
commands := strings.Split(query, ";")

results := []*orderedmap.OrderedMap[string, any]{}

// Execute each command
for _, command := range commands {
command = strings.TrimSpace(command)
if command == "" {
continue
}

rows, err := tx.Query(ctx, command)
if err != nil {
_ = tx.Rollback(ctx)

return nil, err
}

if rows.Next() {
// Process the query results
results, err = processRows(rows)
rows.Close()

if err != nil {
_ = tx.Rollback(ctx)

return nil, err
}
} else {
rows.Close()
}
}

// Commit the transaction
if err := tx.Commit(ctx); err != nil {
return nil, err
}

return results, nil
}

func NewLocalSqlServer(projectName string) (*LocalSqlServer, error) {
localSql := &LocalSqlServer{
projectName: projectName,
Expand All @@ -181,3 +245,113 @@ func NewLocalSqlServer(projectName string) (*LocalSqlServer, error) {

return localSql, nil
}

func processRows(rows pgx.Rows) ([]*orderedmap.OrderedMap[string, any], error) {
fieldDescriptions := rows.FieldDescriptions()
numColumns := len(fieldDescriptions)

results := []*orderedmap.OrderedMap[string, any]{}

for {
values := make([]interface{}, numColumns)
valuePointers := make([]interface{}, numColumns)

for i := range values {
valuePointers[i] = &values[i]
}

err := rows.Scan(valuePointers...)
if err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}

row := orderedmap.New[string, any]()

for i, val := range values {
// format values if necessary
switch v := val.(type) {
case time.Time:
if v.UTC().Hour() == 0 && v.UTC().Minute() == 0 && v.UTC().Second() == 0 {
val = v.Format("2006-01-02")
} else {
val = v.Format("2006-01-02 15:04:05")
}
case netip.Prefix:
val = v.Addr().String()
case net.HardwareAddr:
val = v.String()
case pgtype.Interval:
val = formatInterval(v)
case pgtype.Bits:
var result string
for _, b := range v.Bytes {
result += fmt.Sprintf("%08b", b)
}

val = result
case []uint8:
val = fmt.Sprintf("\\x%s", hex.EncodeToString(v))
case [16]uint8:
u, err := uuid.FromBytes(v[:])
if err != nil {
return nil, fmt.Errorf("failed to parse UUID: %w", err)
}

val = u.String()
}

row.Set(fieldDescriptions[i].Name, val)
}

results = append(results, row)

if !rows.Next() {
break
}
}

if rows.Err() != nil {
return nil, fmt.Errorf("row iteration failed: %w", rows.Err())
}

return results, nil
}

func formatInterval(interval pgtype.Interval) string {
years := interval.Months / 12
months := interval.Months % 12
days := interval.Days

// Calculate hours, minutes, and seconds from microseconds
totalSeconds := interval.Microseconds / 1e6
hours := totalSeconds / 3600
minutes := (totalSeconds % 3600) / 60
seconds := totalSeconds % 60

parts := []string{}
if years != 0 {
parts = append(parts, fmt.Sprintf("%d year%s", years, pluralSuffix(years)))
}

if months != 0 {
parts = append(parts, fmt.Sprintf("%d mon%s", months, pluralSuffix(months)))
}

if days != 0 {
parts = append(parts, fmt.Sprintf("%d day%s", days, pluralSuffix(days)))
}

if hours != 0 || minutes != 0 || seconds != 0 {
parts = append(parts, fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds))
}

return strings.Join(parts, " ")
}

func pluralSuffix(value int32) string {
if value == 1 {
return ""
}

return "s"
}
32 changes: 29 additions & 3 deletions pkg/dashboard/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package dashboard

import (
"context"
"embed"
"encoding/json"
"fmt"
Expand All @@ -39,13 +40,15 @@ import (
"github.com/nitrictech/cli/pkg/collector"
"github.com/nitrictech/cli/pkg/netx"
resourcespb "github.com/nitrictech/nitric/core/pkg/proto/resources/v1"
sqlpb "github.com/nitrictech/nitric/core/pkg/proto/sql/v1"
websocketspb "github.com/nitrictech/nitric/core/pkg/proto/websockets/v1"

"github.com/nitrictech/cli/pkg/cloud/apis"
"github.com/nitrictech/cli/pkg/cloud/gateway"
httpproxy "github.com/nitrictech/cli/pkg/cloud/http"
"github.com/nitrictech/cli/pkg/cloud/resources"
"github.com/nitrictech/cli/pkg/cloud/schedules"
"github.com/nitrictech/cli/pkg/cloud/sql"
"github.com/nitrictech/cli/pkg/cloud/storage"
"github.com/nitrictech/cli/pkg/cloud/topics"
"github.com/nitrictech/cli/pkg/cloud/websockets"
Expand Down Expand Up @@ -96,6 +99,8 @@ type KeyValueSpec struct {

type SQLDatabaseSpec struct {
*BaseResourceSpec

ConnectionString string `json:"connectionString"`
}

type NotifierSpec struct {
Expand Down Expand Up @@ -137,6 +142,7 @@ type Dashboard struct {
project *project.Project
storageService *storage.LocalStorageService
gatewayService *gateway.LocalGatewayService
databaseService *sql.LocalSqlServer
apis []ApiSpec
apiUseHttps bool
apiSecurityDefinitions map[string]map[string]*resourcespb.ApiSecurityDefinitionResource
Expand Down Expand Up @@ -190,6 +196,7 @@ type DashboardResponse struct {
CurrentVersion string `json:"currentVersion"`
LatestVersion string `json:"latestVersion"`
Connected bool `json:"connected"`
DashboardAddress string `json:"dashboardAddress"`
}

type Bucket struct {
Expand Down Expand Up @@ -341,15 +348,30 @@ func (d *Dashboard) updateResources(lrs resources.LocalResourcesState) {
})

if !exists {
connectionString, err := d.databaseService.ConnectionString(context.TODO(), &sqlpb.SqlConnectionStringRequest{
DatabaseName: dbName,
})
if err != nil {
fmt.Printf("Error getting connection string for database %s: %v\n", dbName, err)
continue
}

d.sqlDatabases = append(d.sqlDatabases, &SQLDatabaseSpec{
BaseResourceSpec: &BaseResourceSpec{
Name: dbName,
RequestingServices: resource.RequestingServices,
},
ConnectionString: connectionString.GetConnectionString(),
})
}
}

if len(d.sqlDatabases) > 0 {
slices.SortFunc(d.sqlDatabases, func(a, b *SQLDatabaseSpec) int {
return compare(a.Name, b.Name)
})
}

d.refresh()
}

Expand Down Expand Up @@ -605,6 +627,8 @@ func (d *Dashboard) Start() error {

http.HandleFunc("/api/storage", d.handleStorage())

http.HandleFunc("/api/sql", d.createSqlQueryHandler())

// handle websockets
http.HandleFunc("/ws-info", func(w http.ResponseWriter, r *http.Request) {
err := d.wsWebSocket.HandleRequest(w, r)
Expand Down Expand Up @@ -706,9 +730,10 @@ func (d *Dashboard) sendStackUpdate() error {
HttpWorkerAddresses: d.gatewayService.GetHttpWorkerAddresses(),
TriggerAddress: d.gatewayService.GetTriggerAddress(),
// StorageAddress: d.storageService.GetStorageEndpoint(),
CurrentVersion: currentVersion,
LatestVersion: latestVersion,
Connected: d.isConnected(),
CurrentVersion: currentVersion,
LatestVersion: latestVersion,
Connected: d.isConnected(),
DashboardAddress: d.GetDashboardUrl(),
}

// Encode the response as JSON
Expand Down Expand Up @@ -764,6 +789,7 @@ func New(noBrowser bool, localCloud *cloud.LocalCloud, project *project.Project)
project: project,
storageService: localCloud.Storage,
gatewayService: localCloud.Gateway,
databaseService: localCloud.Databases,
apis: []ApiSpec{},
apiUseHttps: localCloud.Gateway.ApiTlsCredentials != nil,
apiSecurityDefinitions: map[string]map[string]*resourcespb.ApiSecurityDefinitionResource{},
Expand Down
1 change: 1 addition & 0 deletions pkg/dashboard/frontend/cypress/e2e/a11y.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ describe('a11y test suite', () => {
'/',
'/schedules',
'/storage',
'/databases',
'/topics',
'/not-found',
'/architecture',
Expand Down
5 changes: 4 additions & 1 deletion pkg/dashboard/frontend/cypress/e2e/architecture.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ const expectedNodes = [
'subscribe-tests',
'subscribe-tests-2',
':8000',
'functions/my-test-function.ts',
'my-db',
'my-second-db',
'services/my-test-service.ts',
'services/my-test-db.ts',
]

describe('Architecture Spec', () => {
Expand Down
Loading