Skip to content

feat(BRE2-736): add ability to register device to Brev#295

Merged
patelspratik merged 5 commits intomainfrom
BRE2-736-register
Mar 4, 2026
Merged

feat(BRE2-736): add ability to register device to Brev#295
patelspratik merged 5 commits intomainfrom
BRE2-736-register

Conversation

@patelspratik
Copy link
Contributor

@patelspratik patelspratik commented Feb 25, 2026

  • Add ability to register/deregister device as External Node. Device should be a linux device (e.g. Spark).
  • Uses NetBird, not Cloudflare for Tunnel capability
  • Uses ExternalNode RPC Connect API in dev-plane, NOT brevapi
  • Add ability to grant SSH access to other users
  • Add ability to list external nodes
  • Extremely lightweight, no heartbeats

@patelspratik patelspratik force-pushed the BRE2-736-register branch 2 times, most recently from ac1ac18 to f41d40a Compare March 2, 2026 19:09
@patelspratik patelspratik marked this pull request as ready for review March 2, 2026 23:48
@patelspratik patelspratik requested a review from a team as a code owner March 2, 2026 23:48
@brevdev brevdev deleted a comment from codecov bot Mar 3, 2026
@@ -1,44 +1,320 @@
// Package register provides the brev register command for DGX Spark registration
// Package register provides the brev register command for device registration
Copy link
Contributor

Choose a reason for hiding this comment

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

🤘

return breverrors.WrapAndTrace(err)
}

u, _ := user.Current()
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this not error?

Also, could we call this the osUser or something similar?

Comment on lines +50 to +53
// Confirmer prompts for yes/no confirmation.
type Confirmer interface {
ConfirmYesNo(label string) bool
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if this + the Selector is pointing us to having more of a "Terminal" or "Prompter" interface, as I assume they would generally be satisfied by the same struct.

Comment on lines +158 to +162
t.Vprint(t.Yellow("[Step 1/3] Installing NetBird..."))
if err := deps.netbird.Install(); err != nil {
return fmt.Errorf("NetBird installation failed: %w", err)
}
t.Vprint(t.Green(" NetBird installed successfully."))
Copy link
Contributor

Choose a reason for hiding this comment

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

For later discussion, but I do wonder about just saying "we are installing the tunnel" (or whatever). Maybe this will always be netbird, but it feels like a little bit of a behind the scenes detail.

Comment on lines +206 to +212
if ci := node.GetConnectivityInfo(); ci != nil {
if cmd := ci.GetRegistrationCommand(); cmd != "" {
if err := deps.setupRunner.RunSetup(cmd); err != nil {
t.Vprintf(" Warning: setup command failed: %v\n", err)
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we react to any of these else cases? ci == nil, cmd == ""?

pkg/cmd/ls/ls.go Outdated
Comment on lines +764 to +767
if ci := n.GetConnectivityInfo(); ci != nil && ci.GetRegistrationCommand() != "" {
return "REGISTERED"
}
return "UNKNOWN"
Copy link
Contributor

Choose a reason for hiding this comment

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

We now have NetworkMemberStatus


// runSetupCommand executes the setup command returned by the AddNode RPC.
func runSetupCommand(script string) error {
cmd := exec.Command("bash", "-c", script) // #nosec G204
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should minimally check to see that this command starts with netbird up

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't mind that.

Comment on lines +36 to +39
// PlatformChecker checks whether the current platform is supported.
type PlatformChecker interface {
IsCompatible() bool
}
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this same interface is defined several times -- I wonder if we should just have something more general-purpose in the register package here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ya, I'm thinking of pulling out like an "interfaces.go" inside of one of these packages or something.

Comment on lines +46 to +49
// NodeClientFactory creates ConnectRPC ExternalNodeService clients.
type NodeClientFactory interface {
NewNodeClient(provider register.TokenProvider, baseURL string) nodev1connect.ExternalNodeServiceClient
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above -- do we need four different versions of these interfaces?

Comment on lines +39 to +42
// NetBirdUninstaller uninstalls the NetBird network agent.
type NetBirdUninstaller interface {
Uninstall() error
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we just have a NetBirdManager (rather than an Installer and Uninstaller)? I don't know that we benefit from individual interfaces here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ya, I was debating whether it was worth to have this separated out. No strong preference so less code is best code, will fix.

…emory auth store, added reconnect logic, renamed registration, tests
case nodev1.NetworkMemberStatus_NETWORK_MEMBER_STATUS_DISCONNECTED:
return "Disconnected"
case nodev1.NetworkMemberStatus_NETWORK_MEMBER_STATUS_UNSPECIFIED:
return "Registered"
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably not a bad idea, but "unspecified" is akin to an erroneous state, so this may be fine to put down as "Unknown"

if err != nil {
t.Vprintf(" %s\n", t.Yellow(fmt.Sprintf("Warning: could not fetch node status: %v", err)))
} else {
ci := resp.Msg.GetExternalNode().GetConnectivityInfo()
Copy link
Contributor

Choose a reason for hiding this comment

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

Possible nil deref here on GetExternalNode

t.Vprint(" Run 'brev deregister' first if you want to re-register.")
return nil
}
t.Vprintf(" Node status: %s\n", externalnode.FriendlyNetworkStatus(ci.GetStatus()))
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar here as ci could be nil


// If the key exists but isn't tagged, replace it with the tagged version
// so that RemoveBrevAuthorizedKeys can find it later.
if strings.Contains(string(existing), pubKey) {
Copy link
Contributor

Choose a reason for hiding this comment

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

While this would be crazy if it led to an actual bug, I wonder if for safety we should do an exact match rather than a Contains

return nil, fmt.Errorf("brev register is only supported on Linux")
}

_, err := s.GetCurrentUser() // ensure active token
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should unwind this a bit so that runRegister itself does:

  • check platform is compatible
  • check if registration is already complete
  • get current user
  • get current org
  • ...

Right now it looks like:

  • check platform
  • get current user
  • get org
  • check if registration is already complete
  • get current user (again!)
  • ..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ah ya didn't fix after my machinations


// EnableSSH grants SSH access to the given node for the specified Brev user.
// It is exported so that the register command can reuse it after registration.
func EnableSSH(
Copy link
Contributor

Choose a reason for hiding this comment

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

As per this comment, it looks like the register command has its own (mostly identical) copy of this function. Did you intend to use this from there?

authKeysPath := filepath.Join(u.HomeDir, ".ssh", "authorized_keys")
existing, err := os.ReadFile(authKeysPath) // #nosec G304
if err != nil {
return fmt.Errorf("SSH has not been enabled on this device. Run 'brev enable-ssh' first.")
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this be due to read permissions instead of the nonexistence of keys?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh ya this is a bad error message, fixing


removeTunnel := deps.prompter.Select(
"Would you also like to remove the Brev tunnel?",
[]string{"Yes, remove Brev tunnel", "No, keep Brev tunnel installed"},
Copy link
Member

Choose a reason for hiding this comment

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

why would you want to keep the brev tunnel installed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

when it was called netbird it made more sense, I agree it now does not

// unquote removes surrounding double quotes from a string.
func unquote(s string) string {
s = strings.TrimSpace(s)
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
Copy link
Member

Choose a reason for hiding this comment

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

can be simplified
return strings.TrimPrefix(strings.TrimSuffix(s, "), ")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oops missed this, will put it in the next

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

return nil, breverrors.WrapAndTrace(err)
}

func getOrgToRegisterFor(s RegisterStore) (*entity.Organization, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This function name ends in a preposition and you should feel bad.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#uninstall

@patelspratik patelspratik merged commit 88dc335 into main Mar 4, 2026
9 checks passed
@patelspratik patelspratik deleted the BRE2-736-register branch March 4, 2026 00:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants