feat: Add auth flow (#146)

This adds a simple way to incorporate a centralized authentication flow.

The idea is to have the user, API and SSH (for machine authentication)
all in one place using `openauthjs` + `SST`

We also have a database now :)

> We are using InstantDB as it allows us to authenticate a use with just
the email. Plus it is super simple simple to use _of course after the
initial fumbles trying to design the db and relationships_
This commit is contained in:
Wanjohi
2025-01-04 00:02:28 +03:00
committed by GitHub
parent 33895974a7
commit fc5a755408
136 changed files with 3512 additions and 1914 deletions

50
packages/cli/cmd/root.go Normal file
View File

@@ -0,0 +1,50 @@
package cmd
import (
"runtime/debug"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "nestri",
Short: "A CLI tool to run and manage your self-hosted cloud gaming service",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() error {
err := rootCmd.Execute()
return err
}
var (
// Version stores the build version of VHS at the time of package through
// -ldflags.
//
// go build -ldflags "-s -w -X=main.Version=$(VERSION)"
Version string
// CommitSHA stores the git commit SHA at the time of package through -ldflags.
CommitSHA string
)
func init() {
rootCmd.AddCommand(runCmd)
if len(CommitSHA) >= 7 { //nolint:gomnd
vt := rootCmd.VersionTemplate()
rootCmd.SetVersionTemplate(vt[:len(vt)-1] + " (" + CommitSHA[0:7] + ")\n")
}
if Version == "" {
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
Version = info.Main.Version
} else {
Version = "unknown (built from source)"
}
}
rootCmd.Version = Version
}

26
packages/cli/cmd/run.go Normal file
View File

@@ -0,0 +1,26 @@
package cmd
import (
"nestrilabs/cli/internal/auth"
"github.com/charmbracelet/log"
"github.com/spf13/cobra"
)
var runCmd = &cobra.Command{
Use: "run",
Short: "Run a new Nestri node",
Long: "Create and run a new Nestri node from this machine",
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
credentials, err := auth.FetchUserCredentials()
if err != nil {
return err
}
log.Info("Credentials", "access_token", credentials.AccessToken)
log.Info("Credentials", "refresh_token", credentials.RefreshToken)
return nil
},
}

54
packages/cli/go.mod Normal file
View File

@@ -0,0 +1,54 @@
module nestrilabs/cli
go 1.23.3
require (
github.com/charmbracelet/log v0.4.0
github.com/docker/docker v27.4.1+incompatible
github.com/gorilla/websocket v1.5.3
github.com/nestrilabs/nestri-go-sdk v0.1.0-alpha.3
github.com/spf13/cobra v1.8.1
)
require (
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v0.13.0 // indirect
github.com/charmbracelet/x/ansi v0.2.3 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/otel v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect
go.opentelemetry.io/otel/metric v1.33.0 // indirect
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/time v0.8.0 // indirect
gotest.tools/v3 v3.5.1 // indirect
)

169
packages/cli/go.sum Normal file
View File

@@ -0,0 +1,169 @@
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=
github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4=
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
github.com/nestrilabs/nestri-go-sdk v0.1.0-alpha.3 h1:IqtLHbOF3y/SD3riYYKauQKj9dpqU7uuEExqL5zQ390=
github.com/nestrilabs/nestri-go-sdk v0.1.0-alpha.3/go.mod h1:b4AuAQSxfqtAzu4ie0Q+NOVNF9YUZTyP4XnxK0ZN05U=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=

View File

@@ -0,0 +1,26 @@
package api
import (
"context"
"fmt"
"nestrilabs/cli/internal/resource"
"github.com/nestrilabs/nestri-go-sdk"
"github.com/nestrilabs/nestri-go-sdk/option"
)
func RegisterMachine(token string) {
client := nestri.NewClient(
option.WithBearerToken(token),
option.WithBaseURL(resource.Resource.Api.Url),
)
machine, err := client.Machines.New(
context.TODO(),
nestri.MachineNewParams{})
if err != nil {
panic(err.Error())
}
fmt.Printf("%+v\n", machine.Data)
}

View File

@@ -0,0 +1,44 @@
package auth
import (
"encoding/json"
"fmt"
"io"
"nestrilabs/cli/internal/machine"
"nestrilabs/cli/internal/resource"
"net/http"
"net/url"
)
type UserCredentials struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
func FetchUserCredentials() (*UserCredentials, error) {
m := machine.NewMachine()
fingerprint := m.GetMachineID()
data := url.Values{}
data.Set("grant_type", "client_credentials")
data.Set("client_id", "device")
data.Set("client_secret", resource.Resource.AuthFingerprintKey.Value)
data.Set("hostname", m.Hostname)
data.Set("fingerprint", fingerprint)
data.Set("provider", "device")
resp, err := http.PostForm(resource.Resource.Auth.Url+"/token", data)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
return nil, fmt.Errorf("failed to auth: " + string(body))
}
credentials := UserCredentials{}
err = json.NewDecoder(resp.Body).Decode(&credentials)
if err != nil {
return nil, err
}
return &credentials, nil
}

View File

@@ -0,0 +1,202 @@
package machine
import (
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"github.com/charmbracelet/log"
)
type Machine struct {
OperatingSystem string
Arch string
Kernel string
Virtualization string
Hostname string
}
func NewMachine() *Machine {
var OS string
var architecture string
var kernel string
var virtualisation string
var hostname string
output, _ := exec.Command("hostnamectl", "status").Output()
os := regexp.MustCompile(`Operating System:\s+(.*)`)
matchingOS := os.FindStringSubmatch(string(output))
if len(matchingOS) > 1 {
OS = matchingOS[1]
}
arch := regexp.MustCompile(`Architecture:\s+(\w+)`)
matchingArch := arch.FindStringSubmatch(string(output))
if len(matchingArch) > 1 {
architecture = matchingArch[1]
}
kern := regexp.MustCompile(`Kernel:\s+(.*)`)
matchingKernel := kern.FindStringSubmatch(string(output))
if len(matchingKernel) > 1 {
kernel = matchingKernel[1]
}
virt := regexp.MustCompile(`Virtualization:\s+(\w+)`)
matchingVirt := virt.FindStringSubmatch(string(output))
if len(matchingVirt) > 1 {
virtualisation = matchingVirt[1]
}
host := regexp.MustCompile(`Static hostname:\s+(.*)`)
matchingHost := host.FindStringSubmatch(string(output))
if len(matchingHost) > 1 {
hostname = matchingHost[1]
}
return &Machine{
OperatingSystem: OS,
Arch: architecture,
Kernel: kernel,
Virtualization: virtualisation,
Hostname: hostname,
}
}
func (m *Machine) GetOS() string {
if m.OperatingSystem != "" {
return m.OperatingSystem
}
return "unknown"
}
func (m *Machine) GetArchitecture() string {
if m.Arch != "" {
return m.Arch
}
return "unknown"
}
func (m *Machine) GetKernel() string {
if m.Kernel != "" {
return m.Kernel
}
return "unknown"
}
func (m *Machine) GetVirtualization() string {
if m.Virtualization != "" {
return m.Virtualization
}
return "none"
}
func (m *Machine) GetHostname() string {
if m.Hostname != "" {
return m.Hostname
}
return "unknown"
}
func (m *Machine) GetMachineID() string {
id, err := os.ReadFile("/etc/machine-id")
if err != nil {
log.Error("Error getting your machine's ID", "err", err)
os.Exit(1)
}
return strings.TrimSpace(string(id))
}
func (m *Machine) GPUInfo() (string, string, error) {
// The command for GPU information varies depending on the system and drivers.
// lshw is a good general-purpose tool, but might need adjustments for specific hardware.
output, err := exec.Command("lshw", "-C", "display").Output()
if err != nil {
return "", "", fmt.Errorf("failed to get GPU information: %w", err)
}
gpuType := ""
gpuSize := ""
// Regular expressions for extracting product and size information. These might need to be
// adapted based on the output of lshw on your specific system.
typeRegex := regexp.MustCompile(`product:\s+(.*)`)
sizeRegex := regexp.MustCompile(`size:\s+(\d+MiB)`) // Example: extracts size in MiB
typeMatch := typeRegex.FindStringSubmatch(string(output))
if len(typeMatch) > 1 {
gpuType = typeMatch[1]
}
sizeMatch := sizeRegex.FindStringSubmatch(string(output))
if len(sizeMatch) > 1 {
gpuSize = sizeMatch[1]
}
if gpuType == "" && gpuSize == "" {
return "", "", fmt.Errorf("could not parse GPU information using lshw")
}
return gpuType, gpuSize, nil
}
func (m *Machine) GetCPUInfo() (string, string, error) {
output, err := exec.Command("lscpu").Output()
if err != nil {
return "", "", fmt.Errorf("failed to get CPU information: %w", err)
}
cpuType := ""
cpuSize := "" // This will store the number of cores
typeRegex := regexp.MustCompile(`Model name:\s+(.*)`)
coresRegex := regexp.MustCompile(`CPU\(s\):\s+(\d+)`)
typeMatch := typeRegex.FindStringSubmatch(string(output))
if len(typeMatch) > 1 {
cpuType = typeMatch[1]
}
coresMatch := coresRegex.FindStringSubmatch(string(output))
if len(coresMatch) > 1 {
cpuSize = coresMatch[1]
}
if cpuType == "" && cpuSize == "" {
return "", "", fmt.Errorf("could not parse CPU information using lscpu")
}
return cpuType, cpuSize, nil
}
func (m *Machine) GetRAMSize() (string, error) {
output, err := exec.Command("free", "-h", "--si").Output() // Using -h for human-readable and --si for base-10 units
if err != nil {
return "", fmt.Errorf("failed to get RAM information: %w", err)
}
ramSize := ""
ramRegex := regexp.MustCompile(`Mem:\s+(\S+)`) // Matches the total memory size
ramMatch := ramRegex.FindStringSubmatch(string(output))
if len(ramMatch) > 1 {
ramSize = ramMatch[1]
} else {
return "", fmt.Errorf("could not parse RAM information from free command")
}
return ramSize, nil
}
// func cleanString(s string) string {
// s = strings.ToLower(s)
// reg := regexp.MustCompile("[^a-z0-9]+") // Matches one or more non-alphanumeric characters
// return reg.ReplaceAllString(s, "")
// }

View File

@@ -0,0 +1,112 @@
package party
import (
"fmt"
"nestrilabs/cli/internal/machine"
"net/url"
"time"
"github.com/charmbracelet/log"
"github.com/gorilla/websocket"
)
const (
// Initial retry delay
initialRetryDelay = 1 * time.Second
// Maximum retry delay
maxRetryDelay = 30 * time.Second
// Factor to increase delay by after each attempt
backoffFactor = 2
)
type Party struct {
// Channel to signal shutdown
done chan struct{}
fingerprint string
hostname string
}
func NewParty() *Party {
m := machine.NewMachine()
fingerpint := m.GetMachineID()
return &Party{
done: make(chan struct{}),
fingerprint: fingerpint,
hostname: m.Hostname,
}
}
// Shutdown gracefully closes the connection
func (p *Party) Shutdown() {
close(p.done)
}
func (p *Party) Connect() {
baseURL := fmt.Sprintf("ws://localhost:1999/parties/main/%s", p.fingerprint)
params := url.Values{}
params.Add("_pk", p.hostname)
wsURL := baseURL + "?" + params.Encode()
retryDelay := initialRetryDelay
for {
select {
case <-p.done:
log.Info("Shutting down connection")
return
default:
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
log.Error("Failed to connect to party server", "err", err)
time.Sleep(retryDelay)
// Increase retry delay exponentially, but cap it
retryDelay = time.Duration(float64(retryDelay) * backoffFactor)
if retryDelay > maxRetryDelay {
retryDelay = maxRetryDelay
}
continue
}
// Reset retry delay on successful connection
retryDelay = initialRetryDelay
// Handle connection in a separate goroutine
connectionClosed := make(chan struct{})
go func() {
defer close(connectionClosed)
defer conn.Close()
// Send initial message
if err := conn.WriteMessage(websocket.TextMessage, []byte("hello there")); err != nil {
log.Error("Failed to send initial message", "err", err)
return
}
// Read messages loop
for {
select {
case <-p.done:
return
default:
_, message, err := conn.ReadMessage()
if err != nil {
log.Error("Error reading message", "err", err)
return
}
log.Info("Received message from party server", "message", string(message))
}
}
}()
// Wait for either connection to close or shutdown signal
select {
case <-connectionClosed:
log.Warn("Connection closed, attempting to reconnect...")
time.Sleep(retryDelay)
case <-p.done:
log.Info("Shutting down connection")
return
}
}
}
}

View File

@@ -0,0 +1,125 @@
package party
import (
"encoding/json"
"fmt"
"nestrilabs/cli/internal/machine"
"net/url"
"time"
"github.com/charmbracelet/log"
"github.com/gorilla/websocket"
)
// RetryConfig holds configuration for retry behavior
type RetryConfig struct {
InitialDelay time.Duration
MaxDelay time.Duration
BackoffFactor float64
MaxAttempts int // use 0 for infinite retries
}
// DefaultRetryConfig provides sensible default values
var DefaultRetryConfig = RetryConfig{
InitialDelay: time.Second,
MaxDelay: 30 * time.Second,
BackoffFactor: 2.0,
MaxAttempts: 0, // infinite retries
}
// RetryFunc is a function that will be retried
type RetryFunc[T any] func() (T, error)
// Retry executes the given function with retries based on the config
func Retry[T any](config RetryConfig, operation RetryFunc[T]) (T, error) {
var result T
currentDelay := config.InitialDelay
attempts := 0
for {
if config.MaxAttempts > 0 && attempts >= config.MaxAttempts {
return result, fmt.Errorf("max retry attempts (%d) exceeded", config.MaxAttempts)
}
result, err := operation()
if err == nil {
return result, nil
}
log.Warn("Operation failed, retrying...",
"attempt", attempts+1,
"delay", currentDelay,
"error", err)
time.Sleep(currentDelay)
// Increase delay for next attempt
currentDelay = time.Duration(float64(currentDelay) * config.BackoffFactor)
if currentDelay > config.MaxDelay {
currentDelay = config.MaxDelay
}
attempts++
}
}
// MessageHandler processes a message and returns true if it's the expected type
type MessageHandler[T any] func(msg T) bool
type TypeListener[T any] struct {
retryConfig RetryConfig
handler MessageHandler[T]
fingerprint string
hostname string
}
func NewTypeListener[T any](handler MessageHandler[T]) *TypeListener[T] {
m := machine.NewMachine()
fingerprint := m.GetMachineID()
return &TypeListener[T]{
retryConfig: DefaultRetryConfig,
handler: handler,
fingerprint: fingerprint,
hostname: m.Hostname,
}
}
// SetRetryConfig allows customizing the retry behavior
func (t *TypeListener[T]) SetRetryConfig(config RetryConfig) {
t.retryConfig = config
}
func (t *TypeListener[T]) ConnectUntilMessage() (T, error) {
baseURL := fmt.Sprintf("ws://localhost:1999/parties/main/%s", t.fingerprint)
params := url.Values{}
params.Add("_pk", t.hostname)
wsURL := baseURL + "?" + params.Encode()
return Retry(t.retryConfig, func() (T, error) {
var result T
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
return result, fmt.Errorf("connection failed: %w", err)
}
defer conn.Close()
// Read messages until we get the one we want
for {
_, message, err := conn.ReadMessage()
if err != nil {
return result, fmt.Errorf("read error: %w", err)
}
if err := json.Unmarshal(message, &result); err != nil {
// log.Error("Failed to unmarshal message", "err", err)
continue
}
if t.handler(result) {
return result, nil
}
}
})
}

View File

@@ -0,0 +1,38 @@
package resource
import (
"encoding/json"
"fmt"
"os"
"reflect"
)
type resource struct {
Api struct {
Url string `json:"url"`
}
Auth struct {
Url string `json:"url"`
}
AuthFingerprintKey struct {
Value string `json:"value"`
}
}
var Resource resource
func init() {
val := reflect.ValueOf(&Resource).Elem()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
typeField := val.Type().Field(i)
envVarName := fmt.Sprintf("SST_RESOURCE_%s", typeField.Name)
envValue, exists := os.LookupEnv(envVarName)
if !exists {
panic(fmt.Sprintf("Environment variable %s is required", envVarName))
}
if err := json.Unmarshal([]byte(envValue), field.Addr().Interface()); err != nil {
panic(err)
}
}
}

View File

@@ -0,0 +1,286 @@
package session
import (
"context"
"fmt"
"io"
"os"
"strings"
"sync"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
)
// GPUType represents the type of GPU available
type GPUType int
const (
GPUNone GPUType = iota
GPUNvidia
GPUIntelAMD
)
// Session represents a Docker container session
type Session struct {
client *client.Client
containerID string
imageName string
config *SessionConfig
mu sync.RWMutex
isRunning bool
}
// SessionConfig holds the configuration for the session
type SessionConfig struct {
Room string
Resolution string
Framerate string
RelayURL string
Params string
GamePath string
}
// NewSession creates a new Docker session
func NewSession(config *SessionConfig) (*Session, error) {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, fmt.Errorf("failed to create Docker client: %v", err)
}
return &Session{
client: cli,
imageName: "archlinux", //"ghcr.io/datcaptainhorse/nestri-cachyos:latest-noavx2",
config: config,
}, nil
}
// Start initiates the Docker container session
func (s *Session) Start(ctx context.Context) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.isRunning {
return fmt.Errorf("session is already running")
}
// Detect GPU type
gpuType := detectGPU()
if gpuType == GPUNone {
return fmt.Errorf("no supported GPU detected")
}
// Get GPU-specific configurations
deviceRequests, err := getGPUDeviceRequests(gpuType)
if err != nil {
return err
}
devices := getGPUDevices(gpuType)
// Check if image exists locally
_, _, err = s.client.ImageInspectWithRaw(ctx, s.imageName)
if err != nil {
// Pull the image if it doesn't exist
reader, err := s.client.ImagePull(ctx, s.imageName, image.PullOptions{})
if err != nil {
return fmt.Errorf("failed to pull image: %v", err)
}
defer reader.Close()
// Copy pull output to stdout
io.Copy(os.Stdout, reader)
}
// Create container
resp, err := s.client.ContainerCreate(ctx, &container.Config{
Image: s.imageName,
Env: []string{
fmt.Sprintf("NESTRI_ROOM=%s", s.config.Room),
fmt.Sprintf("RESOLUTION=%s", s.config.Resolution),
fmt.Sprintf("NESTRI_PARAMS=%s", s.config.Params),
fmt.Sprintf("FRAMERATE=%s", s.config.Framerate),
fmt.Sprintf("RELAY_URL=%s", s.config.RelayURL),
},
}, &container.HostConfig{
Binds: []string{
fmt.Sprintf("%s:/home/nestri/.steam/", s.config.GamePath),
},
Resources: container.Resources{
DeviceRequests: deviceRequests,
Devices: devices,
},
SecurityOpt: []string{"label=disable"},
ShmSize: 5368709120, // 5GB
// ShmSize: 1073741824, // 1GB
}, nil, nil, "")
if err != nil {
return fmt.Errorf("failed to create container: %v", err)
}
// Start container
if err := s.client.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
return fmt.Errorf("failed to start container: %v", err)
}
// Store container ID and update state
s.containerID = resp.ID
s.isRunning = true
// Start logging in a goroutine
go s.streamLogs(ctx)
return nil
}
// Stop stops the Docker container session
func (s *Session) Stop(ctx context.Context) error {
s.mu.Lock()
defer s.mu.Unlock()
if !s.isRunning {
return fmt.Errorf("session is not running")
}
timeout := 30 // seconds
if err := s.client.ContainerStop(ctx, s.containerID, container.StopOptions{Timeout: &timeout}); err != nil {
return fmt.Errorf("failed to stop container: %v", err)
}
if err := s.client.ContainerRemove(ctx, s.containerID, container.RemoveOptions{}); err != nil {
return fmt.Errorf("failed to remove container: %v", err)
}
s.isRunning = false
s.containerID = ""
return nil
}
// IsRunning returns the current state of the session
func (s *Session) IsRunning() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.isRunning
}
// GetContainerID returns the current container ID
func (s *Session) GetContainerID() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.containerID
}
// streamLogs streams container logs to stdout
func (s *Session) streamLogs(ctx context.Context) {
opts := container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
}
logs, err := s.client.ContainerLogs(ctx, s.containerID, opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting container logs: %v\n", err)
return
}
defer logs.Close()
_, err = io.Copy(os.Stdout, logs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error streaming logs: %v\n", err)
}
}
// VerifyEnvironment checks if all expected environment variables are set correctly in the container
func (s *Session) VerifyEnvironment(ctx context.Context) error {
s.mu.RLock()
defer s.mu.RUnlock()
if !s.isRunning {
return fmt.Errorf("session is not running")
}
// Get container info to verify it's actually running
inspect, err := s.client.ContainerInspect(ctx, s.containerID)
if err != nil {
return fmt.Errorf("failed to inspect container: %v", err)
}
if !inspect.State.Running {
return fmt.Errorf("container is not in running state")
}
// Expected environment variables
expectedEnv := map[string]string{
"NESTRI_ROOM": s.config.Room,
"RESOLUTION": s.config.Resolution,
"FRAMERATE": s.config.Framerate,
"RELAY_URL": s.config.RelayURL,
"NESTRI_PARAMS": s.config.Params,
}
// Get actual environment variables from container
containerEnv := make(map[string]string)
for _, env := range inspect.Config.Env {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
containerEnv[parts[0]] = parts[1]
}
}
// Check each expected variable
var missingVars []string
var mismatchedVars []string
for key, expectedValue := range expectedEnv {
actualValue, exists := containerEnv[key]
if !exists {
missingVars = append(missingVars, key)
} else if actualValue != expectedValue {
mismatchedVars = append(mismatchedVars, fmt.Sprintf("%s (expected: %s, got: %s)",
key, expectedValue, actualValue))
}
}
// Build error message if there are any issues
if len(missingVars) > 0 || len(mismatchedVars) > 0 {
var errorMsg strings.Builder
if len(missingVars) > 0 {
errorMsg.WriteString(fmt.Sprintf("Missing environment variables: %s\n",
strings.Join(missingVars, ", ")))
}
if len(mismatchedVars) > 0 {
errorMsg.WriteString(fmt.Sprintf("Mismatched environment variables: %s",
strings.Join(mismatchedVars, ", ")))
}
return fmt.Errorf(errorMsg.String())
}
return nil
}
// GetEnvironment returns all environment variables in the container
func (s *Session) GetEnvironment(ctx context.Context) (map[string]string, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if !s.isRunning {
return nil, fmt.Errorf("session is not running")
}
inspect, err := s.client.ContainerInspect(ctx, s.containerID)
if err != nil {
return nil, fmt.Errorf("failed to inspect container: %v", err)
}
env := make(map[string]string)
for _, e := range inspect.Config.Env {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 2 {
env[parts[0]] = parts[1]
}
}
return env, nil
}

View File

@@ -0,0 +1,76 @@
package session
import (
"bytes"
"context"
"fmt"
"io"
"strings"
"github.com/docker/docker/api/types/container"
)
// ExecResult holds the output from a container command
type ExecResult struct {
ExitCode int
Stdout string
Stderr string
}
func (s *Session) execInContainer(ctx context.Context, cmd []string) (*ExecResult, error) {
execConfig := container.ExecOptions{
Cmd: cmd,
AttachStdout: true,
AttachStderr: true,
}
execID, err := s.client.ContainerExecCreate(ctx, s.containerID, execConfig)
if err != nil {
return nil, err
}
resp, err := s.client.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{})
if err != nil {
return nil, err
}
defer resp.Close()
var outBuf bytes.Buffer
_, err = io.Copy(&outBuf, resp.Reader)
if err != nil {
return nil, err
}
inspect, err := s.client.ContainerExecInspect(ctx, execID.ID)
if err != nil {
return nil, err
}
return &ExecResult{
ExitCode: inspect.ExitCode,
Stdout: outBuf.String(),
}, nil
}
// CheckSteamGames returns the list of installed games in the container
func (s *Session) CheckInstalledSteamGames(ctx context.Context) ([]uint64, error) {
result, err := s.execInContainer(ctx, []string{
"sh", "-c",
"find /home/nestri/.steam/steam/steamapps -name '*.acf' -exec grep -H '\"appid\"' {} \\;",
})
if err != nil {
return nil, fmt.Errorf("failed to check steam games: %v", err)
}
var gameIDs []uint64
for _, line := range strings.Split(result.Stdout, "\n") {
if strings.Contains(line, "appid") {
var id uint64
if _, err := fmt.Sscanf(line, `"appid" "%d"`, &id); err == nil {
gameIDs = append(gameIDs, id)
}
}
}
return gameIDs, nil
}

View File

@@ -0,0 +1,72 @@
package session
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/docker/docker/api/types/container"
)
// detectGPU checks for available GPU type
func detectGPU() GPUType {
// First check for NVIDIA
cmd := exec.Command("nvidia-smi")
if err := cmd.Run(); err == nil {
return GPUNvidia
}
// Check for Intel/AMD GPU by looking for DRI devices
if _, err := os.Stat("/dev/dri"); err == nil {
return GPUIntelAMD
}
return GPUNone
}
// getGPUDeviceRequests returns appropriate device configuration based on GPU type
func getGPUDeviceRequests(gpuType GPUType) ([]container.DeviceRequest, error) {
switch gpuType {
case GPUNvidia:
return []container.DeviceRequest{
{
Driver: "nvidia",
Count: 1,
DeviceIDs: []string{"0"},
Capabilities: [][]string{{"gpu"}},
},
}, nil
case GPUIntelAMD:
return []container.DeviceRequest{}, nil // Empty as we'll handle this in Devices
default:
return nil, fmt.Errorf("no supported GPU detected")
}
}
// getGPUDevices returns appropriate device mappings based on GPU type
func getGPUDevices(gpuType GPUType) []container.DeviceMapping {
if gpuType == GPUIntelAMD {
devices := []container.DeviceMapping{}
// Only look for card and renderD nodes
for _, pattern := range []string{"card[0-9]*", "renderD[0-9]*"} {
matches, err := filepath.Glob(fmt.Sprintf("/dev/dri/%s", pattern))
if err != nil {
continue
}
for _, match := range matches {
// Verify it's a device file
if info, err := os.Stat(match); err == nil && (info.Mode()&os.ModeDevice) != 0 {
devices = append(devices, container.DeviceMapping{
PathOnHost: match,
PathInContainer: match,
CgroupPermissions: "rwm",
})
}
}
}
return devices
}
return nil
}

58
packages/cli/main.go Normal file
View File

@@ -0,0 +1,58 @@
package main
import (
"context"
"nestrilabs/cli/internal/session"
"github.com/charmbracelet/log"
)
func main() {
// err := cmd.Execute()
// if err != nil {
// log.Error("Error running the cmd command", "err", err)
// }
ctx := context.Background()
config := &session.SessionConfig{
Room: "victortest",
Resolution: "1920x1080",
Framerate: "60",
RelayURL: "https://relay.dathorse.com",
Params: "--verbose=true --video-codec=h264 --video-bitrate=4000 --video-bitrate-max=6000 --gpu-card-path=/dev/dri/card1",
GamePath: "/path/to/your/game",
}
sess, err := session.NewSession(config)
if err != nil {
log.Error("Failed to create session", "err", err)
}
// Start the session
if err := sess.Start(ctx); err != nil {
log.Error("Failed to start session", "err", err)
}
// Check if it's running
if sess.IsRunning() {
log.Info("Session is running with container ID", "containerId", sess.GetContainerID())
}
env, err := sess.GetEnvironment(ctx)
if err != nil {
log.Printf("Failed to get environment: %v", err)
} else {
for key, value := range env {
log.Info("Found this environment variables", key, value)
}
}
// Let it run for a while
// time.Sleep(time.Second * 50)
// Stop the session
if err := sess.Stop(ctx); err != nil {
log.Error("Failed to stop session", "err", err)
}
}