Skip to content

Commit b8cdcaa

Browse files
committed
Merge branch 'main' into makeshellfast
2 parents cfdc4ae + 5e6c288 commit b8cdcaa

24 files changed

Lines changed: 664 additions & 464 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ endif
2525
fast-build: ## go build -o brev
2626
$(call print-target)
2727
echo ${VERSION}
28-
CGO_ENABLED=1 go build -o brev -ldflags "-X github.com/brevdev/brev-cli/pkg/cmd/version.Version=${VERSION}"
28+
$(_BUILD_PREFIX) go build -o brev -ldflags "-X github.com/brevdev/brev-cli/pkg/cmd/version.Version=${VERSION}"
2929

3030
.PHONY: local
3131
local: ## build with env wrapper (use: make local env=dev0|dev1|dev2|stg arch=linux/amd64, or make local for defaults)

docs/CONTRIBUTING.md

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
```
1212
VERSION=unknown
13-
BREV_API_URL=http://localhost:8080
13+
BREV_API_URL=http://localhost:8080
1414
# BREV_API_URL=<your backend>
1515
```
1616

@@ -89,15 +89,6 @@ index a33540c..b03d5f2 100644
8989
func hasHousekeepingCommands(cmd *cobra.Command) bool {
9090
```
9191

92-
test your function
93-
94-
```
95-
make && ./brev recreate
96-
```
97-
98-
add documentation by editing `pkg/cmd/recreate/doc.md`. Docs should fill out the
99-
minimum fields:
100-
10192
```
10293
<!-- Insert title here -->
10394
#

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module github.com/brevdev/brev-cli
33
go 1.25.0
44

55
require (
6-
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.20.0-20260521231113-5bd61a2e035f.1
7-
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260521231113-5bd61a2e035f.1
6+
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.20.0-20260626205643-49b0d20e08f1.1
7+
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260626205643-49b0d20e08f1.1
88
connectrpc.com/connect v1.20.0
99
github.com/NVIDIA/go-nvml v0.13.0-1
1010
github.com/alessio/shellescape v1.4.1

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.20.0-20260521231113-5bd61a2e035f.1 h1:p2gDnCmIeMzMuRNP05Jh143Q8iiSq0/oXG8eckzCkSY=
2-
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.20.0-20260521231113-5bd61a2e035f.1/go.mod h1:CwGL+2J9G36DvGlMYW/5f+LTnGAOGJPcAw3S/Zy7lbk=
3-
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260521231113-5bd61a2e035f.1 h1:NyJ55L5BmM+AOC77hUrLysVvzU4m9YO+g93YwvZS3Y4=
4-
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260521231113-5bd61a2e035f.1/go.mod h1:V/y7Wxg0QvU4XPVwqErF5NHLobUT1QEyfgrGuQIxdPo=
1+
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.20.0-20260626205643-49b0d20e08f1.1 h1:Qj4BTbhIF0KE5YHiJJ+SN2goGYF8dJC1l8cv69YU/Ms=
2+
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.20.0-20260626205643-49b0d20e08f1.1/go.mod h1:KW+lsYUmrF994Z/zj/wibrS7zhitXrYLicqR5BbVSp0=
3+
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260626205643-49b0d20e08f1.1 h1:+GNKe6qV3aRH+N/FBlH6NfqyKOxMecAtbHndj3NPZc4=
4+
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260626205643-49b0d20e08f1.1/go.mod h1:V/y7Wxg0QvU4XPVwqErF5NHLobUT1QEyfgrGuQIxdPo=
55
buf.build/gen/go/brevdev/protoc-gen-gotag/protocolbuffers/go v1.36.11-20220906235457-8b4922735da5.1 h1:6amhprQmCKJ4wgJ6ngkh32d9V+dQcOLUZ/SfHdOnYgo=
66
buf.build/gen/go/brevdev/protoc-gen-gotag/protocolbuffers/go v1.36.11-20220906235457-8b4922735da5.1/go.mod h1:O+pnSHMru/naTMrm4tmpBoH3wz6PHa+R75HR7Mv8X2g=
77
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=

pkg/cmd/cmd.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import (
3838
"github.com/brevdev/brev-cli/pkg/cmd/portforward"
3939
"github.com/brevdev/brev-cli/pkg/cmd/profile"
4040
"github.com/brevdev/brev-cli/pkg/cmd/proxy"
41-
"github.com/brevdev/brev-cli/pkg/cmd/recreate"
4241
"github.com/brevdev/brev-cli/pkg/cmd/redeem"
4342
"github.com/brevdev/brev-cli/pkg/cmd/refresh"
4443
"github.com/brevdev/brev-cli/pkg/cmd/register"
@@ -58,7 +57,6 @@ import (
5857
"github.com/brevdev/brev-cli/pkg/cmd/updatemodel"
5958
"github.com/brevdev/brev-cli/pkg/cmd/upgrade"
6059
"github.com/brevdev/brev-cli/pkg/cmd/version"
61-
"github.com/brevdev/brev-cli/pkg/cmd/workspacegroups"
6260
"github.com/brevdev/brev-cli/pkg/config"
6361
"github.com/brevdev/brev-cli/pkg/entity"
6462
"github.com/brevdev/brev-cli/pkg/featureflag"
@@ -308,7 +306,6 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor
308306
} else {
309307
_ = 0 // noop
310308
}
311-
cmd.AddCommand(workspacegroups.NewCmdWorkspaceGroups(t, loginCmdStore))
312309
cmd.AddCommand(scale.NewCmdScale(t, noLoginCmdStore))
313310
cmd.AddCommand(gpusearch.NewCmdGPUSearch(t, noLoginCmdStore))
314311
cmd.AddCommand(gpucreate.NewCmdGPUCreate(t, loginCmdStore))
@@ -341,7 +338,6 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor
341338
cmd.AddCommand(healthcheck.NewCmdHealthcheck(t, noLoginCmdStore))
342339

343340
cmd.AddCommand(setupworkspace.NewCmdSetupWorkspace(noLoginCmdStore))
344-
cmd.AddCommand(recreate.NewCmdRecreate(t, loginCmdStore))
345341
cmd.AddCommand(updatemodel.NewCmdupdatemodel(t, loginCmdStore))
346342
cmd.AddCommand(feedback.NewCmdFeedback(t, noLoginCmdStore))
347343
}

pkg/cmd/cmderrors/cmderrors.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ func DisplayAndHandleError(err error) {
2929
if err != nil {
3030
t := terminal.New()
3131
prettyErr := ""
32+
// showTrace gates whether we dump the full wrapped error (with stack
33+
// frames) in dev/debug builds. Only unexpected, Sentry-reported errors
34+
// warrant a trace; expected user-facing errors always print cleanly.
35+
showTrace := false
3236
switch errors.Cause(err).(type) {
3337
case breverrors.ValidationError:
3438
// do not report error
@@ -65,9 +69,10 @@ func DisplayAndHandleError(err error) {
6569
} else {
6670
er.ReportError(err)
6771
prettyErr = (t.Red(errors.Cause(err).Error()))
72+
showTrace = true
6873
}
6974
}
70-
if featureflag.Debug() || featureflag.IsDev() {
75+
if showTrace && (featureflag.Debug() || featureflag.IsDev()) {
7176
fmt.Fprintln(os.Stderr, err)
7277
} else {
7378
fmt.Fprintln(os.Stderr, prettyErr)

pkg/cmd/gpucreate/gpucreate.go

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77
"io"
88
"math/rand/v2"
9+
"net"
10+
"net/http"
911
"net/url"
1012
"os"
1113
"strconv"
@@ -1181,10 +1183,13 @@ func applyLaunchableConfig(cwOptions *store.CreateWorkspacesOptions, launchableI
11811183
cwOptions.WorkspaceGroupID = wsReq.WorkspaceGroupID
11821184
}
11831185

1184-
// Location
1186+
// Location / sub-location
11851187
if wsReq.Location != "" {
11861188
cwOptions.Location = wsReq.Location
11871189
}
1190+
if wsReq.SubLocation != "" {
1191+
cwOptions.SubLocation = wsReq.SubLocation
1192+
}
11881193

11891194
// Disk storage — the API may return a bare number (e.g., "256") or with
11901195
// a unit suffix (e.g., "256Gi"). The server's ParseDiskStorage expects a
@@ -1215,6 +1220,10 @@ func applyLaunchableConfig(cwOptions *store.CreateWorkspacesOptions, launchableI
12151220
cwOptions.PortMappings = portMappings
12161221
}
12171222

1223+
if len(wsReq.FirewallRules) > 0 {
1224+
cwOptions.FirewallRules = resolveFirewallRulesClientIP(wsReq.FirewallRules, publicIPLookup)
1225+
}
1226+
12181227
// Files from launchable
12191228
if info.File != nil {
12201229
cwOptions.Files = []map[string]string{
@@ -1238,6 +1247,98 @@ func applyLaunchableConfig(cwOptions *store.CreateWorkspacesOptions, launchableI
12381247
cwOptions.Labels = labels
12391248
}
12401249

1250+
// resolveFirewallRulesClientIP fills ClientIPs on any "user-ip" rule that
1251+
// doesn't already have one, calling lookupIP at most once. Rules are left
1252+
// unchanged on lookup failure or unparseable IPs.
1253+
func resolveFirewallRulesClientIP(rules []store.CreateFirewallRule, lookupIP func() (string, error)) []store.CreateFirewallRule {
1254+
out := make([]store.CreateFirewallRule, len(rules))
1255+
copy(out, rules)
1256+
1257+
var (
1258+
ip string
1259+
ipErr error
1260+
looked bool
1261+
)
1262+
for i := range out {
1263+
if out[i].AllowedIPs != "user-ip" || len(out[i].ClientIPs) > 0 {
1264+
continue
1265+
}
1266+
if !looked {
1267+
ip, ipErr = lookupIP()
1268+
looked = true
1269+
}
1270+
if ipErr != nil || ip == "" {
1271+
continue
1272+
}
1273+
cidr := toHostCIDR(ip)
1274+
if cidr == "" {
1275+
continue
1276+
}
1277+
out[i].ClientIPs = []string{cidr}
1278+
}
1279+
return out
1280+
}
1281+
1282+
// toHostCIDR returns the single-host CIDR for an IP literal: /32 for IPv4,
1283+
// /128 for IPv6. Returns "" if raw isn't a valid IP.
1284+
func toHostCIDR(raw string) string {
1285+
parsed := net.ParseIP(strings.TrimSpace(raw))
1286+
if parsed == nil {
1287+
return ""
1288+
}
1289+
if v4 := parsed.To4(); v4 != nil {
1290+
return v4.String() + "/32"
1291+
}
1292+
return parsed.String() + "/128"
1293+
}
1294+
1295+
// publicIPLookup is a var so tests can stub it.
1296+
var publicIPLookup = resolvePublicIP
1297+
1298+
// publicIPEndpoints are tried in order until one returns a valid IP.
1299+
// All return the IP as a plain-text body.
1300+
var publicIPEndpoints = []string{
1301+
"https://api.ipify.org",
1302+
"https://ifconfig.me/ip",
1303+
"https://checkip.amazonaws.com",
1304+
}
1305+
1306+
func resolvePublicIP() (string, error) {
1307+
client := &http.Client{Timeout: 3 * time.Second}
1308+
var lastErr error
1309+
for _, url := range publicIPEndpoints {
1310+
ip, err := fetchPublicIP(client, url)
1311+
if err == nil {
1312+
return ip, nil
1313+
}
1314+
lastErr = err
1315+
}
1316+
if lastErr == nil {
1317+
lastErr = fmt.Errorf("no public IP endpoints configured")
1318+
}
1319+
return "", lastErr
1320+
}
1321+
1322+
func fetchPublicIP(client *http.Client, url string) (string, error) {
1323+
resp, err := client.Get(url)
1324+
if err != nil {
1325+
return "", breverrors.WrapAndTrace(err)
1326+
}
1327+
defer resp.Body.Close() //nolint:errcheck // best-effort
1328+
if resp.StatusCode != http.StatusOK {
1329+
return "", fmt.Errorf("%s returned status %d", url, resp.StatusCode)
1330+
}
1331+
body, err := io.ReadAll(io.LimitReader(resp.Body, 64))
1332+
if err != nil {
1333+
return "", breverrors.WrapAndTrace(err)
1334+
}
1335+
ipStr := strings.TrimSpace(string(body))
1336+
if net.ParseIP(ipStr) == nil {
1337+
return "", fmt.Errorf("%s returned non-IP response: %q", url, ipStr)
1338+
}
1339+
return ipStr, nil
1340+
}
1341+
12411342
// normalizeDiskStorage ensures a disk storage value has a Kubernetes quantity suffix.
12421343
// If the value is purely numeric (e.g., "256"), appends "Gi". Otherwise passes through
12431344
// as-is, trusting the server's ParseDiskStorage to handle formats like "256Gi", "100G", etc.

0 commit comments

Comments
 (0)