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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dist/
# binary
brev-cli
brev
brev-local

# golang executable
go1.*
Expand Down
9 changes: 5 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,25 @@ fast-build: ## go build -o brev
CGO_ENABLED=0 go build -o brev -ldflags "-X github.com/brevdev/brev-cli/pkg/cmd/version.Version=${VERSION}"

.PHONY: local
local: ## build with env wrapper (use: make local env=dev0|dev1|dev2|stg, or make local for defaults)
local: ## build with env wrapper (use: make local env=dev0|dev1|dev2|stg arch=linux/amd64, or make local for defaults)
$(call print-target)
ifdef env
@echo "Building with env=$(env) wrapper..."
@echo ${VERSION}
CGO_ENABLED=0 go build -o brev -ldflags "-X github.com/brevdev/brev-cli/pkg/cmd/version.Version=${VERSION}"
$(if $(arch),GOOS=$(word 1,$(subst /, ,$(arch))) GOARCH=$(word 2,$(subst /, ,$(arch))),) CGO_ENABLED=0 go build -o brev-local -ldflags "-X github.com/brevdev/brev-cli/pkg/cmd/version.Version=${VERSION}"
@echo '#!/bin/sh' > brev
@echo '# Auto-generated wrapper with environment overrides' >> brev
@echo 'export BREV_CONSOLE_URL="https://localhost.nvidia.com:3000"' >> brev
@echo 'export BREV_AUTH_URL="https://api.stg.ngc.nvidia.com"' >> brev
@echo 'export BREV_AUTH_ISSUER_URL="https://stg.login.nvidia.com"' >> brev
@echo 'export BREV_API_URL="https://bd.$(env).brev.nvidia.com"' >> brev
@echo 'export BREV_PUBLIC_API_URL="https://api.$(env).brev.nvidia.com"' >> brev
@echo 'export BREV_GRPC_URL="api.$(env).brev.nvidia.com:443"' >> brev
@echo 'exec "$$(cd "$$(dirname "$$0")" && pwd)/brev" "$$@"' >> brev
@echo 'exec "$$(cd "$$(dirname "$$0")" && pwd)/brev-local" "$$@"' >> brev
@chmod +x brev
else
@echo "Building without environment overrides (using config.go defaults)..."
$(MAKE) fast-build
$(if $(arch),GOOS=$(word 1,$(subst /, ,$(arch))) GOARCH=$(word 2,$(subst /, ,$(arch))),) CGO_ENABLED=0 go build -o brev -ldflags "-X github.com/brevdev/brev-cli/pkg/cmd/version.Version=${VERSION}"
endif

.PHONY: install-dev
Expand Down
8 changes: 6 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ module github.com/brevdev/brev-cli
go 1.24.0

require (
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260228021043-887d38e1b474.2
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260228021043-887d38e1b474.1
connectrpc.com/connect v1.19.1
github.com/alessio/shellescape v1.4.1
github.com/brevdev/parse v0.0.11
github.com/briandowns/spinner v1.16.0
Expand All @@ -12,7 +15,7 @@ require (
github.com/go-git/go-git/v5 v5.13.2
github.com/go-resty/resty/v2 v2.17.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/go-cmp v0.6.0
github.com/google/go-cmp v0.7.0
github.com/google/huproxy v0.0.0-20210816191033-a131ee126ce3
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.0
Expand Down Expand Up @@ -45,6 +48,7 @@ require (
)

require (
buf.build/gen/go/brevdev/protoc-gen-gotag/protocolbuffers/go v1.36.11-20220906235457-8b4922735da5.1 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
Expand Down Expand Up @@ -148,7 +152,7 @@ require (
golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/protobuf v1.34.2
google.golang.org/protobuf v1.36.11
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
16 changes: 12 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260228021043-887d38e1b474.2 h1:Sq0kIa/xKzScbJcqB5EbPVhOL0QYHPr3araQaupL2lk=
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260228021043-887d38e1b474.2/go.mod h1:Yh34p9aADmWsKv2umYlMpnCZuBmNBE9N+HImgRriJXM=
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260228021043-887d38e1b474.1 h1:WlSch6mGiV/gO+vq6y0Ut+HO2ffFHsLhTI3lVWdO0bI=
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260228021043-887d38e1b474.1/go.mod h1:V/y7Wxg0QvU4XPVwqErF5NHLobUT1QEyfgrGuQIxdPo=
buf.build/gen/go/brevdev/protoc-gen-gotag/protocolbuffers/go v1.36.11-20220906235457-8b4922735da5.1 h1:6amhprQmCKJ4wgJ6ngkh32d9V+dQcOLUZ/SfHdOnYgo=
buf.build/gen/go/brevdev/protoc-gen-gotag/protocolbuffers/go v1.36.11-20220906235457-8b4922735da5.1/go.mod h1:O+pnSHMru/naTMrm4tmpBoH3wz6PHa+R75HR7Mv8X2g=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
Expand Down Expand Up @@ -35,6 +41,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
Expand Down Expand Up @@ -208,8 +216,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
Expand Down Expand Up @@ -776,8 +784,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
5 changes: 5 additions & 0 deletions pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ func (t *Auth) WithAccessTokenValidator(val func(string) (bool, error)) *Auth {
return t
}

func (t *Auth) WithShouldLogin(fn func() (bool, error)) *Auth {
t.shouldLogin = fn
return t
}

// Gets fresh access token and prompts for login and saves to store
func (t Auth) GetFreshAccessTokenOrLogin() (string, error) {
token, err := t.GetFreshAccessTokenOrNil()
Expand Down
34 changes: 31 additions & 3 deletions pkg/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ import (
"github.com/brevdev/brev-cli/pkg/cmd/connect"
"github.com/brevdev/brev-cli/pkg/cmd/copy"
"github.com/brevdev/brev-cli/pkg/cmd/delete"
"github.com/brevdev/brev-cli/pkg/cmd/deregister"
"github.com/brevdev/brev-cli/pkg/cmd/enablessh"
"github.com/brevdev/brev-cli/pkg/cmd/envvars"
"github.com/brevdev/brev-cli/pkg/cmd/exec"
"github.com/brevdev/brev-cli/pkg/cmd/fu"
"github.com/brevdev/brev-cli/pkg/cmd/gpucreate"
"github.com/brevdev/brev-cli/pkg/cmd/gpusearch"
"github.com/brevdev/brev-cli/pkg/cmd/grantssh"
"github.com/brevdev/brev-cli/pkg/cmd/healthcheck"
"github.com/brevdev/brev-cli/pkg/cmd/hello"
"github.com/brevdev/brev-cli/pkg/cmd/importideconfig"
Expand Down Expand Up @@ -252,12 +255,33 @@ func NewBrevCommand() *cobra.Command { //nolint:funlen,gocognit,gocyclo // defin

cmds.SetUsageTemplate(usageTemplate)

createCmdTree(cmds, t, loginCmdStore, noLoginCmdStore, loginAuth)
// In-memory auth for external node commands — never touches credentials.json.
memAuthStore := store.NewMemoryAuthStore()
memAuthenticator := auth.StandardLogin("", "", nil)
memLoginAuth := auth.NewLoginAuth(memAuthStore, memAuthenticator)
memLoginAuth.WithShouldLogin(func() (bool, error) { return true, nil })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is nice


externalNodeCmdStore := fsStore.WithNoAuthHTTPClient(
store.NewNoAuthHTTPClient(conf.GetBrevAPIURl()),
).WithAuth(memLoginAuth, store.WithDebug(conf.GetDebugHTTP()))

err = externalNodeCmdStore.SetForbiddenStatusRetryHandler(func() error {
_, err1 := memLoginAuth.GetAccessToken()
if err1 != nil {
return breverrors.WrapAndTrace(err1)
}
return nil
})
if err != nil {
fmt.Printf("%v\n", err)
}

createCmdTree(cmds, t, loginCmdStore, noLoginCmdStore, loginAuth, externalNodeCmdStore)

return cmds
}

func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *store.AuthHTTPStore, noLoginCmdStore *store.AuthHTTPStore, loginAuth *auth.LoginAuth) { //nolint:funlen // define brev command
func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *store.AuthHTTPStore, noLoginCmdStore *store.AuthHTTPStore, loginAuth *auth.LoginAuth, externalNodeCmdStore *store.AuthHTTPStore) { //nolint:funlen // define brev command
cmd.AddCommand(set.NewCmdSet(t, loginCmdStore, noLoginCmdStore))
cmd.AddCommand(ls.NewCmdLs(t, loginCmdStore, noLoginCmdStore))
cmd.AddCommand(org.NewCmdOrg(t, loginCmdStore, noLoginCmdStore))
Expand Down Expand Up @@ -305,7 +329,10 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor
cmd.AddCommand(reset.NewCmdReset(t, loginCmdStore, noLoginCmdStore))
cmd.AddCommand(profile.NewCmdProfile(t, loginCmdStore, noLoginCmdStore))
cmd.AddCommand(refresh.NewCmdRefresh(t, loginCmdStore))
cmd.AddCommand(register.NewCmdRegister(t))
cmd.AddCommand(register.NewCmdRegister(t, externalNodeCmdStore))
cmd.AddCommand(deregister.NewCmdDeregister(t, externalNodeCmdStore))
cmd.AddCommand(enablessh.NewCmdEnableSSH(t, externalNodeCmdStore))
cmd.AddCommand(grantssh.NewCmdGrantSSH(t, externalNodeCmdStore))
cmd.AddCommand(runtasks.NewCmdRunTasks(t, noLoginCmdStore))
cmd.AddCommand(proxy.NewCmdProxy(t, noLoginCmdStore))
cmd.AddCommand(healthcheck.NewCmdHealthcheck(t, noLoginCmdStore))
Expand Down Expand Up @@ -525,4 +552,5 @@ var (
_ store.Auth = auth.LoginAuth{}
_ store.Auth = auth.NoLoginAuth{}
_ auth.AuthStore = store.FileStore{}
_ auth.AuthStore = &store.MemoryAuthStore{}
)
179 changes: 179 additions & 0 deletions pkg/cmd/deregister/deregister.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Package deregister provides the brev deregister command for device deregistration
package deregister

import (
"context"
"fmt"
"os/user"

nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1"
"connectrpc.com/connect"

"github.com/brevdev/brev-cli/pkg/cmd/register"
"github.com/brevdev/brev-cli/pkg/config"
"github.com/brevdev/brev-cli/pkg/entity"
breverrors "github.com/brevdev/brev-cli/pkg/errors"
"github.com/brevdev/brev-cli/pkg/externalnode"
"github.com/brevdev/brev-cli/pkg/terminal"

"github.com/spf13/cobra"
)

// DeregisterStore defines the store methods needed by the deregister command.
type DeregisterStore interface {
GetCurrentUser() (*entity.User, error)
GetBrevHomePath() (string, error)
GetAccessToken() (string, error)
}

// SSHKeyRemover removes Brev-managed SSH keys and returns the lines removed.
type SSHKeyRemover interface {
RemoveBrevKeys(u *user.User) ([]string, error)
}

// brevSSHKeyRemover delegates to register.RemoveBrevAuthorizedKeys.
type brevSSHKeyRemover struct{}

func (brevSSHKeyRemover) RemoveBrevKeys(u *user.User) ([]string, error) {
removed, err := register.RemoveBrevAuthorizedKeys(u)
if err != nil {
return nil, fmt.Errorf("removing brev authorized keys: %w", err)
}
return removed, nil
}

// deregisterDeps bundles the side-effecting dependencies of runDeregister so
// they can be replaced in tests.
type deregisterDeps struct {
platform externalnode.PlatformChecker
prompter terminal.Selector
netbird register.NetBirdManager
nodeClients externalnode.NodeClientFactory
registrationStore register.RegistrationStore
sshKeys SSHKeyRemover
}

func defaultDeregisterDeps(brevHome string) deregisterDeps {
return deregisterDeps{
platform: register.LinuxPlatform{},
prompter: register.TerminalPrompter{},
netbird: register.Netbird{},
nodeClients: register.DefaultNodeClientFactory{},
registrationStore: register.NewFileRegistrationStore(brevHome),
sshKeys: brevSSHKeyRemover{},
}
}

var (
deregisterLong = `Deregister your device from NVIDIA Brev

This command removes the local registration data and uninstalls
the Brev tunnel (network agent).`

deregisterExample = ` brev deregister`
)

func NewCmdDeregister(t *terminal.Terminal, store DeregisterStore) *cobra.Command {
cmd := &cobra.Command{
Annotations: map[string]string{"configuration": ""},
Use: "deregister",
DisableFlagsInUseLine: true,
Short: "Deregister your device from Brev",
Long: deregisterLong,
Example: deregisterExample,
RunE: func(cmd *cobra.Command, args []string) error {
brevHome, err := store.GetBrevHomePath()
if err != nil {
return breverrors.WrapAndTrace(err)
}
return runDeregister(cmd.Context(), t, store, defaultDeregisterDeps(brevHome))
},
}

return cmd
}

func runDeregister(ctx context.Context, t *terminal.Terminal, s DeregisterStore, deps deregisterDeps) error { //nolint:funlen // deregistration flow
if !deps.platform.IsCompatible() {
return fmt.Errorf("brev deregister is only supported on Linux")
}

registered, err := deps.registrationStore.Exists()
if err != nil {
return breverrors.WrapAndTrace(err)
}
if !registered {
return fmt.Errorf("no registration found; this machine does not appear to be registered\nRun 'brev register' to register your device")
}

reg, err := deps.registrationStore.Load()
if err != nil {
return fmt.Errorf("failed to read registration file: %w", err)
}

t.Vprint("")
t.Vprint(t.Green("Deregistering device"))
t.Vprint("")
t.Vprintf(" Node ID: %s\n", reg.ExternalNodeID)
t.Vprintf(" Name: %s\n", reg.DisplayName)
t.Vprint("")

confirm := deps.prompter.Select(
"Proceed with deregistration?",
[]string{"Yes, proceed", "No, cancel"},
)
if confirm != "Yes, proceed" {
t.Vprint("Deregistration canceled.")
return nil
}

t.Vprint("")
t.Vprint(t.Yellow("Removing node from Brev..."))
client := deps.nodeClients.NewNodeClient(s, config.GlobalConfig.GetBrevPublicAPIURL())
if _, err := client.RemoveNode(ctx, connect.NewRequest(&nodev1.RemoveNodeRequest{
ExternalNodeId: reg.ExternalNodeID,
})); err != nil {
return fmt.Errorf("failed to deregister node: %w", err)
}
t.Vprint(t.Green(" Node removed from Brev."))
t.Vprint("")

// Remove Brev SSH keys from authorized_keys.
osUser, err := user.Current()
if err != nil {
t.Vprintf(" Warning: could not determine current user for SSH key cleanup: %v\n", err)
} else {
removed, kerr := deps.sshKeys.RemoveBrevKeys(osUser)
switch {
case kerr != nil:
t.Vprintf(" Warning: failed to remove Brev SSH keys: %v\n", kerr)
case len(removed) > 0:
t.Vprint(t.Green(" Brev SSH keys removed from authorized_keys:"))
for _, key := range removed {
t.Vprintf(" - %s\n", key)
}
default:
t.Vprint(" No Brev SSH keys found in authorized_keys.")
}
}
t.Vprint("")

t.Vprint("Removing Brev tunnel...")
if err := deps.netbird.Uninstall(); err != nil {
t.Vprintf(" Warning: failed to remove Brev tunnel: %v\n", err)
} else {
t.Vprint(t.Green(" Brev tunnel removed."))
}
t.Vprint("")

t.Vprint("Removing registration data...")
if err := deps.registrationStore.Delete(); err != nil {
t.Vprintf(" Warning: failed to remove local registration file: %v\n", err)
t.Vprint(" You can manually remove it with: rm ~/.brev/device_registration.json")
}

t.Vprint(t.Green("Deregistration complete."))
t.Vprint("")

return nil
}
Loading
Loading